1/*
2Package store implements storage for accounts, their mailboxes, IMAP
3subscriptions and messages, and broadcasts updates (e.g. mail delivery) to
4interested sessions (e.g. IMAP connections).
5
6Layout of storage for accounts:
7
8 <DataDir>/accounts/<name>/index.db
9 <DataDir>/accounts/<name>/msg/[a-zA-Z0-9_-]+/<id>
10
11Index.db holds tables for user information, mailboxes, and messages. Messages
12are stored in the msg/ subdirectory, each in their own file. The on-disk message
13does not contain headers generated during an incoming SMTP transaction, such as
14Received and Authentication-Results headers. Those are in the database to
15prevent having to rewrite incoming messages (e.g. Authentication-Result for DKIM
16signatures can only be determined after having read the message). Messages must
17be read through MsgReader, which transparently adds the prefix from the
18database.
19*/
20package store
21
22// todo: make up a function naming scheme that indicates whether caller should broadcast changes.
23
24import (
25 "context"
26 "crypto/md5"
27 "crypto/sha1"
28 "crypto/sha256"
29 "encoding"
30 "encoding/json"
31 "errors"
32 "fmt"
33 "hash"
34 "io"
35 "os"
36 "path/filepath"
37 "runtime/debug"
38 "sort"
39 "strconv"
40 "strings"
41 "sync"
42 "time"
43
44 "golang.org/x/crypto/bcrypt"
45 "golang.org/x/exp/slices"
46 "golang.org/x/text/unicode/norm"
47
48 "github.com/mjl-/bstore"
49
50 "github.com/mjl-/mox/config"
51 "github.com/mjl-/mox/dns"
52 "github.com/mjl-/mox/message"
53 "github.com/mjl-/mox/metrics"
54 "github.com/mjl-/mox/mlog"
55 "github.com/mjl-/mox/mox-"
56 "github.com/mjl-/mox/moxio"
57 "github.com/mjl-/mox/moxvar"
58 "github.com/mjl-/mox/publicsuffix"
59 "github.com/mjl-/mox/scram"
60 "github.com/mjl-/mox/smtp"
61)
62
63// If true, each time an account is closed its database file is checked for
64// consistency. If an inconsistency is found, panic is called. Set by default
65// because of all the packages with tests, the mox main function sets it to
66// false again.
67var CheckConsistencyOnClose = true
68
69var xlog = mlog.New("store")
70
71var (
72 ErrUnknownMailbox = errors.New("no such mailbox")
73 ErrUnknownCredentials = errors.New("credentials not found")
74 ErrAccountUnknown = errors.New("no such account")
75)
76
77var subjectpassRand = mox.NewRand()
78
79var DefaultInitialMailboxes = config.InitialMailboxes{
80 SpecialUse: config.SpecialUseMailboxes{
81 Sent: "Sent",
82 Archive: "Archive",
83 Trash: "Trash",
84 Draft: "Drafts",
85 Junk: "Junk",
86 },
87}
88
89type SCRAM struct {
90 Salt []byte
91 Iterations int
92 SaltedPassword []byte
93}
94
95// CRAMMD5 holds HMAC ipad and opad hashes that are initialized with the first
96// block with (a derivation of) the key/password, so we don't store the password in plain
97// text.
98type CRAMMD5 struct {
99 Ipad hash.Hash
100 Opad hash.Hash
101}
102
103// BinaryMarshal is used by bstore to store the ipad/opad hash states.
104func (c CRAMMD5) MarshalBinary() ([]byte, error) {
105 if c.Ipad == nil || c.Opad == nil {
106 return nil, nil
107 }
108
109 ipad, err := c.Ipad.(encoding.BinaryMarshaler).MarshalBinary()
110 if err != nil {
111 return nil, fmt.Errorf("marshal ipad: %v", err)
112 }
113 opad, err := c.Opad.(encoding.BinaryMarshaler).MarshalBinary()
114 if err != nil {
115 return nil, fmt.Errorf("marshal opad: %v", err)
116 }
117 buf := make([]byte, 2+len(ipad)+len(opad))
118 ipadlen := uint16(len(ipad))
119 buf[0] = byte(ipadlen >> 8)
120 buf[1] = byte(ipadlen >> 0)
121 copy(buf[2:], ipad)
122 copy(buf[2+len(ipad):], opad)
123 return buf, nil
124}
125
126// BinaryUnmarshal is used by bstore to restore the ipad/opad hash states.
127func (c *CRAMMD5) UnmarshalBinary(buf []byte) error {
128 if len(buf) == 0 {
129 *c = CRAMMD5{}
130 return nil
131 }
132 if len(buf) < 2 {
133 return fmt.Errorf("short buffer")
134 }
135 ipadlen := int(uint16(buf[0])<<8 | uint16(buf[1])<<0)
136 if len(buf) < 2+ipadlen {
137 return fmt.Errorf("buffer too short for ipadlen")
138 }
139 ipad := md5.New()
140 opad := md5.New()
141 if err := ipad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2 : 2+ipadlen]); err != nil {
142 return fmt.Errorf("unmarshal ipad: %v", err)
143 }
144 if err := opad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2+ipadlen:]); err != nil {
145 return fmt.Errorf("unmarshal opad: %v", err)
146 }
147 *c = CRAMMD5{ipad, opad}
148 return nil
149}
150
151// Password holds credentials in various forms, for logging in with SMTP/IMAP.
152type Password struct {
153 Hash string // bcrypt hash for IMAP LOGIN, SASL PLAIN and HTTP basic authentication.
154 CRAMMD5 CRAMMD5 // For SASL CRAM-MD5.
155 SCRAMSHA1 SCRAM // For SASL SCRAM-SHA-1.
156 SCRAMSHA256 SCRAM // For SASL SCRAM-SHA-256.
157}
158
159// Subjectpass holds the secret key used to sign subjectpass tokens.
160type Subjectpass struct {
161 Email string // Our destination address (canonical, with catchall localpart stripped).
162 Key string
163}
164
165// NextUIDValidity is a singleton record in the database with the next UIDValidity
166// to use for the next mailbox.
167type NextUIDValidity struct {
168 ID int // Just a single record with ID 1.
169 Next uint32
170}
171
172// SyncState track ModSeqs.
173type SyncState struct {
174 ID int // Just a single record with ID 1.
175
176 // Last used, next assigned will be one higher. The first value we hand out is 2.
177 // That's because 0 (the default value for old existing messages, from before the
178 // Message.ModSeq field) is special in IMAP, so we return it as 1.
179 LastModSeq ModSeq `bstore:"nonzero"`
180
181 // Highest ModSeq of expunged record that we deleted. When a clients synchronizes
182 // and requests changes based on a modseq before this one, we don't have the
183 // history to provide information about deletions. We normally keep these expunged
184 // records around, but we may periodically truly delete them to reclaim storage
185 // space. Initially set to -1 because we don't want to match with any ModSeq in the
186 // database, which can be zero values.
187 HighestDeletedModSeq ModSeq
188}
189
190// Mailbox is collection of messages, e.g. Inbox or Sent.
191type Mailbox struct {
192 ID int64
193
194 // "Inbox" is the name for the special IMAP "INBOX". Slash separated
195 // for hierarchy.
196 Name string `bstore:"nonzero,unique"`
197
198 // If UIDs are invalidated, e.g. when renaming a mailbox to a previously existing
199 // name, UIDValidity must be changed. Used by IMAP for synchronization.
200 UIDValidity uint32
201
202 // UID likely to be assigned to next message. Used by IMAP to detect messages
203 // delivered to a mailbox.
204 UIDNext UID
205
206 SpecialUse
207
208 // Keywords as used in messages. Storing a non-system keyword for a message
209 // automatically adds it to this list. Used in the IMAP FLAGS response. Only
210 // "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in
211 // lower case (for JMAP), sorted.
212 Keywords []string
213
214 HaveCounts bool // Whether MailboxCounts have been initialized.
215 MailboxCounts // Statistics about messages, kept up to date whenever a change happens.
216}
217
218// MailboxCounts tracks statistics about messages for a mailbox.
219type MailboxCounts struct {
220 Total int64 // Total number of messages, excluding \Deleted. For JMAP.
221 Deleted int64 // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted.
222 Unread int64 // Messages without \Seen, excluding those with \Deleted, for JMAP.
223 Unseen int64 // Messages without \Seen, including those with \Deleted, for IMAP.
224 Size int64 // Number of bytes for all messages.
225}
226
227func (mc MailboxCounts) String() string {
228 return fmt.Sprintf("%d total, %d deleted, %d unread, %d unseen, size %d bytes", mc.Total, mc.Deleted, mc.Unread, mc.Unseen, mc.Size)
229}
230
231// Add increases mailbox counts mc with those of delta.
232func (mc *MailboxCounts) Add(delta MailboxCounts) {
233 mc.Total += delta.Total
234 mc.Deleted += delta.Deleted
235 mc.Unread += delta.Unread
236 mc.Unseen += delta.Unseen
237 mc.Size += delta.Size
238}
239
240// Add decreases mailbox counts mc with those of delta.
241func (mc *MailboxCounts) Sub(delta MailboxCounts) {
242 mc.Total -= delta.Total
243 mc.Deleted -= delta.Deleted
244 mc.Unread -= delta.Unread
245 mc.Unseen -= delta.Unseen
246 mc.Size -= delta.Size
247}
248
249// SpecialUse identifies a specific role for a mailbox, used by clients to
250// understand where messages should go.
251type SpecialUse struct {
252 Archive bool
253 Draft bool
254 Junk bool
255 Sent bool
256 Trash bool
257}
258
259// CalculateCounts calculates the full current counts for messages in the mailbox.
260func (mb *Mailbox) CalculateCounts(tx *bstore.Tx) (mc MailboxCounts, err error) {
261 q := bstore.QueryTx[Message](tx)
262 q.FilterNonzero(Message{MailboxID: mb.ID})
263 q.FilterEqual("Expunged", false)
264 err = q.ForEach(func(m Message) error {
265 mc.Add(m.MailboxCounts())
266 return nil
267 })
268 return
269}
270
271// ChangeSpecialUse returns a change for special-use flags, for broadcasting to
272// other connections.
273func (mb Mailbox) ChangeSpecialUse() ChangeMailboxSpecialUse {
274 return ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse}
275}
276
277// ChangeKeywords returns a change with new keywords for a mailbox (e.g. after
278// setting a new keyword on a message in the mailbox), for broadcasting to other
279// connections.
280func (mb Mailbox) ChangeKeywords() ChangeMailboxKeywords {
281 return ChangeMailboxKeywords{mb.ID, mb.Name, mb.Keywords}
282}
283
284// KeywordsChanged returns whether the keywords in a mailbox have changed.
285func (mb Mailbox) KeywordsChanged(origmb Mailbox) bool {
286 if len(mb.Keywords) != len(origmb.Keywords) {
287 return true
288 }
289 // Keywords are stored sorted.
290 for i, kw := range mb.Keywords {
291 if origmb.Keywords[i] != kw {
292 return true
293 }
294 }
295 return false
296}
297
298// CountsChange returns a change with mailbox counts.
299func (mb Mailbox) ChangeCounts() ChangeMailboxCounts {
300 return ChangeMailboxCounts{mb.ID, mb.Name, mb.MailboxCounts}
301}
302
303// Subscriptions are separate from existence of mailboxes.
304type Subscription struct {
305 Name string
306}
307
308// Flags for a mail message.
309type Flags struct {
310 Seen bool
311 Answered bool
312 Flagged bool
313 Forwarded bool
314 Junk bool
315 Notjunk bool
316 Deleted bool
317 Draft bool
318 Phishing bool
319 MDNSent bool
320}
321
322// FlagsAll is all flags set, for use as mask.
323var FlagsAll = Flags{true, true, true, true, true, true, true, true, true, true}
324
325// Validation of "message From" domain.
326type Validation uint8
327
328const (
329 ValidationUnknown Validation = 0
330 ValidationStrict Validation = 1 // Like DMARC, with strict policies.
331 ValidationDMARC Validation = 2 // Actual DMARC policy.
332 ValidationRelaxed Validation = 3 // Like DMARC, with relaxed policies.
333 ValidationPass Validation = 4 // For SPF.
334 ValidationNeutral Validation = 5 // For SPF.
335 ValidationTemperror Validation = 6
336 ValidationPermerror Validation = 7
337 ValidationFail Validation = 8
338 ValidationSoftfail Validation = 9 // For SPF.
339 ValidationNone Validation = 10 // E.g. No records.
340)
341
342// Message stored in database and per-message file on disk.
343//
344// Contents are always the combined data from MsgPrefix and the on-disk file named
345// based on ID.
346//
347// Messages always have a header section, even if empty. Incoming messages without
348// header section must get an empty header section added before inserting.
349type Message struct {
350 // ID, unchanged over lifetime, determines path to on-disk msg file.
351 // Set during deliver.
352 ID int64
353
354 UID UID `bstore:"nonzero"` // UID, for IMAP. Set during deliver.
355 MailboxID int64 `bstore:"nonzero,unique MailboxID+UID,index MailboxID+Received,index MailboxID+ModSeq,ref Mailbox"`
356
357 // Modification sequence, for faster syncing with IMAP QRESYNC and JMAP.
358 // ModSeq is the last modification. CreateSeq is the Seq the message was inserted,
359 // always <= ModSeq. If Expunged is set, the message has been removed and should not
360 // be returned to the user. In this case, ModSeq is the Seq where the message is
361 // removed, and will never be changed again.
362 // We have an index on both ModSeq (for JMAP that synchronizes per account) and
363 // MailboxID+ModSeq (for IMAP that synchronizes per mailbox).
364 // The index on CreateSeq helps efficiently finding created messages for JMAP.
365 // The value of ModSeq is special for IMAP. Messages that existed before ModSeq was
366 // added have 0 as value. But modseq 0 in IMAP is special, so we return it as 1. If
367 // we get modseq 1 from a client, the IMAP server will translate it to 0. When we
368 // return modseq to clients, we turn 0 into 1.
369 ModSeq ModSeq `bstore:"index"`
370 CreateSeq ModSeq `bstore:"index"`
371 Expunged bool
372
373 // If set, this message was delivered to a Rejects mailbox. When it is moved to a
374 // different mailbox, its MailboxOrigID is set to the destination mailbox and this
375 // flag cleared.
376 IsReject bool
377
378 // If set, this is a forwarded message (through a ruleset with IsForward). This
379 // causes fields used during junk analysis to be moved to their Orig variants, and
380 // masked IP fields cleared, so they aren't used in junk classifications for
381 // incoming messages. This ensures the forwarded messages don't cause negative
382 // reputation for the forwarding mail server, which may also be sending regular
383 // messages.
384 IsForward bool
385
386 // MailboxOrigID is the mailbox the message was originally delivered to. Typically
387 // Inbox or Rejects, but can also be a mailbox configured in a Ruleset, or
388 // Postmaster, TLS/DMARC reporting addresses. MailboxOrigID is not changed when the
389 // message is moved to another mailbox, e.g. Archive/Trash/Junk. Used for
390 // per-mailbox reputation.
391 //
392 // MailboxDestinedID is normally 0, but when a message is delivered to the Rejects
393 // mailbox, it is set to the intended mailbox according to delivery rules,
394 // typically that of Inbox. When such a message is moved out of Rejects, the
395 // MailboxOrigID is corrected by setting it to MailboxDestinedID. This ensures the
396 // message is used for reputation calculation for future deliveries to that
397 // mailbox.
398 //
399 // These are not bstore references to prevent having to update all messages in a
400 // mailbox when the original mailbox is removed. Use of these fields requires
401 // checking if the mailbox still exists.
402 MailboxOrigID int64
403 MailboxDestinedID int64
404
405 Received time.Time `bstore:"default now,index"`
406
407 // Full IP address of remote SMTP server. Empty if not delivered over SMTP. The
408 // masked IPs are used to classify incoming messages. They are left empty for
409 // messages matching a ruleset for forwarded messages.
410 RemoteIP string
411 RemoteIPMasked1 string `bstore:"index RemoteIPMasked1+Received"` // For IPv4 /32, for IPv6 /64, for reputation.
412 RemoteIPMasked2 string `bstore:"index RemoteIPMasked2+Received"` // For IPv4 /26, for IPv6 /48.
413 RemoteIPMasked3 string `bstore:"index RemoteIPMasked3+Received"` // For IPv4 /21, for IPv6 /32.
414
415 // Only set if present and not an IP address. Unicode string. Empty for forwarded
416 // messages.
417 EHLODomain string `bstore:"index EHLODomain+Received"`
418 MailFrom string // With localpart and domain. Can be empty.
419 MailFromLocalpart smtp.Localpart // SMTP "MAIL FROM", can be empty.
420 // Only set if it is a domain, not an IP. Unicode string. Empty for forwarded
421 // messages, but see OrigMailFromDomain.
422 MailFromDomain string `bstore:"index MailFromDomain+Received"`
423 RcptToLocalpart smtp.Localpart // SMTP "RCPT TO", can be empty.
424 RcptToDomain string // Unicode string.
425
426 // Parsed "From" message header, used for reputation along with domain validation.
427 MsgFromLocalpart smtp.Localpart
428 MsgFromDomain string `bstore:"index MsgFromDomain+Received"` // Unicode string.
429 MsgFromOrgDomain string `bstore:"index MsgFromOrgDomain+Received"` // Unicode string.
430
431 // Simplified statements of the Validation fields below, used for incoming messages
432 // to check reputation.
433 EHLOValidated bool
434 MailFromValidated bool
435 MsgFromValidated bool
436
437 EHLOValidation Validation // Validation can also take reverse IP lookup into account, not only SPF.
438 MailFromValidation Validation // Can have SPF-specific validations like ValidationSoftfail.
439 MsgFromValidation Validation // Desirable validations: Strict, DMARC, Relaxed. Will not be just Pass.
440
441 // Domains with verified DKIM signatures. Unicode string. For forwarded messages, a
442 // DKIM domain that matched a ruleset's verified domain is left out, but included
443 // in OrigDKIMDomains.
444 DKIMDomains []string `bstore:"index DKIMDomains+Received"`
445
446 // For forwarded messages,
447 OrigEHLODomain string
448 OrigDKIMDomains []string
449
450 // Canonicalized Message-Id, always lower-case and normalized quoting, without
451 // <>'s. Empty if missing. Used for matching message threads, and to prevent
452 // duplicate reject delivery.
453 MessageID string `bstore:"index"`
454 // lower-case: ../rfc/5256:495
455
456 // For matching threads in case there is no References/In-Reply-To header. It is
457 // lower-cased, white-space collapsed, mailing list tags and re/fwd tags removed.
458 SubjectBase string `bstore:"index"`
459 // ../rfc/5256:90
460
461 // Hash of message. For rejects delivery in case there is no Message-ID, only set
462 // when delivered as reject.
463 MessageHash []byte
464
465 // ID of message starting this thread.
466 ThreadID int64 `bstore:"index"`
467 // IDs of parent messages, from closest parent to the root message. Parent messages
468 // may be in a different mailbox, or may no longer exist. ThreadParentIDs must
469 // never contain the message id itself (a cycle), and parent messages must
470 // reference the same ancestors.
471 ThreadParentIDs []int64
472 // ThreadMissingLink is true if there is no match with a direct parent. E.g. first
473 // ID in ThreadParentIDs is not the direct ancestor (an intermediate message may
474 // have been deleted), or subject-based matching was done.
475 ThreadMissingLink bool
476 // If set, newly delivered child messages are automatically marked as read. This
477 // field is copied to new child messages. Changes are propagated to the webmail
478 // client.
479 ThreadMuted bool
480 // If set, this (sub)thread is collapsed in the webmail client, for threading mode
481 // "on" (mode "unread" ignores it). This field is copied to new child message.
482 // Changes are propagated to the webmail client.
483 ThreadCollapsed bool
484
485 Flags
486 // For keywords other than system flags or the basic well-known $-flags. Only in
487 // "atom" syntax (IMAP), they are case-insensitive, always stored in lower-case
488 // (for JMAP), sorted.
489 Keywords []string `bstore:"index"`
490 Size int64
491 TrainedJunk *bool // If nil, no training done yet. Otherwise, true is trained as junk, false trained as nonjunk.
492 MsgPrefix []byte // Typically holds received headers and/or header separator.
493
494 // ParsedBuf message structure. Currently saved as JSON of message.Part because bstore
495 // cannot yet store recursive types. Created when first needed, and saved in the
496 // database.
497 // todo: once replaced with non-json storage, remove date fixup in ../message/part.go.
498 ParsedBuf []byte
499}
500
501// MailboxCounts returns the delta to counts this message means for its
502// mailbox.
503func (m Message) MailboxCounts() (mc MailboxCounts) {
504 if m.Expunged {
505 return
506 }
507 if m.Deleted {
508 mc.Deleted++
509 } else {
510 mc.Total++
511 }
512 if !m.Seen {
513 mc.Unseen++
514 if !m.Deleted {
515 mc.Unread++
516 }
517 }
518 mc.Size += m.Size
519 return
520}
521
522func (m Message) ChangeAddUID() ChangeAddUID {
523 return ChangeAddUID{m.MailboxID, m.UID, m.ModSeq, m.Flags, m.Keywords}
524}
525
526func (m Message) ChangeFlags(orig Flags) ChangeFlags {
527 mask := m.Flags.Changed(orig)
528 return ChangeFlags{MailboxID: m.MailboxID, UID: m.UID, ModSeq: m.ModSeq, Mask: mask, Flags: m.Flags, Keywords: m.Keywords}
529}
530
531func (m Message) ChangeThread() ChangeThread {
532 return ChangeThread{[]int64{m.ID}, m.ThreadMuted, m.ThreadCollapsed}
533}
534
535// ModSeq represents a modseq as stored in the database. ModSeq 0 in the
536// database is sent to the client as 1, because modseq 0 is special in IMAP.
537// ModSeq coming from the client are of type int64.
538type ModSeq int64
539
540func (ms ModSeq) Client() int64 {
541 if ms == 0 {
542 return 1
543 }
544 return int64(ms)
545}
546
547// ModSeqFromClient converts a modseq from a client to a modseq for internal
548// use, e.g. in a database query.
549// ModSeq 1 is turned into 0 (the Go zero value for ModSeq).
550func ModSeqFromClient(modseq int64) ModSeq {
551 if modseq == 1 {
552 return 0
553 }
554 return ModSeq(modseq)
555}
556
557// PrepareExpunge clears fields that are no longer needed after an expunge, so
558// almost all fields. Does not change ModSeq, but does set Expunged.
559func (m *Message) PrepareExpunge() {
560 *m = Message{
561 ID: m.ID,
562 UID: m.UID,
563 MailboxID: m.MailboxID,
564 CreateSeq: m.CreateSeq,
565 ModSeq: m.ModSeq,
566 Expunged: true,
567 }
568}
569
570// PrepareThreading sets MessageID and SubjectBase (used in threading) based on the
571// envelope in part.
572func (m *Message) PrepareThreading(log *mlog.Log, part *message.Part) {
573 if part.Envelope == nil {
574 return
575 }
576 messageID, raw, err := message.MessageIDCanonical(part.Envelope.MessageID)
577 if err != nil {
578 log.Debugx("parsing message-id, ignoring", err, mlog.Field("messageid", part.Envelope.MessageID))
579 } else if raw {
580 log.Debug("could not parse message-id as address, continuing with raw value", mlog.Field("messageid", part.Envelope.MessageID))
581 }
582 m.MessageID = messageID
583 m.SubjectBase, _ = message.ThreadSubject(part.Envelope.Subject, false)
584}
585
586// LoadPart returns a message.Part by reading from m.ParsedBuf.
587func (m Message) LoadPart(r io.ReaderAt) (message.Part, error) {
588 if m.ParsedBuf == nil {
589 return message.Part{}, fmt.Errorf("message not parsed")
590 }
591 var p message.Part
592 err := json.Unmarshal(m.ParsedBuf, &p)
593 if err != nil {
594 return p, fmt.Errorf("unmarshal message part")
595 }
596 p.SetReaderAt(r)
597 return p, nil
598}
599
600// NeedsTraining returns whether message needs a training update, based on
601// TrainedJunk (current training status) and new Junk/Notjunk flags.
602func (m Message) NeedsTraining() bool {
603 untrain := m.TrainedJunk != nil
604 untrainJunk := untrain && *m.TrainedJunk
605 train := m.Junk || m.Notjunk && !(m.Junk && m.Notjunk)
606 trainJunk := m.Junk
607 return untrain != train || untrain && train && untrainJunk != trainJunk
608}
609
610// JunkFlagsForMailbox sets Junk and Notjunk flags based on mailbox name if configured. Often
611// used when delivering/moving/copying messages to a mailbox. Mail clients are not
612// very helpful with setting junk/notjunk flags. But clients can move/copy messages
613// to other mailboxes. So we set flags when clients move a message.
614func (m *Message) JunkFlagsForMailbox(mb Mailbox, conf config.Account) {
615 if mb.Junk {
616 m.Junk = true
617 m.Notjunk = false
618 return
619 }
620
621 if !conf.AutomaticJunkFlags.Enabled {
622 return
623 }
624
625 lmailbox := strings.ToLower(mb.Name)
626
627 if conf.JunkMailbox != nil && conf.JunkMailbox.MatchString(lmailbox) {
628 m.Junk = true
629 m.Notjunk = false
630 } else if conf.NeutralMailbox != nil && conf.NeutralMailbox.MatchString(lmailbox) {
631 m.Junk = false
632 m.Notjunk = false
633 } else if conf.NotJunkMailbox != nil && conf.NotJunkMailbox.MatchString(lmailbox) {
634 m.Junk = false
635 m.Notjunk = true
636 } else if conf.JunkMailbox == nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox != nil {
637 m.Junk = true
638 m.Notjunk = false
639 } else if conf.JunkMailbox != nil && conf.NeutralMailbox == nil && conf.NotJunkMailbox != nil {
640 m.Junk = false
641 m.Notjunk = false
642 } else if conf.JunkMailbox != nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox == nil {
643 m.Junk = false
644 m.Notjunk = true
645 }
646}
647
648// Recipient represents the recipient of a message. It is tracked to allow
649// first-time incoming replies from users this account has sent messages to. When a
650// mailbox is added to the Sent mailbox the message is parsed and recipients are
651// inserted as recipient. Recipients are never removed other than for removing the
652// message. On move/copy of a message, recipients aren't modified either. For IMAP,
653// this assumes a client simply appends messages to the Sent mailbox (as opposed to
654// copying messages from some place).
655type Recipient struct {
656 ID int64
657 MessageID int64 `bstore:"nonzero,ref Message"` // Ref gives it its own index, useful for fast removal as well.
658 Localpart smtp.Localpart `bstore:"nonzero"`
659 Domain string `bstore:"nonzero,index Domain+Localpart"` // Unicode string.
660 OrgDomain string `bstore:"nonzero,index"` // Unicode string.
661 Sent time.Time `bstore:"nonzero"`
662}
663
664// Outgoing is a message submitted for delivery from the queue. Used to enforce
665// maximum outgoing messages.
666type Outgoing struct {
667 ID int64
668 Recipient string `bstore:"nonzero,index"` // Canonical international address with utf8 domain.
669 Submitted time.Time `bstore:"nonzero,default now"`
670}
671
672// Types stored in DB.
673var DBTypes = []any{NextUIDValidity{}, Message{}, Recipient{}, Mailbox{}, Subscription{}, Outgoing{}, Password{}, Subjectpass{}, SyncState{}, Upgrade{}}
674
675// Account holds the information about a user, includings mailboxes, messages, imap subscriptions.
676type Account struct {
677 Name string // Name, according to configuration.
678 Dir string // Directory where account files, including the database, bloom filter, and mail messages, are stored for this account.
679 DBPath string // Path to database with mailboxes, messages, etc.
680 DB *bstore.DB // Open database connection.
681
682 // Channel that is closed if/when account has/gets "threads" accounting (see
683 // Upgrade.Threads).
684 threadsCompleted chan struct{}
685 // If threads upgrade completed with error, this is set. Used for warning during
686 // delivery, or aborting when importing.
687 threadsErr error
688
689 // Write lock must be held for account/mailbox modifications including message delivery.
690 // Read lock for reading mailboxes/messages.
691 // When making changes to mailboxes/messages, changes must be broadcasted before
692 // releasing the lock to ensure proper UID ordering.
693 sync.RWMutex
694
695 nused int // Reference count, while >0, this account is alive and shared.
696}
697
698type Upgrade struct {
699 ID byte
700 Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed.
701}
702
703// InitialUIDValidity returns a UIDValidity used for initializing an account.
704// It can be replaced during tests with a predictable value.
705var InitialUIDValidity = func() uint32 {
706 return uint32(time.Now().Unix() >> 1) // A 2-second resolution will get us far enough beyond 2038.
707}
708
709var openAccounts = struct {
710 names map[string]*Account
711 sync.Mutex
712}{
713 names: map[string]*Account{},
714}
715
716func closeAccount(acc *Account) (rerr error) {
717 openAccounts.Lock()
718 acc.nused--
719 defer openAccounts.Unlock()
720 if acc.nused == 0 {
721 // threadsCompleted must be closed now because it increased nused.
722 rerr = acc.DB.Close()
723 acc.DB = nil
724 delete(openAccounts.names, acc.Name)
725 }
726 return
727}
728
729// OpenAccount opens an account by name.
730//
731// No additional data path prefix or ".db" suffix should be added to the name.
732// A single shared account exists per name.
733func OpenAccount(name string) (*Account, error) {
734 openAccounts.Lock()
735 defer openAccounts.Unlock()
736 if acc, ok := openAccounts.names[name]; ok {
737 acc.nused++
738 return acc, nil
739 }
740
741 if _, ok := mox.Conf.Account(name); !ok {
742 return nil, ErrAccountUnknown
743 }
744
745 acc, err := openAccount(name)
746 if err != nil {
747 return nil, err
748 }
749 openAccounts.names[name] = acc
750 return acc, nil
751}
752
753// openAccount opens an existing account, or creates it if it is missing.
754func openAccount(name string) (a *Account, rerr error) {
755 dir := filepath.Join(mox.DataDirPath("accounts"), name)
756 return OpenAccountDB(dir, name)
757}
758
759// OpenAccountDB opens an account database file and returns an initialized account
760// or error. Only exported for use by subcommands that verify the database file.
761// Almost all account opens must go through OpenAccount/OpenEmail/OpenEmailAuth.
762func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
763 dbpath := filepath.Join(accountDir, "index.db")
764
765 // Create account if it doesn't exist yet.
766 isNew := false
767 if _, err := os.Stat(dbpath); err != nil && os.IsNotExist(err) {
768 isNew = true
769 os.MkdirAll(accountDir, 0770)
770 }
771
772 db, err := bstore.Open(context.TODO(), dbpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
773 if err != nil {
774 return nil, err
775 }
776
777 defer func() {
778 if rerr != nil {
779 db.Close()
780 if isNew {
781 os.Remove(dbpath)
782 }
783 }
784 }()
785
786 acc := &Account{
787 Name: accountName,
788 Dir: accountDir,
789 DBPath: dbpath,
790 DB: db,
791 nused: 1,
792 threadsCompleted: make(chan struct{}),
793 }
794
795 if isNew {
796 if err := initAccount(db); err != nil {
797 return nil, fmt.Errorf("initializing account: %v", err)
798 }
799 close(acc.threadsCompleted)
800 return acc, nil
801 }
802
803 // Ensure mailbox counts are set.
804 var mentioned bool
805 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
806 return bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
807 if !mentioned {
808 mentioned = true
809 xlog.Info("first calculation of mailbox counts for account", mlog.Field("account", accountName))
810 }
811 mc, err := mb.CalculateCounts(tx)
812 if err != nil {
813 return err
814 }
815 mb.HaveCounts = true
816 mb.MailboxCounts = mc
817 return tx.Update(&mb)
818 })
819 })
820 if err != nil {
821 return nil, fmt.Errorf("calculating counts for mailbox: %v", err)
822 }
823
824 // Start adding threading if needed.
825 up := Upgrade{ID: 1}
826 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
827 err := tx.Get(&up)
828 if err == bstore.ErrAbsent {
829 if err := tx.Insert(&up); err != nil {
830 return fmt.Errorf("inserting initial upgrade record: %v", err)
831 }
832 err = nil
833 }
834 return err
835 })
836 if err != nil {
837 return nil, fmt.Errorf("checking message threading: %v", err)
838 }
839 if up.Threads == 2 {
840 close(acc.threadsCompleted)
841 return acc, nil
842 }
843
844 // Increase account use before holding on to account in background.
845 // Caller holds the lock. The goroutine below decreases nused by calling
846 // closeAccount.
847 acc.nused++
848
849 // Ensure all messages have a MessageID and SubjectBase, which are needed when
850 // matching threads.
851 // Then assign messages to threads, in the same way we do during imports.
852 xlog.Info("upgrading account for threading, in background", mlog.Field("account", acc.Name))
853 go func() {
854 defer func() {
855 err := closeAccount(acc)
856 xlog.Check(err, "closing use of account after upgrading account storage for threads", mlog.Field("account", a.Name))
857 }()
858
859 defer func() {
860 x := recover() // Should not happen, but don't take program down if it does.
861 if x != nil {
862 xlog.Error("upgradeThreads panic", mlog.Field("err", x))
863 debug.PrintStack()
864 metrics.PanicInc(metrics.Upgradethreads)
865 acc.threadsErr = fmt.Errorf("panic during upgradeThreads: %v", x)
866 }
867
868 // Mark that upgrade has finished, possibly error is indicated in threadsErr.
869 close(acc.threadsCompleted)
870 }()
871
872 err := upgradeThreads(mox.Shutdown, acc, &up)
873 if err != nil {
874 a.threadsErr = err
875 xlog.Errorx("upgrading account for threading, aborted", err, mlog.Field("account", a.Name))
876 } else {
877 xlog.Info("upgrading account for threading, completed", mlog.Field("account", a.Name))
878 }
879 }()
880 return acc, nil
881}
882
883// ThreadingWait blocks until the one-time account threading upgrade for the
884// account has completed, and returns an error if not successful.
885//
886// To be used before starting an import of messages.
887func (a *Account) ThreadingWait(log *mlog.Log) error {
888 select {
889 case <-a.threadsCompleted:
890 return a.threadsErr
891 default:
892 }
893 log.Debug("waiting for account upgrade to complete")
894
895 <-a.threadsCompleted
896 return a.threadsErr
897}
898
899func initAccount(db *bstore.DB) error {
900 return db.Write(context.TODO(), func(tx *bstore.Tx) error {
901 uidvalidity := InitialUIDValidity()
902
903 if err := tx.Insert(&Upgrade{ID: 1, Threads: 2}); err != nil {
904 return err
905 }
906
907 if len(mox.Conf.Static.DefaultMailboxes) > 0 {
908 // Deprecated in favor of InitialMailboxes.
909 defaultMailboxes := mox.Conf.Static.DefaultMailboxes
910 mailboxes := []string{"Inbox"}
911 for _, name := range defaultMailboxes {
912 if strings.EqualFold(name, "Inbox") {
913 continue
914 }
915 mailboxes = append(mailboxes, name)
916 }
917 for _, name := range mailboxes {
918 mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true}
919 if strings.HasPrefix(name, "Archive") {
920 mb.Archive = true
921 } else if strings.HasPrefix(name, "Drafts") {
922 mb.Draft = true
923 } else if strings.HasPrefix(name, "Junk") {
924 mb.Junk = true
925 } else if strings.HasPrefix(name, "Sent") {
926 mb.Sent = true
927 } else if strings.HasPrefix(name, "Trash") {
928 mb.Trash = true
929 }
930 if err := tx.Insert(&mb); err != nil {
931 return fmt.Errorf("creating mailbox: %w", err)
932 }
933 if err := tx.Insert(&Subscription{name}); err != nil {
934 return fmt.Errorf("adding subscription: %w", err)
935 }
936 }
937 } else {
938 mailboxes := mox.Conf.Static.InitialMailboxes
939 var zerouse config.SpecialUseMailboxes
940 if mailboxes.SpecialUse == zerouse && len(mailboxes.Regular) == 0 {
941 mailboxes = DefaultInitialMailboxes
942 }
943
944 add := func(name string, use SpecialUse) error {
945 mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, SpecialUse: use, HaveCounts: true}
946 if err := tx.Insert(&mb); err != nil {
947 return fmt.Errorf("creating mailbox: %w", err)
948 }
949 if err := tx.Insert(&Subscription{name}); err != nil {
950 return fmt.Errorf("adding subscription: %w", err)
951 }
952 return nil
953 }
954 addSpecialOpt := func(nameOpt string, use SpecialUse) error {
955 if nameOpt == "" {
956 return nil
957 }
958 return add(nameOpt, use)
959 }
960 l := []struct {
961 nameOpt string
962 use SpecialUse
963 }{
964 {"Inbox", SpecialUse{}},
965 {mailboxes.SpecialUse.Archive, SpecialUse{Archive: true}},
966 {mailboxes.SpecialUse.Draft, SpecialUse{Draft: true}},
967 {mailboxes.SpecialUse.Junk, SpecialUse{Junk: true}},
968 {mailboxes.SpecialUse.Sent, SpecialUse{Sent: true}},
969 {mailboxes.SpecialUse.Trash, SpecialUse{Trash: true}},
970 }
971 for _, e := range l {
972 if err := addSpecialOpt(e.nameOpt, e.use); err != nil {
973 return err
974 }
975 }
976 for _, name := range mailboxes.Regular {
977 if err := add(name, SpecialUse{}); err != nil {
978 return err
979 }
980 }
981 }
982
983 uidvalidity++
984 if err := tx.Insert(&NextUIDValidity{1, uidvalidity}); err != nil {
985 return fmt.Errorf("inserting nextuidvalidity: %w", err)
986 }
987 return nil
988 })
989}
990
991// Close reduces the reference count, and closes the database connection when
992// it was the last user.
993func (a *Account) Close() error {
994 if CheckConsistencyOnClose {
995 xerr := a.CheckConsistency()
996 err := closeAccount(a)
997 if xerr != nil {
998 panic(xerr)
999 }
1000 return err
1001 }
1002 return closeAccount(a)
1003}
1004
1005// CheckConsistency checks the consistency of the database and returns a non-nil
1006// error for these cases:
1007//
1008// - Missing on-disk file for message.
1009// - Mismatch between message size and length of MsgPrefix and on-disk file.
1010// - Missing HaveCounts.
1011// - Incorrect mailbox counts.
1012// - Message with UID >= mailbox uid next.
1013// - Mailbox uidvalidity >= account uid validity.
1014// - ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq.
1015// - All messages have a nonzero ThreadID, and no cycles in ThreadParentID, and parent messages the same ThreadParentIDs tail.
1016func (a *Account) CheckConsistency() error {
1017 var uidErrors []string // With a limit, could be many.
1018 var modseqErrors []string // With limit.
1019 var fileErrors []string // With limit.
1020 var threadidErrors []string // With limit.
1021 var threadParentErrors []string // With limit.
1022 var threadAncestorErrors []string // With limit.
1023 var errors []string
1024
1025 err := a.DB.Read(context.Background(), func(tx *bstore.Tx) error {
1026 nuv := NextUIDValidity{ID: 1}
1027 err := tx.Get(&nuv)
1028 if err != nil {
1029 return fmt.Errorf("fetching next uid validity: %v", err)
1030 }
1031
1032 mailboxes := map[int64]Mailbox{}
1033 err = bstore.QueryTx[Mailbox](tx).ForEach(func(mb Mailbox) error {
1034 mailboxes[mb.ID] = mb
1035
1036 if mb.UIDValidity >= nuv.Next {
1037 errmsg := fmt.Sprintf("mailbox %q (id %d) has uidvalidity %d >= account next uidvalidity %d", mb.Name, mb.ID, mb.UIDValidity, nuv.Next)
1038 errors = append(errors, errmsg)
1039 }
1040 return nil
1041 })
1042 if err != nil {
1043 return fmt.Errorf("listing mailboxes: %v", err)
1044 }
1045
1046 counts := map[int64]MailboxCounts{}
1047 err = bstore.QueryTx[Message](tx).ForEach(func(m Message) error {
1048 mc := counts[m.MailboxID]
1049 mc.Add(m.MailboxCounts())
1050 counts[m.MailboxID] = mc
1051
1052 mb := mailboxes[m.MailboxID]
1053
1054 if (m.ModSeq == 0 || m.CreateSeq == 0 || m.CreateSeq > m.ModSeq) && len(modseqErrors) < 20 {
1055 modseqerr := fmt.Sprintf("message %d in mailbox %q (id %d) has invalid modseq %d or createseq %d, both must be > 0 and createseq <= modseq", m.ID, mb.Name, mb.ID, m.ModSeq, m.CreateSeq)
1056 modseqErrors = append(modseqErrors, modseqerr)
1057 }
1058 if m.UID >= mb.UIDNext && len(uidErrors) < 20 {
1059 uiderr := fmt.Sprintf("message %d in mailbox %q (id %d) has uid %d >= mailbox uidnext %d", m.ID, mb.Name, mb.ID, m.UID, mb.UIDNext)
1060 uidErrors = append(uidErrors, uiderr)
1061 }
1062 if m.Expunged {
1063 return nil
1064 }
1065 p := a.MessagePath(m.ID)
1066 st, err := os.Stat(p)
1067 if err != nil {
1068 existserr := fmt.Sprintf("message %d in mailbox %q (id %d) on-disk file %s: %v", m.ID, mb.Name, mb.ID, p, err)
1069 fileErrors = append(fileErrors, existserr)
1070 } else if len(fileErrors) < 20 && m.Size != int64(len(m.MsgPrefix))+st.Size() {
1071 sizeerr := fmt.Sprintf("message %d in mailbox %q (id %d) has size %d != len msgprefix %d + on-disk file size %d = %d", m.ID, mb.Name, mb.ID, m.Size, len(m.MsgPrefix), st.Size(), int64(len(m.MsgPrefix))+st.Size())
1072 fileErrors = append(fileErrors, sizeerr)
1073 }
1074
1075 if m.ThreadID <= 0 && len(threadidErrors) < 20 {
1076 err := fmt.Sprintf("message %d in mailbox %q (id %d) has threadid 0", m.ID, mb.Name, mb.ID)
1077 threadidErrors = append(threadidErrors, err)
1078 }
1079 if slices.Contains(m.ThreadParentIDs, m.ID) && len(threadParentErrors) < 20 {
1080 err := fmt.Sprintf("message %d in mailbox %q (id %d) references itself in threadparentids", m.ID, mb.Name, mb.ID)
1081 threadParentErrors = append(threadParentErrors, err)
1082 }
1083 for i, pid := range m.ThreadParentIDs {
1084 am := Message{ID: pid}
1085 if err := tx.Get(&am); err == bstore.ErrAbsent {
1086 continue
1087 } else if err != nil {
1088 return fmt.Errorf("get ancestor message: %v", err)
1089 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) && len(threadAncestorErrors) < 20 {
1090 err := fmt.Sprintf("message %d, thread %d has ancestor ids %v, and ancestor at index %d with id %d should have the same tail but has %v\n", m.ID, m.ThreadID, m.ThreadParentIDs, i, am.ID, am.ThreadParentIDs)
1091 threadAncestorErrors = append(threadAncestorErrors, err)
1092 } else {
1093 break
1094 }
1095 }
1096 return nil
1097 })
1098 if err != nil {
1099 return fmt.Errorf("reading messages: %v", err)
1100 }
1101
1102 for _, mb := range mailboxes {
1103 if !mb.HaveCounts {
1104 errmsg := fmt.Sprintf("mailbox %q (id %d) does not have counts, should be %#v", mb.Name, mb.ID, counts[mb.ID])
1105 errors = append(errors, errmsg)
1106 } else if mb.MailboxCounts != counts[mb.ID] {
1107 mbcounterr := fmt.Sprintf("mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, counts[mb.ID])
1108 errors = append(errors, mbcounterr)
1109 }
1110 }
1111
1112 return nil
1113 })
1114 if err != nil {
1115 return err
1116 }
1117 errors = append(errors, uidErrors...)
1118 errors = append(errors, modseqErrors...)
1119 errors = append(errors, fileErrors...)
1120 errors = append(errors, threadidErrors...)
1121 errors = append(errors, threadParentErrors...)
1122 errors = append(errors, threadAncestorErrors...)
1123 if len(errors) > 0 {
1124 return fmt.Errorf("%s", strings.Join(errors, "; "))
1125 }
1126 return nil
1127}
1128
1129// Conf returns the configuration for this account if it still exists. During
1130// an SMTP session, a configuration update may drop an account.
1131func (a *Account) Conf() (config.Account, bool) {
1132 return mox.Conf.Account(a.Name)
1133}
1134
1135// NextUIDValidity returns the next new/unique uidvalidity to use for this account.
1136func (a *Account) NextUIDValidity(tx *bstore.Tx) (uint32, error) {
1137 nuv := NextUIDValidity{ID: 1}
1138 if err := tx.Get(&nuv); err != nil {
1139 return 0, err
1140 }
1141 v := nuv.Next
1142 nuv.Next++
1143 if err := tx.Update(&nuv); err != nil {
1144 return 0, err
1145 }
1146 return v, nil
1147}
1148
1149// NextModSeq returns the next modification sequence, which is global per account,
1150// over all types.
1151func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) {
1152 v := SyncState{ID: 1}
1153 if err := tx.Get(&v); err == bstore.ErrAbsent {
1154 // We start assigning from modseq 2. Modseq 0 is not usable, so returned as 1, so
1155 // already used.
1156 // HighestDeletedModSeq is -1 so comparison against the default ModSeq zero value
1157 // makes sense.
1158 v = SyncState{1, 2, -1}
1159 return v.LastModSeq, tx.Insert(&v)
1160 } else if err != nil {
1161 return 0, err
1162 }
1163 v.LastModSeq++
1164 return v.LastModSeq, tx.Update(&v)
1165}
1166
1167func (a *Account) HighestDeletedModSeq(tx *bstore.Tx) (ModSeq, error) {
1168 v := SyncState{ID: 1}
1169 err := tx.Get(&v)
1170 if err == bstore.ErrAbsent {
1171 return 0, nil
1172 }
1173 return v.HighestDeletedModSeq, err
1174}
1175
1176// WithWLock runs fn with account writelock held. Necessary for account/mailbox modification. For message delivery, a read lock is required.
1177func (a *Account) WithWLock(fn func()) {
1178 a.Lock()
1179 defer a.Unlock()
1180 fn()
1181}
1182
1183// WithRLock runs fn with account read lock held. Needed for message delivery.
1184func (a *Account) WithRLock(fn func()) {
1185 a.RLock()
1186 defer a.RUnlock()
1187 fn()
1188}
1189
1190// DeliverMessage delivers a mail message to the account.
1191//
1192// If consumeFile is set, the original msgFile is moved/renamed or copied and
1193// removed as part of delivery.
1194//
1195// The message, with msg.MsgPrefix and msgFile combined, must have a header
1196// section. The caller is responsible for adding a header separator to
1197// msg.MsgPrefix if missing from an incoming message.
1198//
1199// If the destination mailbox has the Sent special-use flag, the message is parsed
1200// for its recipients (to/cc/bcc). Their domains are added to Recipients for use in
1201// dmarc reputation.
1202//
1203// If sync is true, the message file and its directory are synced. Should be true
1204// for regular mail delivery, but can be false when importing many messages.
1205//
1206// If CreateSeq/ModSeq is not set, it is assigned automatically.
1207//
1208// Must be called with account rlock or wlock.
1209//
1210// Caller must broadcast new message.
1211//
1212// Caller must update mailbox counts.
1213func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, consumeFile, sync, notrain, nothreads bool) error {
1214 if m.Expunged {
1215 return fmt.Errorf("cannot deliver expunged message")
1216 }
1217
1218 mb := Mailbox{ID: m.MailboxID}
1219 if err := tx.Get(&mb); err != nil {
1220 return fmt.Errorf("get mailbox: %w", err)
1221 }
1222 m.UID = mb.UIDNext
1223 mb.UIDNext++
1224 if err := tx.Update(&mb); err != nil {
1225 return fmt.Errorf("updating mailbox nextuid: %w", err)
1226 }
1227
1228 conf, _ := a.Conf()
1229 m.JunkFlagsForMailbox(mb, conf)
1230
1231 mr := FileMsgReader(m.MsgPrefix, msgFile) // We don't close, it would close the msgFile.
1232 var part *message.Part
1233 if m.ParsedBuf == nil {
1234 p, err := message.EnsurePart(log, false, mr, m.Size)
1235 if err != nil {
1236 log.Infox("parsing delivered message", err, mlog.Field("parse", ""), mlog.Field("message", m.ID))
1237 // We continue, p is still valid.
1238 }
1239 part = &p
1240 buf, err := json.Marshal(part)
1241 if err != nil {
1242 return fmt.Errorf("marshal parsed message: %w", err)
1243 }
1244 m.ParsedBuf = buf
1245 } else {
1246 var p message.Part
1247 if err := json.Unmarshal(m.ParsedBuf, &p); err != nil {
1248 log.Errorx("unmarshal parsed message, continuing", err, mlog.Field("parse", ""))
1249 } else {
1250 part = &p
1251 }
1252 }
1253
1254 // If we are delivering to the originally intended mailbox, no need to store the mailbox ID again.
1255 if m.MailboxDestinedID != 0 && m.MailboxDestinedID == m.MailboxOrigID {
1256 m.MailboxDestinedID = 0
1257 }
1258 if m.CreateSeq == 0 || m.ModSeq == 0 {
1259 modseq, err := a.NextModSeq(tx)
1260 if err != nil {
1261 return fmt.Errorf("assigning next modseq: %w", err)
1262 }
1263 m.CreateSeq = modseq
1264 m.ModSeq = modseq
1265 }
1266
1267 if part != nil && m.MessageID == "" && m.SubjectBase == "" {
1268 m.PrepareThreading(log, part)
1269 }
1270
1271 // Assign to thread (if upgrade has completed).
1272 noThreadID := nothreads
1273 if m.ThreadID == 0 && !nothreads && part != nil {
1274 select {
1275 case <-a.threadsCompleted:
1276 if a.threadsErr != nil {
1277 log.Info("not assigning threads for new delivery, upgrading to threads failed")
1278 noThreadID = true
1279 } else {
1280 if err := assignThread(log, tx, m, part); err != nil {
1281 return fmt.Errorf("assigning thread: %w", err)
1282 }
1283 }
1284 default:
1285 // note: since we have a write transaction to get here, we can't wait for the
1286 // thread upgrade to finish.
1287 // If we don't assign a threadid the upgrade process will do it.
1288 log.Info("not assigning threads for new delivery, upgrading to threads in progress which will assign this message")
1289 noThreadID = true
1290 }
1291 }
1292
1293 if err := tx.Insert(m); err != nil {
1294 return fmt.Errorf("inserting message: %w", err)
1295 }
1296 if !noThreadID && m.ThreadID == 0 {
1297 m.ThreadID = m.ID
1298 if err := tx.Update(m); err != nil {
1299 return fmt.Errorf("updating message for its own thread id: %w", err)
1300 }
1301 }
1302
1303 // todo: perhaps we should match the recipients based on smtp submission and a matching message-id? we now miss the addresses in bcc's. for webmail, we could insert the recipients directly.
1304 if mb.Sent && part != nil && part.Envelope != nil {
1305 e := part.Envelope
1306 sent := e.Date
1307 if sent.IsZero() {
1308 sent = m.Received
1309 }
1310 if sent.IsZero() {
1311 sent = time.Now()
1312 }
1313 addrs := append(append(e.To, e.CC...), e.BCC...)
1314 for _, addr := range addrs {
1315 if addr.User == "" {
1316 // Would trigger error because Recipient.Localpart must be nonzero. todo: we could allow empty localpart in db, and filter by not using FilterNonzero.
1317 log.Info("to/cc/bcc address with empty localpart, not inserting as recipient", mlog.Field("address", addr))
1318 continue
1319 }
1320 d, err := dns.ParseDomain(addr.Host)
1321 if err != nil {
1322 log.Debugx("parsing domain in to/cc/bcc address", err, mlog.Field("address", addr))
1323 continue
1324 }
1325 mr := Recipient{
1326 MessageID: m.ID,
1327 Localpart: smtp.Localpart(addr.User),
1328 Domain: d.Name(),
1329 OrgDomain: publicsuffix.Lookup(context.TODO(), d).Name(),
1330 Sent: sent,
1331 }
1332 if err := tx.Insert(&mr); err != nil {
1333 return fmt.Errorf("inserting sent message recipients: %w", err)
1334 }
1335 }
1336 }
1337
1338 msgPath := a.MessagePath(m.ID)
1339 msgDir := filepath.Dir(msgPath)
1340 os.MkdirAll(msgDir, 0770)
1341
1342 // Sync file data to disk.
1343 if sync {
1344 if err := msgFile.Sync(); err != nil {
1345 return fmt.Errorf("fsync message file: %w", err)
1346 }
1347 }
1348
1349 if consumeFile {
1350 if err := os.Rename(msgFile.Name(), msgPath); err != nil {
1351 // Could be due to cross-filesystem rename. Users shouldn't configure their systems that way.
1352 return fmt.Errorf("moving msg file to destination directory: %w", err)
1353 }
1354 } else if err := moxio.LinkOrCopy(log, msgPath, msgFile.Name(), &moxio.AtReader{R: msgFile}, true); err != nil {
1355 return fmt.Errorf("linking/copying message to new file: %w", err)
1356 }
1357
1358 if sync {
1359 if err := moxio.SyncDir(msgDir); err != nil {
1360 return fmt.Errorf("sync directory: %w", err)
1361 }
1362 }
1363
1364 if !notrain && m.NeedsTraining() {
1365 l := []Message{*m}
1366 if err := a.RetrainMessages(context.TODO(), log, tx, l, false); err != nil {
1367 return fmt.Errorf("training junkfilter: %w", err)
1368 }
1369 *m = l[0]
1370 }
1371
1372 return nil
1373}
1374
1375// SetPassword saves a new password for this account. This password is used for
1376// IMAP, SMTP (submission) sessions and the HTTP account web page.
1377func (a *Account) SetPassword(password string) error {
1378 hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1379 if err != nil {
1380 return fmt.Errorf("generating password hash: %w", err)
1381 }
1382
1383 err = a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1384 if _, err := bstore.QueryTx[Password](tx).Delete(); err != nil {
1385 return fmt.Errorf("deleting existing password: %v", err)
1386 }
1387 var pw Password
1388 pw.Hash = string(hash)
1389
1390 // CRAM-MD5 calculates an HMAC-MD5, with the password as key, over a per-attempt
1391 // unique text that includes a timestamp. HMAC performs two hashes. Both times, the
1392 // first block is based on the key/password. We hash those first blocks now, and
1393 // store the hash state in the database. When we actually authenticate, we'll
1394 // complete the HMAC by hashing only the text. We cannot store crypto/hmac's hash,
1395 // because it does not expose its internal state and isn't a BinaryMarshaler.
1396 // ../rfc/2104:121
1397 pw.CRAMMD5.Ipad = md5.New()
1398 pw.CRAMMD5.Opad = md5.New()
1399 key := []byte(password)
1400 if len(key) > 64 {
1401 t := md5.Sum(key)
1402 key = t[:]
1403 }
1404 ipad := make([]byte, md5.BlockSize)
1405 opad := make([]byte, md5.BlockSize)
1406 copy(ipad, key)
1407 copy(opad, key)
1408 for i := range ipad {
1409 ipad[i] ^= 0x36
1410 opad[i] ^= 0x5c
1411 }
1412 pw.CRAMMD5.Ipad.Write(ipad)
1413 pw.CRAMMD5.Opad.Write(opad)
1414
1415 pw.SCRAMSHA1.Salt = scram.MakeRandom()
1416 pw.SCRAMSHA1.Iterations = 2 * 4096
1417 pw.SCRAMSHA1.SaltedPassword = scram.SaltPassword(sha1.New, password, pw.SCRAMSHA1.Salt, pw.SCRAMSHA1.Iterations)
1418
1419 pw.SCRAMSHA256.Salt = scram.MakeRandom()
1420 pw.SCRAMSHA256.Iterations = 4096
1421 pw.SCRAMSHA256.SaltedPassword = scram.SaltPassword(sha256.New, password, pw.SCRAMSHA256.Salt, pw.SCRAMSHA256.Iterations)
1422
1423 if err := tx.Insert(&pw); err != nil {
1424 return fmt.Errorf("inserting new password: %v", err)
1425 }
1426 return nil
1427 })
1428 if err == nil {
1429 xlog.Info("new password set for account", mlog.Field("account", a.Name))
1430 }
1431 return err
1432}
1433
1434// Subjectpass returns the signing key for use with subjectpass for the given
1435// email address with canonical localpart.
1436func (a *Account) Subjectpass(email string) (key string, err error) {
1437 return key, a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1438 v := Subjectpass{Email: email}
1439 err := tx.Get(&v)
1440 if err == nil {
1441 key = v.Key
1442 return nil
1443 }
1444 if !errors.Is(err, bstore.ErrAbsent) {
1445 return fmt.Errorf("get subjectpass key from accounts database: %w", err)
1446 }
1447 key = ""
1448 const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1449 for i := 0; i < 16; i++ {
1450 key += string(chars[subjectpassRand.Intn(len(chars))])
1451 }
1452 v.Key = key
1453 return tx.Insert(&v)
1454 })
1455}
1456
1457// Ensure mailbox is present in database, adding records for the mailbox and its
1458// parents if they aren't present.
1459//
1460// If subscribe is true, any mailboxes that were created will also be subscribed to.
1461// Caller must hold account wlock.
1462// Caller must propagate changes if any.
1463func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool) (mb Mailbox, changes []Change, rerr error) {
1464 if norm.NFC.String(name) != name {
1465 return Mailbox{}, nil, fmt.Errorf("mailbox name not normalized")
1466 }
1467
1468 // Quick sanity check.
1469 if strings.EqualFold(name, "inbox") && name != "Inbox" {
1470 return Mailbox{}, nil, fmt.Errorf("bad casing for inbox")
1471 }
1472
1473 elems := strings.Split(name, "/")
1474 q := bstore.QueryTx[Mailbox](tx)
1475 q.FilterFn(func(mb Mailbox) bool {
1476 return mb.Name == elems[0] || strings.HasPrefix(mb.Name, elems[0]+"/")
1477 })
1478 l, err := q.List()
1479 if err != nil {
1480 return Mailbox{}, nil, fmt.Errorf("list mailboxes: %v", err)
1481 }
1482
1483 mailboxes := map[string]Mailbox{}
1484 for _, xmb := range l {
1485 mailboxes[xmb.Name] = xmb
1486 }
1487
1488 p := ""
1489 for _, elem := range elems {
1490 if p != "" {
1491 p += "/"
1492 }
1493 p += elem
1494 var ok bool
1495 mb, ok = mailboxes[p]
1496 if ok {
1497 continue
1498 }
1499 uidval, err := a.NextUIDValidity(tx)
1500 if err != nil {
1501 return Mailbox{}, nil, fmt.Errorf("next uid validity: %v", err)
1502 }
1503 mb = Mailbox{
1504 Name: p,
1505 UIDValidity: uidval,
1506 UIDNext: 1,
1507 HaveCounts: true,
1508 }
1509 err = tx.Insert(&mb)
1510 if err != nil {
1511 return Mailbox{}, nil, fmt.Errorf("creating new mailbox: %v", err)
1512 }
1513
1514 var flags []string
1515 if subscribe {
1516 if tx.Get(&Subscription{p}) != nil {
1517 err := tx.Insert(&Subscription{p})
1518 if err != nil {
1519 return Mailbox{}, nil, fmt.Errorf("subscribing to mailbox: %v", err)
1520 }
1521 }
1522 flags = []string{`\Subscribed`}
1523 }
1524 changes = append(changes, ChangeAddMailbox{mb, flags})
1525 }
1526 return mb, changes, nil
1527}
1528
1529// MailboxExists checks if mailbox exists.
1530// Caller must hold account rlock.
1531func (a *Account) MailboxExists(tx *bstore.Tx, name string) (bool, error) {
1532 q := bstore.QueryTx[Mailbox](tx)
1533 q.FilterEqual("Name", name)
1534 return q.Exists()
1535}
1536
1537// MailboxFind finds a mailbox by name, returning a nil mailbox and nil error if mailbox does not exist.
1538func (a *Account) MailboxFind(tx *bstore.Tx, name string) (*Mailbox, error) {
1539 q := bstore.QueryTx[Mailbox](tx)
1540 q.FilterEqual("Name", name)
1541 mb, err := q.Get()
1542 if err == bstore.ErrAbsent {
1543 return nil, nil
1544 }
1545 if err != nil {
1546 return nil, fmt.Errorf("looking up mailbox: %w", err)
1547 }
1548 return &mb, nil
1549}
1550
1551// SubscriptionEnsure ensures a subscription for name exists. The mailbox does not
1552// have to exist. Any parents are not automatically subscribed.
1553// Changes are returned and must be broadcasted by the caller.
1554func (a *Account) SubscriptionEnsure(tx *bstore.Tx, name string) ([]Change, error) {
1555 if err := tx.Get(&Subscription{name}); err == nil {
1556 return nil, nil
1557 }
1558
1559 if err := tx.Insert(&Subscription{name}); err != nil {
1560 return nil, fmt.Errorf("inserting subscription: %w", err)
1561 }
1562
1563 q := bstore.QueryTx[Mailbox](tx)
1564 q.FilterEqual("Name", name)
1565 _, err := q.Get()
1566 if err == nil {
1567 return []Change{ChangeAddSubscription{name, nil}}, nil
1568 } else if err != bstore.ErrAbsent {
1569 return nil, fmt.Errorf("looking up mailbox for subscription: %w", err)
1570 }
1571 return []Change{ChangeAddSubscription{name, []string{`\NonExistent`}}}, nil
1572}
1573
1574// MessageRuleset returns the first ruleset (if any) that message the message
1575// represented by msgPrefix and msgFile, with smtp and validation fields from m.
1576func MessageRuleset(log *mlog.Log, dest config.Destination, m *Message, msgPrefix []byte, msgFile *os.File) *config.Ruleset {
1577 if len(dest.Rulesets) == 0 {
1578 return nil
1579 }
1580
1581 mr := FileMsgReader(msgPrefix, msgFile) // We don't close, it would close the msgFile.
1582 p, err := message.Parse(log, false, mr)
1583 if err != nil {
1584 log.Errorx("parsing message for evaluating rulesets, continuing with headers", err, mlog.Field("parse", ""))
1585 // note: part is still set.
1586 }
1587 // todo optimize: only parse header if needed for rulesets. and probably reuse an earlier parsing.
1588 header, err := p.Header()
1589 if err != nil {
1590 log.Errorx("parsing message headers for evaluating rulesets, delivering to default mailbox", err, mlog.Field("parse", ""))
1591 // todo: reject message?
1592 return nil
1593 }
1594
1595ruleset:
1596 for _, rs := range dest.Rulesets {
1597 if rs.SMTPMailFromRegexpCompiled != nil {
1598 if !rs.SMTPMailFromRegexpCompiled.MatchString(m.MailFrom) {
1599 continue ruleset
1600 }
1601 }
1602
1603 if !rs.VerifiedDNSDomain.IsZero() {
1604 d := rs.VerifiedDNSDomain.Name()
1605 suffix := "." + d
1606 matchDomain := func(s string) bool {
1607 return s == d || strings.HasSuffix(s, suffix)
1608 }
1609 var ok bool
1610 if m.EHLOValidated && matchDomain(m.EHLODomain) {
1611 ok = true
1612 }
1613 if m.MailFromValidated && matchDomain(m.MailFromDomain) {
1614 ok = true
1615 }
1616 for _, d := range m.DKIMDomains {
1617 if matchDomain(d) {
1618 ok = true
1619 break
1620 }
1621 }
1622 if !ok {
1623 continue ruleset
1624 }
1625 }
1626
1627 header:
1628 for _, t := range rs.HeadersRegexpCompiled {
1629 for k, vl := range header {
1630 k = strings.ToLower(k)
1631 if !t[0].MatchString(k) {
1632 continue
1633 }
1634 for _, v := range vl {
1635 v = strings.ToLower(strings.TrimSpace(v))
1636 if t[1].MatchString(v) {
1637 continue header
1638 }
1639 }
1640 }
1641 continue ruleset
1642 }
1643 return &rs
1644 }
1645 return nil
1646}
1647
1648// MessagePath returns the file system path of a message.
1649func (a *Account) MessagePath(messageID int64) string {
1650 return strings.Join(append([]string{a.Dir, "msg"}, messagePathElems(messageID)...), "/")
1651}
1652
1653// MessageReader opens a message for reading, transparently combining the
1654// message prefix with the original incoming message.
1655func (a *Account) MessageReader(m Message) *MsgReader {
1656 return &MsgReader{prefix: m.MsgPrefix, path: a.MessagePath(m.ID), size: m.Size}
1657}
1658
1659// DeliverDestination delivers an email to dest, based on the configured rulesets.
1660//
1661// Caller must hold account wlock (mailbox may be created).
1662// Message delivery, possible mailbox creation, and updated mailbox counts are
1663// broadcasted.
1664func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *Message, msgFile *os.File, consumeFile bool) error {
1665 var mailbox string
1666 rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
1667 if rs != nil {
1668 mailbox = rs.Mailbox
1669 } else if dest.Mailbox == "" {
1670 mailbox = "Inbox"
1671 } else {
1672 mailbox = dest.Mailbox
1673 }
1674 return a.DeliverMailbox(log, mailbox, m, msgFile, consumeFile)
1675}
1676
1677// DeliverMailbox delivers an email to the specified mailbox.
1678//
1679// Caller must hold account wlock (mailbox may be created).
1680// Message delivery, possible mailbox creation, and updated mailbox counts are
1681// broadcasted.
1682func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgFile *os.File, consumeFile bool) error {
1683 var changes []Change
1684 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1685 mb, chl, err := a.MailboxEnsure(tx, mailbox, true)
1686 if err != nil {
1687 return fmt.Errorf("ensuring mailbox: %w", err)
1688 }
1689 m.MailboxID = mb.ID
1690 m.MailboxOrigID = mb.ID
1691
1692 // Update count early, DeliverMessage will update mb too and we don't want to fetch
1693 // it again before updating.
1694 mb.MailboxCounts.Add(m.MailboxCounts())
1695 if err := tx.Update(&mb); err != nil {
1696 return fmt.Errorf("updating mailbox for delivery: %w", err)
1697 }
1698
1699 if err := a.DeliverMessage(log, tx, m, msgFile, consumeFile, true, false, false); err != nil {
1700 return err
1701 }
1702
1703 changes = append(changes, chl...)
1704 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
1705 return nil
1706 })
1707 // todo: if rename succeeded but transaction failed, we should remove the file.
1708 if err != nil {
1709 return err
1710 }
1711
1712 BroadcastChanges(a, changes)
1713 return nil
1714}
1715
1716// TidyRejectsMailbox removes old reject emails, and returns whether there is space for a new delivery.
1717//
1718// Caller most hold account wlock.
1719// Changes are broadcasted.
1720func (a *Account) TidyRejectsMailbox(log *mlog.Log, rejectsMailbox string) (hasSpace bool, rerr error) {
1721 var changes []Change
1722
1723 var remove []Message
1724 defer func() {
1725 for _, m := range remove {
1726 p := a.MessagePath(m.ID)
1727 err := os.Remove(p)
1728 log.Check(err, "removing rejects message file", mlog.Field("path", p))
1729 }
1730 }()
1731
1732 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1733 mb, err := a.MailboxFind(tx, rejectsMailbox)
1734 if err != nil {
1735 return fmt.Errorf("finding mailbox: %w", err)
1736 }
1737 if mb == nil {
1738 // No messages have been delivered yet.
1739 hasSpace = true
1740 return nil
1741 }
1742
1743 // Gather old messages to remove.
1744 old := time.Now().Add(-14 * 24 * time.Hour)
1745 qdel := bstore.QueryTx[Message](tx)
1746 qdel.FilterNonzero(Message{MailboxID: mb.ID})
1747 qdel.FilterEqual("Expunged", false)
1748 qdel.FilterLess("Received", old)
1749 remove, err = qdel.List()
1750 if err != nil {
1751 return fmt.Errorf("listing old messages: %w", err)
1752 }
1753
1754 changes, err = a.rejectsRemoveMessages(context.TODO(), log, tx, mb, remove)
1755 if err != nil {
1756 return fmt.Errorf("removing messages: %w", err)
1757 }
1758
1759 // We allow up to n messages.
1760 qcount := bstore.QueryTx[Message](tx)
1761 qcount.FilterNonzero(Message{MailboxID: mb.ID})
1762 qcount.FilterEqual("Expunged", false)
1763 qcount.Limit(1000)
1764 n, err := qcount.Count()
1765 if err != nil {
1766 return fmt.Errorf("counting rejects: %w", err)
1767 }
1768 hasSpace = n < 1000
1769
1770 return nil
1771 })
1772 if err != nil {
1773 remove = nil // Don't remove files on failure.
1774 return false, err
1775 }
1776
1777 BroadcastChanges(a, changes)
1778
1779 return hasSpace, nil
1780}
1781
1782func (a *Account) rejectsRemoveMessages(ctx context.Context, log *mlog.Log, tx *bstore.Tx, mb *Mailbox, l []Message) ([]Change, error) {
1783 if len(l) == 0 {
1784 return nil, nil
1785 }
1786 ids := make([]int64, len(l))
1787 anyids := make([]any, len(l))
1788 for i, m := range l {
1789 ids[i] = m.ID
1790 anyids[i] = m.ID
1791 }
1792
1793 // Remove any message recipients. Should not happen, but a user can move messages
1794 // from a Sent mailbox to the rejects mailbox...
1795 qdmr := bstore.QueryTx[Recipient](tx)
1796 qdmr.FilterEqual("MessageID", anyids...)
1797 if _, err := qdmr.Delete(); err != nil {
1798 return nil, fmt.Errorf("deleting from message recipient: %w", err)
1799 }
1800
1801 // Assign new modseq.
1802 modseq, err := a.NextModSeq(tx)
1803 if err != nil {
1804 return nil, fmt.Errorf("assign next modseq: %w", err)
1805 }
1806
1807 // Expunge the messages.
1808 qx := bstore.QueryTx[Message](tx)
1809 qx.FilterIDs(ids)
1810 var expunged []Message
1811 qx.Gather(&expunged)
1812 if _, err := qx.UpdateNonzero(Message{ModSeq: modseq, Expunged: true}); err != nil {
1813 return nil, fmt.Errorf("expunging messages: %w", err)
1814 }
1815
1816 for _, m := range expunged {
1817 m.Expunged = false // Was set by update, but would cause wrong count.
1818 mb.MailboxCounts.Sub(m.MailboxCounts())
1819 }
1820 if err := tx.Update(mb); err != nil {
1821 return nil, fmt.Errorf("updating mailbox counts: %w", err)
1822 }
1823
1824 // Mark as neutral and train so junk filter gets untrained with these (junk) messages.
1825 for i := range expunged {
1826 expunged[i].Junk = false
1827 expunged[i].Notjunk = false
1828 }
1829 if err := a.RetrainMessages(ctx, log, tx, expunged, true); err != nil {
1830 return nil, fmt.Errorf("retraining expunged messages: %w", err)
1831 }
1832
1833 changes := make([]Change, len(l), len(l)+1)
1834 for i, m := range l {
1835 changes[i] = ChangeRemoveUIDs{mb.ID, []UID{m.UID}, modseq}
1836 }
1837 changes = append(changes, mb.ChangeCounts())
1838 return changes, nil
1839}
1840
1841// RejectsRemove removes a message from the rejects mailbox if present.
1842// Caller most hold account wlock.
1843// Changes are broadcasted.
1844func (a *Account) RejectsRemove(log *mlog.Log, rejectsMailbox, messageID string) error {
1845 var changes []Change
1846
1847 var remove []Message
1848 defer func() {
1849 for _, m := range remove {
1850 p := a.MessagePath(m.ID)
1851 err := os.Remove(p)
1852 log.Check(err, "removing rejects message file", mlog.Field("path", p))
1853 }
1854 }()
1855
1856 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1857 mb, err := a.MailboxFind(tx, rejectsMailbox)
1858 if err != nil {
1859 return fmt.Errorf("finding mailbox: %w", err)
1860 }
1861 if mb == nil {
1862 return nil
1863 }
1864
1865 q := bstore.QueryTx[Message](tx)
1866 q.FilterNonzero(Message{MailboxID: mb.ID, MessageID: messageID})
1867 q.FilterEqual("Expunged", false)
1868 remove, err = q.List()
1869 if err != nil {
1870 return fmt.Errorf("listing messages to remove: %w", err)
1871 }
1872
1873 changes, err = a.rejectsRemoveMessages(context.TODO(), log, tx, mb, remove)
1874 if err != nil {
1875 return fmt.Errorf("removing messages: %w", err)
1876 }
1877
1878 return nil
1879 })
1880 if err != nil {
1881 remove = nil // Don't remove files on failure.
1882 return err
1883 }
1884
1885 BroadcastChanges(a, changes)
1886
1887 return nil
1888}
1889
1890// We keep a cache of recent successful authentications, so we don't have to bcrypt successful calls each time.
1891var authCache = struct {
1892 sync.Mutex
1893 success map[authKey]string
1894}{
1895 success: map[authKey]string{},
1896}
1897
1898type authKey struct {
1899 email, hash string
1900}
1901
1902// StartAuthCache starts a goroutine that regularly clears the auth cache.
1903func StartAuthCache() {
1904 go manageAuthCache()
1905}
1906
1907func manageAuthCache() {
1908 for {
1909 authCache.Lock()
1910 authCache.success = map[authKey]string{}
1911 authCache.Unlock()
1912 time.Sleep(15 * time.Minute)
1913 }
1914}
1915
1916// OpenEmailAuth opens an account given an email address and password.
1917//
1918// The email address may contain a catchall separator.
1919func OpenEmailAuth(email string, password string) (acc *Account, rerr error) {
1920 acc, _, rerr = OpenEmail(email)
1921 if rerr != nil {
1922 return
1923 }
1924
1925 defer func() {
1926 if rerr != nil && acc != nil {
1927 err := acc.Close()
1928 xlog.Check(err, "closing account after open auth failure")
1929 acc = nil
1930 }
1931 }()
1932
1933 pw, err := bstore.QueryDB[Password](context.TODO(), acc.DB).Get()
1934 if err != nil {
1935 if err == bstore.ErrAbsent {
1936 return acc, ErrUnknownCredentials
1937 }
1938 return acc, fmt.Errorf("looking up password: %v", err)
1939 }
1940 authCache.Lock()
1941 ok := len(password) >= 8 && authCache.success[authKey{email, pw.Hash}] == password
1942 authCache.Unlock()
1943 if ok {
1944 return
1945 }
1946 if err := bcrypt.CompareHashAndPassword([]byte(pw.Hash), []byte(password)); err != nil {
1947 rerr = ErrUnknownCredentials
1948 } else {
1949 authCache.Lock()
1950 authCache.success[authKey{email, pw.Hash}] = password
1951 authCache.Unlock()
1952 }
1953 return
1954}
1955
1956// OpenEmail opens an account given an email address.
1957//
1958// The email address may contain a catchall separator.
1959func OpenEmail(email string) (*Account, config.Destination, error) {
1960 addr, err := smtp.ParseAddress(email)
1961 if err != nil {
1962 return nil, config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
1963 }
1964 accountName, _, dest, err := mox.FindAccount(addr.Localpart, addr.Domain, false)
1965 if err != nil && (errors.Is(err, mox.ErrAccountNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
1966 return nil, config.Destination{}, ErrUnknownCredentials
1967 } else if err != nil {
1968 return nil, config.Destination{}, fmt.Errorf("looking up address: %v", err)
1969 }
1970 acc, err := OpenAccount(accountName)
1971 if err != nil {
1972 return nil, config.Destination{}, err
1973 }
1974 return acc, dest, nil
1975}
1976
1977// 64 characters, must be power of 2 for MessagePath
1978const msgDirChars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
1979
1980// MessagePath returns the filename of the on-disk filename, relative to the containing directory such as <account>/msg or queue.
1981// Returns names like "AB/1".
1982func MessagePath(messageID int64) string {
1983 return strings.Join(messagePathElems(messageID), "/")
1984}
1985
1986// messagePathElems returns the elems, for a single join without intermediate
1987// string allocations.
1988func messagePathElems(messageID int64) []string {
1989 v := messageID >> 13 // 8k files per directory.
1990 dir := ""
1991 for {
1992 dir += string(msgDirChars[int(v)&(len(msgDirChars)-1)])
1993 v >>= 6
1994 if v == 0 {
1995 break
1996 }
1997 }
1998 return []string{dir, strconv.FormatInt(messageID, 10)}
1999}
2000
2001// Set returns a copy of f, with each flag that is true in mask set to the
2002// value from flags.
2003func (f Flags) Set(mask, flags Flags) Flags {
2004 set := func(d *bool, m, v bool) {
2005 if m {
2006 *d = v
2007 }
2008 }
2009 r := f
2010 set(&r.Seen, mask.Seen, flags.Seen)
2011 set(&r.Answered, mask.Answered, flags.Answered)
2012 set(&r.Flagged, mask.Flagged, flags.Flagged)
2013 set(&r.Forwarded, mask.Forwarded, flags.Forwarded)
2014 set(&r.Junk, mask.Junk, flags.Junk)
2015 set(&r.Notjunk, mask.Notjunk, flags.Notjunk)
2016 set(&r.Deleted, mask.Deleted, flags.Deleted)
2017 set(&r.Draft, mask.Draft, flags.Draft)
2018 set(&r.Phishing, mask.Phishing, flags.Phishing)
2019 set(&r.MDNSent, mask.MDNSent, flags.MDNSent)
2020 return r
2021}
2022
2023// Changed returns a mask of flags that have been between f and other.
2024func (f Flags) Changed(other Flags) (mask Flags) {
2025 mask.Seen = f.Seen != other.Seen
2026 mask.Answered = f.Answered != other.Answered
2027 mask.Flagged = f.Flagged != other.Flagged
2028 mask.Forwarded = f.Forwarded != other.Forwarded
2029 mask.Junk = f.Junk != other.Junk
2030 mask.Notjunk = f.Notjunk != other.Notjunk
2031 mask.Deleted = f.Deleted != other.Deleted
2032 mask.Draft = f.Draft != other.Draft
2033 mask.Phishing = f.Phishing != other.Phishing
2034 mask.MDNSent = f.MDNSent != other.MDNSent
2035 return
2036}
2037
2038var systemWellKnownFlags = map[string]bool{
2039 `\answered`: true,
2040 `\flagged`: true,
2041 `\deleted`: true,
2042 `\seen`: true,
2043 `\draft`: true,
2044 `$junk`: true,
2045 `$notjunk`: true,
2046 `$forwarded`: true,
2047 `$phishing`: true,
2048 `$mdnsent`: true,
2049}
2050
2051// ParseFlagsKeywords parses a list of textual flags into system/known flags, and
2052// other keywords. Keywords are lower-cased and sorted and check for valid syntax.
2053func ParseFlagsKeywords(l []string) (flags Flags, keywords []string, rerr error) {
2054 fields := map[string]*bool{
2055 `\answered`: &flags.Answered,
2056 `\flagged`: &flags.Flagged,
2057 `\deleted`: &flags.Deleted,
2058 `\seen`: &flags.Seen,
2059 `\draft`: &flags.Draft,
2060 `$junk`: &flags.Junk,
2061 `$notjunk`: &flags.Notjunk,
2062 `$forwarded`: &flags.Forwarded,
2063 `$phishing`: &flags.Phishing,
2064 `$mdnsent`: &flags.MDNSent,
2065 }
2066 seen := map[string]bool{}
2067 for _, f := range l {
2068 f = strings.ToLower(f)
2069 if field, ok := fields[f]; ok {
2070 *field = true
2071 } else if seen[f] {
2072 if moxvar.Pedantic {
2073 return Flags{}, nil, fmt.Errorf("duplicate keyword %s", f)
2074 }
2075 } else {
2076 if err := CheckKeyword(f); err != nil {
2077 return Flags{}, nil, fmt.Errorf("invalid keyword %s", f)
2078 }
2079 keywords = append(keywords, f)
2080 seen[f] = true
2081 }
2082 }
2083 sort.Strings(keywords)
2084 return flags, keywords, nil
2085}
2086
2087// RemoveKeywords removes keywords from l, returning whether any modifications were
2088// made, and a slice, a new slice in case of modifications. Keywords must have been
2089// validated earlier, e.g. through ParseFlagKeywords or CheckKeyword. Should only
2090// be used with valid keywords, not with system flags like \Seen.
2091func RemoveKeywords(l, remove []string) ([]string, bool) {
2092 var copied bool
2093 var changed bool
2094 for _, k := range remove {
2095 if i := slices.Index(l, k); i >= 0 {
2096 if !copied {
2097 l = append([]string{}, l...)
2098 copied = true
2099 }
2100 copy(l[i:], l[i+1:])
2101 l = l[:len(l)-1]
2102 changed = true
2103 }
2104 }
2105 return l, changed
2106}
2107
2108// MergeKeywords adds keywords from add into l, returning whether it added any
2109// keyword, and the slice with keywords, a new slice if modifications were made.
2110// Keywords are only added if they aren't already present. Should only be used with
2111// keywords, not with system flags like \Seen.
2112func MergeKeywords(l, add []string) ([]string, bool) {
2113 var copied bool
2114 var changed bool
2115 for _, k := range add {
2116 if !slices.Contains(l, k) {
2117 if !copied {
2118 l = append([]string{}, l...)
2119 copied = true
2120 }
2121 l = append(l, k)
2122 changed = true
2123 }
2124 }
2125 if changed {
2126 sort.Strings(l)
2127 }
2128 return l, changed
2129}
2130
2131// CheckKeyword returns an error if kw is not a valid keyword. Kw should
2132// already be in lower-case.
2133func CheckKeyword(kw string) error {
2134 if kw == "" {
2135 return fmt.Errorf("keyword cannot be empty")
2136 }
2137 if systemWellKnownFlags[kw] {
2138 return fmt.Errorf("cannot use well-known flag as keyword")
2139 }
2140 for _, c := range kw {
2141 // ../rfc/9051:6334
2142 if c <= ' ' || c > 0x7e || c >= 'A' && c <= 'Z' || strings.ContainsRune(`(){%*"\]`, c) {
2143 return errors.New(`not a valid keyword, must be lower-case ascii without spaces and without any of these characters: (){%*"\]`)
2144 }
2145 }
2146 return nil
2147}
2148
2149// SendLimitReached checks whether sending a message to recipients would reach
2150// the limit of outgoing messages for the account. If so, the message should
2151// not be sent. If the returned numbers are >= 0, the limit was reached and the
2152// values are the configured limits.
2153//
2154// To limit damage to the internet and our reputation in case of account
2155// compromise, we limit the max number of messages sent in a 24 hour window, both
2156// total number of messages and number of first-time recipients.
2157func (a *Account) SendLimitReached(tx *bstore.Tx, recipients []smtp.Path) (msglimit, rcptlimit int, rerr error) {
2158 conf, _ := a.Conf()
2159 msgmax := conf.MaxOutgoingMessagesPerDay
2160 if msgmax == 0 {
2161 // For human senders, 1000 recipients in a day is quite a lot.
2162 msgmax = 1000
2163 }
2164 rcptmax := conf.MaxFirstTimeRecipientsPerDay
2165 if rcptmax == 0 {
2166 // Human senders may address a new human-sized list of people once in a while. In
2167 // case of a compromise, a spammer will probably try to send to many new addresses.
2168 rcptmax = 200
2169 }
2170
2171 rcpts := map[string]time.Time{}
2172 n := 0
2173 err := bstore.QueryTx[Outgoing](tx).FilterGreater("Submitted", time.Now().Add(-24*time.Hour)).ForEach(func(o Outgoing) error {
2174 n++
2175 if rcpts[o.Recipient].IsZero() || o.Submitted.Before(rcpts[o.Recipient]) {
2176 rcpts[o.Recipient] = o.Submitted
2177 }
2178 return nil
2179 })
2180 if err != nil {
2181 return -1, -1, fmt.Errorf("querying message recipients in past 24h: %w", err)
2182 }
2183 if n+len(recipients) > msgmax {
2184 return msgmax, -1, nil
2185 }
2186
2187 // Only check if max first-time recipients is reached if there are enough messages
2188 // to trigger the limit.
2189 if n+len(recipients) < rcptmax {
2190 return -1, -1, nil
2191 }
2192
2193 isFirstTime := func(rcpt string, before time.Time) (bool, error) {
2194 exists, err := bstore.QueryTx[Outgoing](tx).FilterNonzero(Outgoing{Recipient: rcpt}).FilterLess("Submitted", before).Exists()
2195 return !exists, err
2196 }
2197
2198 firsttime := 0
2199 now := time.Now()
2200 for _, r := range recipients {
2201 if first, err := isFirstTime(r.XString(true), now); err != nil {
2202 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
2203 } else if first {
2204 firsttime++
2205 }
2206 }
2207 for r, t := range rcpts {
2208 if first, err := isFirstTime(r, t); err != nil {
2209 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
2210 } else if first {
2211 firsttime++
2212 }
2213 }
2214 if firsttime > rcptmax {
2215 return -1, rcptmax, nil
2216 }
2217 return -1, -1, nil
2218}
2219
2220// MailboxCreate creates a new mailbox, including any missing parent mailboxes,
2221// the total list of created mailboxes is returned in created. On success, if
2222// exists is false and rerr nil, the changes must be broadcasted by the caller.
2223//
2224// Name must be in normalized form.
2225func (a *Account) MailboxCreate(tx *bstore.Tx, name string) (changes []Change, created []string, exists bool, rerr error) {
2226 elems := strings.Split(name, "/")
2227 var p string
2228 for i, elem := range elems {
2229 if i > 0 {
2230 p += "/"
2231 }
2232 p += elem
2233 exists, err := a.MailboxExists(tx, p)
2234 if err != nil {
2235 return nil, nil, false, fmt.Errorf("checking if mailbox exists")
2236 }
2237 if exists {
2238 if i == len(elems)-1 {
2239 return nil, nil, true, fmt.Errorf("mailbox already exists")
2240 }
2241 continue
2242 }
2243 _, nchanges, err := a.MailboxEnsure(tx, p, true)
2244 if err != nil {
2245 return nil, nil, false, fmt.Errorf("ensuring mailbox exists")
2246 }
2247 changes = append(changes, nchanges...)
2248 created = append(created, p)
2249 }
2250 return changes, created, false, nil
2251}
2252
2253// MailboxRename renames mailbox mbsrc to dst, and any missing parents for the
2254// destination, and any children of mbsrc and the destination.
2255//
2256// Names must be normalized and cannot be Inbox.
2257func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (changes []Change, isInbox, notExists, alreadyExists bool, rerr error) {
2258 if mbsrc.Name == "Inbox" || dst == "Inbox" {
2259 return nil, true, false, false, fmt.Errorf("inbox cannot be renamed")
2260 }
2261
2262 // We gather existing mailboxes that we need for deciding what to create/delete/update.
2263 q := bstore.QueryTx[Mailbox](tx)
2264 srcPrefix := mbsrc.Name + "/"
2265 dstRoot := strings.SplitN(dst, "/", 2)[0]
2266 dstRootPrefix := dstRoot + "/"
2267 q.FilterFn(func(mb Mailbox) bool {
2268 return mb.Name == mbsrc.Name || strings.HasPrefix(mb.Name, srcPrefix) || mb.Name == dstRoot || strings.HasPrefix(mb.Name, dstRootPrefix)
2269 })
2270 q.SortAsc("Name") // We'll rename the parents before children.
2271 l, err := q.List()
2272 if err != nil {
2273 return nil, false, false, false, fmt.Errorf("listing relevant mailboxes: %v", err)
2274 }
2275
2276 mailboxes := map[string]Mailbox{}
2277 for _, mb := range l {
2278 mailboxes[mb.Name] = mb
2279 }
2280
2281 if _, ok := mailboxes[mbsrc.Name]; !ok {
2282 return nil, false, true, false, fmt.Errorf("mailbox does not exist")
2283 }
2284
2285 uidval, err := a.NextUIDValidity(tx)
2286 if err != nil {
2287 return nil, false, false, false, fmt.Errorf("next uid validity: %v", err)
2288 }
2289
2290 // Ensure parent mailboxes for the destination paths exist.
2291 var parent string
2292 dstElems := strings.Split(dst, "/")
2293 for i, elem := range dstElems[:len(dstElems)-1] {
2294 if i > 0 {
2295 parent += "/"
2296 }
2297 parent += elem
2298
2299 mb, ok := mailboxes[parent]
2300 if ok {
2301 continue
2302 }
2303 omb := mb
2304 mb = Mailbox{
2305 ID: omb.ID,
2306 Name: parent,
2307 UIDValidity: uidval,
2308 UIDNext: 1,
2309 HaveCounts: true,
2310 }
2311 if err := tx.Insert(&mb); err != nil {
2312 return nil, false, false, false, fmt.Errorf("creating parent mailbox %q: %v", mb.Name, err)
2313 }
2314 if err := tx.Get(&Subscription{Name: parent}); err != nil {
2315 if err := tx.Insert(&Subscription{Name: parent}); err != nil {
2316 return nil, false, false, false, fmt.Errorf("creating subscription for %q: %v", parent, err)
2317 }
2318 }
2319 changes = append(changes, ChangeAddMailbox{Mailbox: mb, Flags: []string{`\Subscribed`}})
2320 }
2321
2322 // Process src mailboxes, renaming them to dst.
2323 for _, srcmb := range l {
2324 if srcmb.Name != mbsrc.Name && !strings.HasPrefix(srcmb.Name, srcPrefix) {
2325 continue
2326 }
2327 srcName := srcmb.Name
2328 dstName := dst + srcmb.Name[len(mbsrc.Name):]
2329 if _, ok := mailboxes[dstName]; ok {
2330 return nil, false, false, true, fmt.Errorf("destination mailbox %q already exists", dstName)
2331 }
2332
2333 srcmb.Name = dstName
2334 srcmb.UIDValidity = uidval
2335 if err := tx.Update(&srcmb); err != nil {
2336 return nil, false, false, false, fmt.Errorf("renaming mailbox: %v", err)
2337 }
2338
2339 var dstFlags []string
2340 if tx.Get(&Subscription{Name: dstName}) == nil {
2341 dstFlags = []string{`\Subscribed`}
2342 }
2343 changes = append(changes, ChangeRenameMailbox{MailboxID: srcmb.ID, OldName: srcName, NewName: dstName, Flags: dstFlags})
2344 }
2345
2346 // If we renamed e.g. a/b to a/b/c/d, and a/b/c to a/b/c/d/c, we'll have to recreate a/b and a/b/c.
2347 srcElems := strings.Split(mbsrc.Name, "/")
2348 xsrc := mbsrc.Name
2349 for i := 0; i < len(dstElems) && strings.HasPrefix(dst, xsrc+"/"); i++ {
2350 mb := Mailbox{
2351 UIDValidity: uidval,
2352 UIDNext: 1,
2353 Name: xsrc,
2354 HaveCounts: true,
2355 }
2356 if err := tx.Insert(&mb); err != nil {
2357 return nil, false, false, false, fmt.Errorf("creating mailbox at old path %q: %v", mb.Name, err)
2358 }
2359 xsrc += "/" + dstElems[len(srcElems)+i]
2360 }
2361 return changes, false, false, false, nil
2362}
2363
2364// MailboxDelete deletes a mailbox by ID. If it has children, the return value
2365// indicates that and an error is returned.
2366//
2367// Caller should broadcast the changes and remove files for the removed message IDs.
2368func (a *Account) MailboxDelete(ctx context.Context, log *mlog.Log, tx *bstore.Tx, mailbox Mailbox) (changes []Change, removeMessageIDs []int64, hasChildren bool, rerr error) {
2369 // Look for existence of child mailboxes. There is a lot of text in the IMAP RFCs about
2370 // NoInferior and NoSelect. We just require only leaf mailboxes are deleted.
2371 qmb := bstore.QueryTx[Mailbox](tx)
2372 mbprefix := mailbox.Name + "/"
2373 qmb.FilterFn(func(mb Mailbox) bool {
2374 return strings.HasPrefix(mb.Name, mbprefix)
2375 })
2376 if childExists, err := qmb.Exists(); err != nil {
2377 return nil, nil, false, fmt.Errorf("checking if mailbox has child: %v", err)
2378 } else if childExists {
2379 return nil, nil, true, fmt.Errorf("mailbox has a child, only leaf mailboxes can be deleted")
2380 }
2381
2382 // todo jmap: instead of completely deleting a mailbox and its messages, we need to mark them all as expunged.
2383
2384 qm := bstore.QueryTx[Message](tx)
2385 qm.FilterNonzero(Message{MailboxID: mailbox.ID})
2386 remove, err := qm.List()
2387 if err != nil {
2388 return nil, nil, false, fmt.Errorf("listing messages to remove: %v", err)
2389 }
2390
2391 if len(remove) > 0 {
2392 removeIDs := make([]any, len(remove))
2393 for i, m := range remove {
2394 removeIDs[i] = m.ID
2395 }
2396 qmr := bstore.QueryTx[Recipient](tx)
2397 qmr.FilterEqual("MessageID", removeIDs...)
2398 if _, err = qmr.Delete(); err != nil {
2399 return nil, nil, false, fmt.Errorf("removing message recipients for messages: %v", err)
2400 }
2401
2402 qm = bstore.QueryTx[Message](tx)
2403 qm.FilterNonzero(Message{MailboxID: mailbox.ID})
2404 if _, err := qm.Delete(); err != nil {
2405 return nil, nil, false, fmt.Errorf("removing messages: %v", err)
2406 }
2407
2408 for _, m := range remove {
2409 if !m.Expunged {
2410 removeMessageIDs = append(removeMessageIDs, m.ID)
2411 }
2412 }
2413
2414 // Mark messages as not needing training. Then retrain them, so they are untrained if they were.
2415 n := 0
2416 o := 0
2417 for _, m := range remove {
2418 if !m.Expunged {
2419 remove[o] = m
2420 remove[o].Junk = false
2421 remove[o].Notjunk = false
2422 n++
2423 }
2424 }
2425 remove = remove[:n]
2426 if err := a.RetrainMessages(ctx, log, tx, remove, true); err != nil {
2427 return nil, nil, false, fmt.Errorf("untraining deleted messages: %v", err)
2428 }
2429 }
2430
2431 if err := tx.Delete(&Mailbox{ID: mailbox.ID}); err != nil {
2432 return nil, nil, false, fmt.Errorf("removing mailbox: %v", err)
2433 }
2434 return []Change{ChangeRemoveMailbox{MailboxID: mailbox.ID, Name: mailbox.Name}}, removeMessageIDs, false, nil
2435}
2436
2437// CheckMailboxName checks if name is valid, returning an INBOX-normalized name.
2438// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
2439// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
2440// unicode-normalized, or when empty or has special characters.
2441//
2442// If name is the inbox, and allowInbox is false, this is indicated with the isInbox return parameter.
2443// For that case, and for other invalid names, an error is returned.
2444func CheckMailboxName(name string, allowInbox bool) (normalizedName string, isInbox bool, rerr error) {
2445 first := strings.SplitN(name, "/", 2)[0]
2446 if strings.EqualFold(first, "inbox") {
2447 if len(name) == len("inbox") && !allowInbox {
2448 return "", true, fmt.Errorf("special mailbox name Inbox not allowed")
2449 }
2450 name = "Inbox" + name[len("Inbox"):]
2451 }
2452
2453 if norm.NFC.String(name) != name {
2454 return "", false, errors.New("non-unicode-normalized mailbox names not allowed")
2455 }
2456
2457 if name == "" {
2458 return "", false, errors.New("empty mailbox name")
2459 }
2460 if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.Contains(name, "//") {
2461 return "", false, errors.New("bad slashes in mailbox name")
2462 }
2463 for _, c := range name {
2464 switch c {
2465 case '%', '*', '#', '&':
2466 return "", false, fmt.Errorf("character %c not allowed in mailbox name", c)
2467 }
2468 // ../rfc/6855:192
2469 if c <= 0x1f || c >= 0x7f && c <= 0x9f || c == 0x2028 || c == 0x2029 {
2470 return "", false, errors.New("control characters not allowed in mailbox name")
2471 }
2472 }
2473 return name, false, nil
2474}
2475