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).
6Layout of storage for accounts:
8 <DataDir>/accounts/<name>/index.db
9 <DataDir>/accounts/<name>/msg/[a-zA-Z0-9_-]+/<id>
11Index.db holds tables for user information, mailboxes, and messages. Message contents
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
22// todo: make up a function naming scheme that indicates whether caller should broadcast changes.
27 cryptorand "crypto/rand"
49 "golang.org/x/crypto/bcrypt"
50 "golang.org/x/text/secure/precis"
51 "golang.org/x/text/unicode/norm"
53 "github.com/mjl-/bstore"
55 "github.com/mjl-/mox/config"
56 "github.com/mjl-/mox/dns"
57 "github.com/mjl-/mox/junk"
58 "github.com/mjl-/mox/message"
59 "github.com/mjl-/mox/metrics"
60 "github.com/mjl-/mox/mlog"
61 "github.com/mjl-/mox/mox-"
62 "github.com/mjl-/mox/moxio"
63 "github.com/mjl-/mox/moxvar"
64 "github.com/mjl-/mox/publicsuffix"
65 "github.com/mjl-/mox/scram"
66 "github.com/mjl-/mox/smtp"
69// If true, each time an account is closed its database file is checked for
70// consistency. If an inconsistency is found, panic is called. Set by default
71// because of all the packages with tests, the mox main function sets it to
73var CheckConsistencyOnClose = true
76 ErrUnknownMailbox = errors.New("no such mailbox")
77 ErrUnknownCredentials = errors.New("credentials not found")
78 ErrAccountUnknown = errors.New("no such account")
79 ErrOverQuota = errors.New("account over quota")
80 ErrLoginDisabled = errors.New("login disabled for account")
83var DefaultInitialMailboxes = config.InitialMailboxes{
84 SpecialUse: config.SpecialUseMailboxes{
99// CRAMMD5 holds HMAC ipad and opad hashes that are initialized with the first
100// block with (a derivation of) the key/password, so we don't store the password in plain
107// BinaryMarshal is used by bstore to store the ipad/opad hash states.
108func (c CRAMMD5) MarshalBinary() ([]byte, error) {
109 if c.Ipad == nil || c.Opad == nil {
113 ipad, err := c.Ipad.(encoding.BinaryMarshaler).MarshalBinary()
115 return nil, fmt.Errorf("marshal ipad: %v", err)
117 opad, err := c.Opad.(encoding.BinaryMarshaler).MarshalBinary()
119 return nil, fmt.Errorf("marshal opad: %v", err)
121 buf := make([]byte, 2+len(ipad)+len(opad))
122 ipadlen := uint16(len(ipad))
123 buf[0] = byte(ipadlen >> 8)
124 buf[1] = byte(ipadlen >> 0)
126 copy(buf[2+len(ipad):], opad)
130// BinaryUnmarshal is used by bstore to restore the ipad/opad hash states.
131func (c *CRAMMD5) UnmarshalBinary(buf []byte) error {
137 return fmt.Errorf("short buffer")
139 ipadlen := int(uint16(buf[0])<<8 | uint16(buf[1])<<0)
140 if len(buf) < 2+ipadlen {
141 return fmt.Errorf("buffer too short for ipadlen")
145 if err := ipad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2 : 2+ipadlen]); err != nil {
146 return fmt.Errorf("unmarshal ipad: %v", err)
148 if err := opad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2+ipadlen:]); err != nil {
149 return fmt.Errorf("unmarshal opad: %v", err)
151 *c = CRAMMD5{ipad, opad}
155// Password holds credentials in various forms, for logging in with SMTP/IMAP.
156type Password struct {
157 Hash string // bcrypt hash for IMAP LOGIN, SASL PLAIN and HTTP basic authentication.
158 CRAMMD5 CRAMMD5 // For SASL CRAM-MD5.
159 SCRAMSHA1 SCRAM // For SASL SCRAM-SHA-1.
160 SCRAMSHA256 SCRAM // For SASL SCRAM-SHA-256.
163// Subjectpass holds the secret key used to sign subjectpass tokens.
164type Subjectpass struct {
165 Email string // Our destination address (canonical, with catchall localpart stripped).
169// NextUIDValidity is a singleton record in the database with the next UIDValidity
170// to use for the next mailbox.
171type NextUIDValidity struct {
172 ID int // Just a single record with ID 1.
176// SyncState track ModSeqs.
177type SyncState struct {
178 ID int // Just a single record with ID 1.
180 // Last used, next assigned will be one higher. The first value we hand out is 2.
181 // That's because 0 (the default value for old existing messages, from before the
182 // Message.ModSeq field) is special in IMAP, so we return it as 1.
183 LastModSeq ModSeq `bstore:"nonzero"`
185 // Highest ModSeq of expunged record that we deleted. When a clients synchronizes
186 // and requests changes based on a modseq before this one, we don't have the
187 // history to provide information about deletions. We normally keep these expunged
188 // records around, but we may periodically truly delete them to reclaim storage
189 // space. Initially set to -1 because we don't want to match with any ModSeq in the
190 // database, which can be zero values.
191 HighestDeletedModSeq ModSeq
194// Mailbox is collection of messages, e.g. Inbox or Sent.
199 ModSeq ModSeq `bstore:"index"` // Of last change, or when deleted.
202 ParentID int64 `bstore:"ref Mailbox"` // Zero for top-level mailbox.
204 // "Inbox" is the name for the special IMAP "INBOX". Slash separated for hierarchy.
205 // Names must be unique for mailboxes that are not expunged.
206 Name string `bstore:"nonzero"`
208 // If UIDs are invalidated, e.g. when renaming a mailbox to a previously existing
209 // name, UIDValidity must be changed. Used by IMAP for synchronization.
212 // UID likely to be assigned to next message. Used by IMAP to detect messages
213 // delivered to a mailbox.
218 // Keywords as used in messages. Storing a non-system keyword for a message
219 // automatically adds it to this list. Used in the IMAP FLAGS response. Only
220 // "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in
221 // lower case (for JMAP), sorted.
224 HaveCounts bool // Deprecated. Covered by Upgrade.MailboxCounts. No longer read.
225 MailboxCounts // Statistics about messages, kept up to date whenever a change happens.
228// Annotation is a per-mailbox or global (per-account) annotation for the IMAP
229// metadata extension, currently always a private annotation.
230type Annotation struct {
234 ModSeq ModSeq `bstore:"index"`
237 // Can be zero, indicates global (per-account) annotation.
238 MailboxID int64 `bstore:"ref Mailbox,index MailboxID+Key"`
240 // "Entry name", always starts with "/private/" or "/shared/". Stored lower-case,
241 // comparisons must be done case-insensitively.
242 Key string `bstore:"nonzero"`
244 IsString bool // If true, the value is a string instead of bytes.
248// Change returns a broadcastable change for the annotation.
249func (a Annotation) Change(mailboxName string) ChangeAnnotation {
250 return ChangeAnnotation{a.MailboxID, mailboxName, a.Key, a.ModSeq}
253// MailboxCounts tracks statistics about messages for a mailbox.
254type MailboxCounts struct {
255 Total int64 // Total number of messages, excluding \Deleted. For JMAP.
256 Deleted int64 // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted.
257 Unread int64 // Messages without \Seen, excluding those with \Deleted, for JMAP.
258 Unseen int64 // Messages without \Seen, including those with \Deleted, for IMAP.
259 Size int64 // Number of bytes for all messages.
262// MessageCountIMAP returns the total message count for use in IMAP. In IMAP,
263// message marked \Deleted are included, in JMAP they those messages are not
265func (mc MailboxCounts) MessageCountIMAP() uint32 {
266 return uint32(mc.Total + mc.Deleted)
269func (mc MailboxCounts) String() string {
270 return fmt.Sprintf("%d total, %d deleted, %d unread, %d unseen, size %d bytes", mc.Total, mc.Deleted, mc.Unread, mc.Unseen, mc.Size)
273// Add increases mailbox counts mc with those of delta.
274func (mc *MailboxCounts) Add(delta MailboxCounts) {
275 mc.Total += delta.Total
276 mc.Deleted += delta.Deleted
277 mc.Unread += delta.Unread
278 mc.Unseen += delta.Unseen
279 mc.Size += delta.Size
282// Add decreases mailbox counts mc with those of delta.
283func (mc *MailboxCounts) Sub(delta MailboxCounts) {
284 mc.Total -= delta.Total
285 mc.Deleted -= delta.Deleted
286 mc.Unread -= delta.Unread
287 mc.Unseen -= delta.Unseen
288 mc.Size -= delta.Size
291// SpecialUse identifies a specific role for a mailbox, used by clients to
292// understand where messages should go.
293type SpecialUse struct {
295 Draft bool // "Drafts"
301// UIDNextAdd increases the UIDNext value by n, returning an error on overflow.
302func (mb *Mailbox) UIDNextAdd(n int) error {
303 uidnext := mb.UIDNext + UID(n)
304 if uidnext < mb.UIDNext {
305 return fmt.Errorf("uid overflow on mailbox %q (id %d): uidnext %d, adding %d; consider recreating the mailbox and copying its messages to compact", mb.Name, mb.ID, mb.UIDNext, n)
311// CalculateCounts calculates the full current counts for messages in the mailbox.
312func (mb *Mailbox) CalculateCounts(tx *bstore.Tx) (mc MailboxCounts, err error) {
313 q := bstore.QueryTx[Message](tx)
314 q.FilterNonzero(Message{MailboxID: mb.ID})
315 q.FilterEqual("Expunged", false)
316 err = q.ForEach(func(m Message) error {
317 mc.Add(m.MailboxCounts())
323// ChangeSpecialUse returns a change for special-use flags, for broadcasting to
325func (mb Mailbox) ChangeSpecialUse() ChangeMailboxSpecialUse {
326 return ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse, mb.ModSeq}
329// ChangeKeywords returns a change with new keywords for a mailbox (e.g. after
330// setting a new keyword on a message in the mailbox), for broadcasting to other
332func (mb Mailbox) ChangeKeywords() ChangeMailboxKeywords {
333 return ChangeMailboxKeywords{mb.ID, mb.Name, mb.Keywords}
336func (mb Mailbox) ChangeAddMailbox(flags []string) ChangeAddMailbox {
337 return ChangeAddMailbox{Mailbox: mb, Flags: flags}
340func (mb Mailbox) ChangeRemoveMailbox() ChangeRemoveMailbox {
341 return ChangeRemoveMailbox{mb.ID, mb.Name, mb.ModSeq}
344// KeywordsChanged returns whether the keywords in a mailbox have changed.
345func (mb Mailbox) KeywordsChanged(origmb Mailbox) bool {
346 if len(mb.Keywords) != len(origmb.Keywords) {
349 // Keywords are stored sorted.
350 for i, kw := range mb.Keywords {
351 if origmb.Keywords[i] != kw {
358// CountsChange returns a change with mailbox counts.
359func (mb Mailbox) ChangeCounts() ChangeMailboxCounts {
360 return ChangeMailboxCounts{mb.ID, mb.Name, mb.MailboxCounts}
363// Subscriptions are separate from existence of mailboxes.
364type Subscription struct {
368// Flags for a mail message.
382// FlagsAll is all flags set, for use as mask.
383var FlagsAll = Flags{true, true, true, true, true, true, true, true, true, true}
385// Validation of "message From" domain.
389 ValidationUnknown Validation = 0
390 ValidationStrict Validation = 1 // Like DMARC, with strict policies.
391 ValidationDMARC Validation = 2 // Actual DMARC policy.
392 ValidationRelaxed Validation = 3 // Like DMARC, with relaxed policies.
393 ValidationPass Validation = 4 // For SPF.
394 ValidationNeutral Validation = 5 // For SPF.
395 ValidationTemperror Validation = 6
396 ValidationPermerror Validation = 7
397 ValidationFail Validation = 8
398 ValidationSoftfail Validation = 9 // For SPF.
399 ValidationNone Validation = 10 // E.g. No records.
402// Message stored in database and per-message file on disk.
404// Contents are always the combined data from MsgPrefix and the on-disk file named
407// Messages always have a header section, even if empty. Incoming messages without
408// header section must get an empty header section added before inserting.
410 // ID of the message, determines path to on-disk message file. Set when adding to a
411 // mailbox. When a message is moved to another mailbox, the mailbox ID is changed,
412 // but for synchronization purposes, a new Message record is inserted (which gets a
413 // new ID) with the Expunged field set and the MailboxID and UID copied.
416 // UID, for IMAP. Set when adding to mailbox. Strictly increasing values, per
417 // mailbox. The UID of a message can never change (though messages can be copied),
418 // and the contents of a message/UID also never changes.
419 UID UID `bstore:"nonzero"`
421 MailboxID int64 `bstore:"nonzero,unique MailboxID+UID,index MailboxID+Received,index MailboxID+ModSeq,ref Mailbox"`
423 // Modification sequence, for faster syncing with IMAP QRESYNC and JMAP.
424 // ModSeq is the last modification. CreateSeq is the Seq the message was inserted,
425 // always <= ModSeq. If Expunged is set, the message has been removed and should not
426 // be returned to the user. In this case, ModSeq is the Seq where the message is
427 // removed, and will never be changed again.
428 // We have an index on both ModSeq (for JMAP that synchronizes per account) and
429 // MailboxID+ModSeq (for IMAP that synchronizes per mailbox).
430 // The index on CreateSeq helps efficiently finding created messages for JMAP.
431 // The value of ModSeq is special for IMAP. Messages that existed before ModSeq was
432 // added have 0 as value. But modseq 0 in IMAP is special, so we return it as 1. If
433 // we get modseq 1 from a client, the IMAP server will translate it to 0. When we
434 // return modseq to clients, we turn 0 into 1.
435 ModSeq ModSeq `bstore:"index"`
436 CreateSeq ModSeq `bstore:"index"`
439 // If set, this message was delivered to a Rejects mailbox. When it is moved to a
440 // different mailbox, its MailboxOrigID is set to the destination mailbox and this
444 // If set, this is a forwarded message (through a ruleset with IsForward). This
445 // causes fields used during junk analysis to be moved to their Orig variants, and
446 // masked IP fields cleared, so they aren't used in junk classifications for
447 // incoming messages. This ensures the forwarded messages don't cause negative
448 // reputation for the forwarding mail server, which may also be sending regular
452 // MailboxOrigID is the mailbox the message was originally delivered to. Typically
453 // Inbox or Rejects, but can also be a mailbox configured in a Ruleset, or
454 // Postmaster, TLS/DMARC reporting addresses. MailboxOrigID is not changed when the
455 // message is moved to another mailbox, e.g. Archive/Trash/Junk. Used for
456 // per-mailbox reputation.
458 // MailboxDestinedID is normally 0, but when a message is delivered to the Rejects
459 // mailbox, it is set to the intended mailbox according to delivery rules,
460 // typically that of Inbox. When such a message is moved out of Rejects, the
461 // MailboxOrigID is corrected by setting it to MailboxDestinedID. This ensures the
462 // message is used for reputation calculation for future deliveries to that
465 // These are not bstore references to prevent having to update all messages in a
466 // mailbox when the original mailbox is removed. Use of these fields requires
467 // checking if the mailbox still exists.
469 MailboxDestinedID int64
471 // Received indicates time of receival over SMTP, or of IMAP APPEND.
472 Received time.Time `bstore:"default now,index"`
474 // SaveDate is the time of copy/move/save to a mailbox, used with IMAP SAVEDATE
475 // extension. Must be updated each time a message is copied/moved to another
476 // mailbox. Can be nil for messages from before this functionality was introduced.
477 SaveDate *time.Time `bstore:"default now"`
479 // Full IP address of remote SMTP server. Empty if not delivered over SMTP. The
480 // masked IPs are used to classify incoming messages. They are left empty for
481 // messages matching a ruleset for forwarded messages.
483 RemoteIPMasked1 string `bstore:"index RemoteIPMasked1+Received"` // For IPv4 /32, for IPv6 /64, for reputation.
484 RemoteIPMasked2 string `bstore:"index RemoteIPMasked2+Received"` // For IPv4 /26, for IPv6 /48.
485 RemoteIPMasked3 string `bstore:"index RemoteIPMasked3+Received"` // For IPv4 /21, for IPv6 /32.
487 // Only set if present and not an IP address. Unicode string. Empty for forwarded
489 EHLODomain string `bstore:"index EHLODomain+Received"`
490 MailFrom string // With localpart and domain. Can be empty.
491 MailFromLocalpart smtp.Localpart // SMTP "MAIL FROM", can be empty.
492 // Only set if it is a domain, not an IP. Unicode string. Empty for forwarded
493 // messages, but see OrigMailFromDomain.
494 MailFromDomain string `bstore:"index MailFromDomain+Received"`
495 RcptToLocalpart smtp.Localpart // SMTP "RCPT TO", can be empty.
496 RcptToDomain string // Unicode string.
498 // Parsed "From" message header, used for reputation along with domain validation.
499 MsgFromLocalpart smtp.Localpart
500 MsgFromDomain string `bstore:"index MsgFromDomain+Received"` // Unicode string.
501 MsgFromOrgDomain string `bstore:"index MsgFromOrgDomain+Received"` // Unicode string.
503 // Simplified statements of the Validation fields below, used for incoming messages
504 // to check reputation.
506 MailFromValidated bool
507 MsgFromValidated bool
509 EHLOValidation Validation // Validation can also take reverse IP lookup into account, not only SPF.
510 MailFromValidation Validation // Can have SPF-specific validations like ValidationSoftfail.
511 MsgFromValidation Validation // Desirable validations: Strict, DMARC, Relaxed. Will not be just Pass.
513 // Domains with verified DKIM signatures. Unicode string. For forwarded messages, a
514 // DKIM domain that matched a ruleset's verified domain is left out, but included
515 // in OrigDKIMDomains.
516 DKIMDomains []string `bstore:"index DKIMDomains+Received"`
518 // For forwarded messages,
519 OrigEHLODomain string
520 OrigDKIMDomains []string
522 // Canonicalized Message-Id, always lower-case and normalized quoting, without
523 // <>'s. Empty if missing. Used for matching message threads, and to prevent
524 // duplicate reject delivery.
525 MessageID string `bstore:"index"`
528 // For matching threads in case there is no References/In-Reply-To header. It is
529 // lower-cased, white-space collapsed, mailing list tags and re/fwd tags removed.
530 SubjectBase string `bstore:"index"`
533 // Hash of message. For rejects delivery in case there is no Message-ID, only set
534 // when delivered as reject.
537 // ID of message starting this thread.
538 ThreadID int64 `bstore:"index"`
539 // IDs of parent messages, from closest parent to the root message. Parent messages
540 // may be in a different mailbox, or may no longer exist. ThreadParentIDs must
541 // never contain the message id itself (a cycle), and parent messages must
542 // reference the same ancestors. Moving a message to another mailbox keeps the
543 // message ID and changes the MailboxID (and UID) of the message, leaving threading
544 // parent ids intact.
545 ThreadParentIDs []int64
546 // ThreadMissingLink is true if there is no match with a direct parent. E.g. first
547 // ID in ThreadParentIDs is not the direct ancestor (an intermediate message may
548 // have been deleted), or subject-based matching was done.
549 ThreadMissingLink bool
550 // If set, newly delivered child messages are automatically marked as read. This
551 // field is copied to new child messages. Changes are propagated to the webmail
554 // If set, this (sub)thread is collapsed in the webmail client, for threading mode
555 // "on" (mode "unread" ignores it). This field is copied to new child message.
556 // Changes are propagated to the webmail client.
559 // If received message was known to match a mailing list rule (with modified junk
563 // If this message is a DSN, generated by us or received. For DSNs, we don't look
564 // at the subject when matching threads.
567 ReceivedTLSVersion uint16 // 0 if unknown, 1 if plaintext/no TLS, otherwise TLS cipher suite.
568 ReceivedTLSCipherSuite uint16
569 ReceivedRequireTLS bool // Whether RequireTLS was known to be used for incoming delivery.
572 // For keywords other than system flags or the basic well-known $-flags. Only in
573 // "atom" syntax (IMAP), they are case-insensitive, always stored in lower-case
574 // (for JMAP), sorted.
575 Keywords []string `bstore:"index"`
577 TrainedJunk *bool // If nil, no training done yet. Otherwise, true is trained as junk, false trained as nonjunk.
578 MsgPrefix []byte // Typically holds received headers and/or header separator.
580 // If non-nil, a preview of the message based on text and/or html parts of the
581 // message. Used in the webmail and IMAP PREVIEW extension. If non-nil, it is empty
582 // if no preview could be created, or the message has not textual content or
583 // couldn't be parsed.
584 // Previews are typically created when delivering a message, but not when importing
585 // messages, for speed. Previews are generated on first request (in the webmail, or
586 // through the IMAP fetch attribute "PREVIEW" (without "LAZY")), and stored with
587 // the message at that time.
588 // The preview is at most 256 characters (can be more bytes), with detected quoted
589 // text replaced with "[...]". Previews typically end with a newline, callers may
590 // want to strip whitespace.
593 // ParsedBuf message structure. Currently saved as JSON of message.Part because
594 // bstore wasn't able to store recursive types when this was implemented. Created
595 // when first needed, and saved in the database.
596 // todo: once replaced with non-json storage, remove date fixup in ../message/part.go.
600// MailboxCounts returns the delta to counts this message means for its
602func (m Message) MailboxCounts() (mc MailboxCounts) {
621func (m Message) ChangeAddUID(mb Mailbox) ChangeAddUID {
622 return ChangeAddUID{m.MailboxID, m.UID, m.ModSeq, m.Flags, m.Keywords, mb.MessageCountIMAP(), uint32(mb.MailboxCounts.Unseen)}
625func (m Message) ChangeFlags(orig Flags, mb Mailbox) ChangeFlags {
626 mask := m.Flags.Changed(orig)
627 return ChangeFlags{m.MailboxID, m.UID, m.ModSeq, mask, m.Flags, m.Keywords, mb.UIDValidity, uint32(mb.MailboxCounts.Unseen)}
630func (m Message) ChangeThread() ChangeThread {
631 return ChangeThread{[]int64{m.ID}, m.ThreadMuted, m.ThreadCollapsed}
634// ModSeq represents a modseq as stored in the database. ModSeq 0 in the
635// database is sent to the client as 1, because modseq 0 is special in IMAP.
636// ModSeq coming from the client are of type int64.
639func (ms ModSeq) Client() int64 {
646// ModSeqFromClient converts a modseq from a client to a modseq for internal
647// use, e.g. in a database query.
648// ModSeq 1 is turned into 0 (the Go zero value for ModSeq).
649func ModSeqFromClient(modseq int64) ModSeq {
653 return ModSeq(modseq)
656// Erase clears fields from a Message that are no longer needed after actually
657// removing the message file from the file system, after all references to the
658// message have gone away. Only the fields necessary for synchronisation are kept.
659func (m *Message) erase() {
661 panic("erase called on non-expunged message")
666 MailboxID: m.MailboxID,
667 CreateSeq: m.CreateSeq,
670 ThreadID: m.ThreadID,
674// PrepareThreading sets MessageID, SubjectBase and DSN (used in threading) based
676func (m *Message) PrepareThreading(log mlog.Log, part *message.Part) {
679 if part.Envelope == nil {
682 messageID, raw, err := message.MessageIDCanonical(part.Envelope.MessageID)
684 log.Debugx("parsing message-id, ignoring", err, slog.String("messageid", part.Envelope.MessageID))
686 log.Debug("could not parse message-id as address, continuing with raw value", slog.String("messageid", part.Envelope.MessageID))
688 m.MessageID = messageID
689 m.SubjectBase, _ = message.ThreadSubject(part.Envelope.Subject, false)
692// LoadPart returns a message.Part by reading from m.ParsedBuf.
693func (m Message) LoadPart(r io.ReaderAt) (message.Part, error) {
694 if m.ParsedBuf == nil {
695 return message.Part{}, fmt.Errorf("message not parsed")
698 err := json.Unmarshal(m.ParsedBuf, &p)
700 return p, fmt.Errorf("unmarshal message part")
706// NeedsTraining returns whether message needs a training update, based on
707// TrainedJunk (current training status) and new Junk/Notjunk flags.
708func (m Message) NeedsTraining() bool {
709 needs, _, _, _, _ := m.needsTraining()
713func (m Message) needsTraining() (needs, untrain, untrainJunk, train, trainJunk bool) {
714 untrain = m.TrainedJunk != nil
715 untrainJunk = untrain && *m.TrainedJunk
716 train = m.Junk != m.Notjunk
718 needs = untrain != train || untrain && train && untrainJunk != trainJunk
722// JunkFlagsForMailbox sets Junk and Notjunk flags based on mailbox name if configured. Often
723// used when delivering/moving/copying messages to a mailbox. Mail clients are not
724// very helpful with setting junk/notjunk flags. But clients can move/copy messages
725// to other mailboxes. So we set flags when clients move a message.
726func (m *Message) JunkFlagsForMailbox(mb Mailbox, conf config.Account) {
733 if !conf.AutomaticJunkFlags.Enabled {
737 lmailbox := strings.ToLower(mb.Name)
739 if conf.JunkMailbox != nil && conf.JunkMailbox.MatchString(lmailbox) {
742 } else if conf.NeutralMailbox != nil && conf.NeutralMailbox.MatchString(lmailbox) {
745 } else if conf.NotJunkMailbox != nil && conf.NotJunkMailbox.MatchString(lmailbox) {
748 } else if conf.JunkMailbox == nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox != nil {
751 } else if conf.JunkMailbox != nil && conf.NeutralMailbox == nil && conf.NotJunkMailbox != nil {
754 } else if conf.JunkMailbox != nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox == nil {
760// Recipient represents the recipient of a message. It is tracked to allow
761// first-time incoming replies from users this account has sent messages to. When a
762// mailbox is added to the Sent mailbox the message is parsed and recipients are
763// inserted as recipient. Recipients are never removed other than for removing the
764// message. On move/copy of a message, recipients aren't modified either. For IMAP,
765// this assumes a client simply appends messages to the Sent mailbox (as opposed to
766// copying messages from some place).
767type Recipient struct {
769 MessageID int64 `bstore:"nonzero,ref Message"` // Ref gives it its own index, useful for fast removal as well.
770 Localpart string `bstore:"nonzero"` // Encoded localpart.
771 Domain string `bstore:"nonzero,index Domain+Localpart"` // Unicode string.
772 OrgDomain string `bstore:"nonzero,index"` // Unicode string.
773 Sent time.Time `bstore:"nonzero"`
776// Outgoing is a message submitted for delivery from the queue. Used to enforce
777// maximum outgoing messages.
778type Outgoing struct {
780 Recipient string `bstore:"nonzero,index"` // Canonical international address with utf8 domain.
781 Submitted time.Time `bstore:"nonzero,default now"`
784// RecipientDomainTLS stores TLS capabilities of a recipient domain as encountered
785// during most recent connection (delivery attempt).
786type RecipientDomainTLS struct {
787 Domain string // Unicode.
788 Updated time.Time `bstore:"default now"`
789 STARTTLS bool // Supports STARTTLS.
790 RequireTLS bool // Supports RequireTLS SMTP extension.
793// DiskUsage tracks quota use.
794type DiskUsage struct {
795 ID int64 // Always one record with ID 1.
796 MessageSize int64 // Sum of all messages, for quota accounting.
799// SessionToken and CSRFToken are types to prevent mixing them up.
800// Base64 raw url encoded.
801type SessionToken string
804// LoginSession represents a login session. We keep a limited number of sessions
805// for a user, removing the oldest session when a new one is created.
806type LoginSession struct {
808 Created time.Time `bstore:"nonzero,default now"` // Of original login.
809 Expires time.Time `bstore:"nonzero"` // Extended each time it is used.
810 SessionTokenBinary [16]byte `bstore:"nonzero"` // Stored in cookie, like "webmailsession" or "webaccountsession".
811 CSRFTokenBinary [16]byte // For API requests, in "x-mox-csrf" header.
812 AccountName string `bstore:"nonzero"`
813 LoginAddress string `bstore:"nonzero"`
815 // Set when loading from database.
816 sessionToken SessionToken
820// Quoting is a setting for how to quote in replies/forwards.
824 Default Quoting = "" // Bottom-quote if text is selected, top-quote otherwise.
825 Bottom Quoting = "bottom"
829// Settings are webmail client settings.
830type Settings struct {
831 ID uint8 // Singleton ID 1.
836 // Whether to show the bars underneath the address input fields indicating
837 // starttls/dnssec/dane/mtasts/requiretls support by address.
838 ShowAddressSecurity bool
840 // Show HTML version of message by default, instead of plain text.
843 // If true, don't show shortcuts in webmail after mouse interaction.
846 // Additional headers to display in message view. E.g. Delivered-To, User-Agent, X-Mox-Reason.
850// ViewMode how a message should be viewed: its text parts, html parts, or html
851// with loading external resources.
855 ModeText ViewMode = "text"
856 ModeHTML ViewMode = "html"
857 ModeHTMLExt ViewMode = "htmlext" // HTML with external resources.
860// FromAddressSettings are webmail client settings per "From" address.
861type FromAddressSettings struct {
862 FromAddress string // Unicode.
866// RulesetNoListID records a user "no" response to the question of
867// creating/removing a ruleset after moving a message with list-id header from/to
869type RulesetNoListID struct {
871 RcptToAddress string `bstore:"nonzero"`
872 ListID string `bstore:"nonzero"`
873 ToInbox bool // Otherwise from Inbox to other mailbox.
876// RulesetNoMsgFrom records a user "no" response to the question of
877// creating/moveing a ruleset after moving a mesage with message "from" address
879type RulesetNoMsgFrom struct {
881 RcptToAddress string `bstore:"nonzero"`
882 MsgFromAddress string `bstore:"nonzero"` // Unicode.
883 ToInbox bool // Otherwise from Inbox to other mailbox.
886// RulesetNoMailbox represents a "never from/to this mailbox" response to the
887// question of adding/removing a ruleset after moving a message.
888type RulesetNoMailbox struct {
891 // The mailbox from/to which the move has happened.
892 // Not a references, if mailbox is deleted, an entry becomes ineffective.
893 MailboxID int64 `bstore:"nonzero"`
894 ToMailbox bool // Whether MailboxID is the destination of the move (instead of source).
897// MessageErase represents the need to remove a message file from disk, and clear
898// message fields from the database, but only when the last reference to the
899// message is gone (all IMAP sessions need to have applied the changes indicating
901type MessageErase struct {
902 ID int64 // Same ID as Message.ID.
904 // Whether to subtract the size from the total disk usage. Useful for moving
905 // messages, which involves duplicating the message temporarily, while there are
906 // still references in the old mailbox, but which isn't counted as using twice the
908 SkipUpdateDiskUsage bool
911// Types stored in DB.
923 RecipientDomainTLS{},
927 FromAddressSettings{},
935// Account holds the information about a user, includings mailboxes, messages, imap subscriptions.
937 Name string // Name, according to configuration.
938 Dir string // Directory where account files, including the database, bloom filter, and mail messages, are stored for this account.
939 DBPath string // Path to database with mailboxes, messages, etc.
940 DB *bstore.DB // Open database connection.
942 // Channel that is closed if/when account has/gets "threads" accounting (see
944 threadsCompleted chan struct{}
945 // If threads upgrade completed with error, this is set. Used for warning during
946 // delivery, or aborting when importing.
949 // Message directory of last delivery. Used to check we don't have to make that
950 // directory when delivering.
953 // If set, consistency checks won't fail on message ModSeq/CreateSeq being zero.
954 skipMessageZeroSeqCheck bool
956 // Write lock must be held when modifying account/mailbox/message/flags/annotations
957 // if the change needs to be synchronized with client connections by broadcasting
958 // the changes. Changes that are not protocol-visible do not require a lock, the
959 // database transactions isolate activity, though locking may be necessary to
960 // protect in-memory-only access.
962 // Read lock for reading mailboxes/messages as a consistent snapsnot (i.e. not
963 // concurrent changes). For longer transactions, e.g. when reading many messages,
964 // the lock can be released while continuing to read from the transaction.
966 // When making changes to mailboxes/messages, changes must be broadcasted before
967 // releasing the lock to ensure proper UID ordering.
970 // Reference count, while >0, this account is alive and shared. Protected by
971 // openAccounts, not by account wlock.
973 removed bool // Marked for removal. Last close removes the account directory.
974 closed chan struct{} // Closed when last reference is gone.
979 Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed.
980 MailboxModSeq bool // Whether mailboxes have been assigned modseqs.
981 MailboxParentID bool // Setting ParentID on mailboxes.
982 MailboxCounts bool // Global flag about whether we have mailbox flags. Instead of previous per-mailbox boolean.
983 MessageParseVersion int // If different than latest, all messages will be reparsed.
986const MessageParseVersionLatest = 2
988// upgradeInit is the value for new account database, which don't need any upgrading.
989var upgradeInit = Upgrade{
993 MailboxParentID: true,
995 MessageParseVersion: MessageParseVersionLatest,
998// InitialUIDValidity returns a UIDValidity used for initializing an account.
999// It can be replaced during tests with a predictable value.
1000var InitialUIDValidity = func() uint32 {
1001 return uint32(time.Now().Unix() >> 1) // A 2-second resolution will get us far enough beyond 2038.
1004var openAccounts = struct {
1006 names map[string]*Account
1008 names: map[string]*Account{},
1011func closeAccount(acc *Account) (rerr error) {
1012 // If we need to remove the account files, we do so without the accounts lock.
1016 log := mlog.New("store", nil)
1017 err := removeAccount(log, acc.Name)
1026 defer openAccounts.Unlock()
1031 remove = acc.removed
1034 err := acc.DB.Close()
1036 delete(openAccounts.names, acc.Name)
1046 // Verify there are no more pending MessageErase records.
1047 l, err := bstore.QueryDB[MessageErase](context.TODO(), acc.DB).List()
1049 return fmt.Errorf("listing messageerase records: %v", err)
1050 } else if len(l) > 0 {
1051 return fmt.Errorf("messageerase records still present after last account reference is gone: %v", l)
1057// removeAccount moves the account directory for an account away and removes
1058// all files, and removes the AccountRemove struct from the database.
1059func removeAccount(log mlog.Log, accountName string) error {
1060 log = log.With(slog.String("account", accountName))
1061 log.Info("removing account directory and files")
1063 // First move the account directory away.
1064 odir := filepath.Join(mox.DataDirPath("accounts"), accountName)
1065 tmpdir := filepath.Join(mox.DataDirPath("tmp"), "oldaccount-"+accountName)
1066 if err := os.Rename(odir, tmpdir); err != nil {
1067 return fmt.Errorf("moving account data directory %q out of the way to %q (account not removed): %v", odir, tmpdir, err)
1072 // Commit removal to database.
1073 err := AuthDB.Write(context.Background(), func(tx *bstore.Tx) error {
1074 if err := tx.Delete(&AccountRemove{accountName}); err != nil {
1075 return fmt.Errorf("deleting account removal request: %v", err)
1077 if err := tlsPublicKeyRemoveForAccount(tx, accountName); err != nil {
1078 return fmt.Errorf("removing tls public keys for account: %v", err)
1081 if err := loginAttemptRemoveAccount(tx, accountName); err != nil {
1082 return fmt.Errorf("removing historic login attempts for account: %v", err)
1087 errs = append(errs, fmt.Errorf("remove account from database: %w", err))
1090 // Remove the account directory and its message and other files.
1091 if err := os.RemoveAll(tmpdir); err != nil {
1092 errs = append(errs, fmt.Errorf("removing account data directory %q that was moved to %q: %v", odir, tmpdir, err))
1095 return errors.Join(errs...)
1098// OpenAccount opens an account by name.
1100// No additional data path prefix or ".db" suffix should be added to the name.
1101// A single shared account exists per name.
1102func OpenAccount(log mlog.Log, name string, checkLoginDisabled bool) (*Account, error) {
1104 defer openAccounts.Unlock()
1105 if acc, ok := openAccounts.names[name]; ok {
1107 return nil, fmt.Errorf("account has been removed")
1114 if a, ok := mox.Conf.Account(name); !ok {
1115 return nil, ErrAccountUnknown
1116 } else if checkLoginDisabled && a.LoginDisabled != "" {
1117 return nil, fmt.Errorf("%w: %s", ErrLoginDisabled, a.LoginDisabled)
1120 acc, err := openAccount(log, name)
1124 openAccounts.names[name] = acc
1128// openAccount opens an existing account, or creates it if it is missing.
1129// Called with openAccounts lock held.
1130func openAccount(log mlog.Log, name string) (a *Account, rerr error) {
1131 dir := filepath.Join(mox.DataDirPath("accounts"), name)
1132 return OpenAccountDB(log, dir, name)
1135// OpenAccountDB opens an account database file and returns an initialized account
1136// or error. Only exported for use by subcommands that verify the database file.
1137// Almost all account opens must go through OpenAccount/OpenEmail/OpenEmailAuth.
1138func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, rerr error) {
1139 log = log.With(slog.String("account", accountName))
1141 dbpath := filepath.Join(accountDir, "index.db")
1143 // Create account if it doesn't exist yet.
1145 if _, err := os.Stat(dbpath); err != nil && os.IsNotExist(err) {
1147 os.MkdirAll(accountDir, 0770)
1150 opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: moxvar.RegisterLogger(dbpath, log.Logger)}
1151 db, err := bstore.Open(context.TODO(), dbpath, &opts, DBTypes...)
1159 log.Check(err, "closing database file after error")
1161 err := os.Remove(dbpath)
1162 log.Check(err, "removing new database file after error")
1173 closed: make(chan struct{}),
1174 threadsCompleted: make(chan struct{}),
1178 if err := initAccount(db); err != nil {
1179 return nil, fmt.Errorf("initializing account: %v", err)
1182 close(acc.threadsCompleted)
1186 // Ensure singletons are present, like DiskUsage and Settings.
1187 // Process pending MessageErase records. Check that next the message ID assigned by
1188 // the database does not already have a file on disk, or increase the sequence so
1190 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
1191 if tx.Get(&Settings{ID: 1}) == bstore.ErrAbsent {
1192 if err := tx.Insert(&Settings{ID: 1, ShowAddressSecurity: true}); err != nil {
1197 du := DiskUsage{ID: 1}
1199 if err == bstore.ErrAbsent {
1200 // No DiskUsage record yet, calculate total size and insert.
1201 err := bstore.QueryTx[Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb Mailbox) error {
1202 du.MessageSize += mb.Size
1208 if err := tx.Insert(&du); err != nil {
1211 } else if err != nil {
1215 var erase []MessageErase
1216 if _, err := bstore.QueryTx[MessageErase](tx).Gather(&erase).Delete(); err != nil {
1217 return fmt.Errorf("fetching messages to erase: %w", err)
1220 log.Debug("deleting message files from message erase records", slog.Int("count", len(erase)))
1223 for _, me := range erase {
1224 // Clear the fields from the message not needed for synchronization.
1225 m := Message{ID: me.ID}
1226 if err := tx.Get(&m); err != nil {
1227 return fmt.Errorf("get message %d to expunge: %w", me.ID, err)
1228 } else if !m.Expunged {
1229 return fmt.Errorf("message %d to erase is not expunged", m.ID)
1232 // We remove before we update/commit the database, so we are sure we don't leave
1233 // files behind in case of an error/crash.
1234 p := acc.MessagePath(me.ID)
1236 log.Check(err, "removing message file for expunged message", slog.String("path", p))
1238 if !me.SkipUpdateDiskUsage {
1239 du.MessageSize -= m.Size
1244 if err := tx.Update(&m); err != nil {
1245 return fmt.Errorf("save erase of message %d in database: %w", m.ID, err)
1250 if err := tx.Update(&du); err != nil {
1251 return fmt.Errorf("saving disk usage after erasing messages: %w", err)
1255 // Ensure the message directories don't have a higher message ID than occurs in our
1256 // database. If so, increase the next ID used for inserting a message to prevent
1257 // clash during delivery.
1258 last, err := bstore.QueryTx[Message](tx).SortDesc("ID").Limit(1).Get()
1259 if err != nil && err != bstore.ErrAbsent {
1260 return fmt.Errorf("querying last message: %v", err)
1263 // We look in the directory where the message is stored (the id can be 0, which is fine).
1265 p := acc.MessagePath(maxDBID)
1266 dir := filepath.Dir(p)
1268 // We also try looking for the next directories that would be created for messages,
1269 // until one doesn't exist anymore. We never delete these directories.
1271 np := acc.MessagePath(maxFSID + msgFilesPerDir)
1272 ndir := filepath.Dir(np)
1273 if _, err := os.Stat(ndir); err == nil {
1274 maxFSID = (maxFSID + msgFilesPerDir) &^ (msgFilesPerDir - 1) // First ID for dir.
1276 } else if errors.Is(err, fs.ErrNotExist) {
1279 return fmt.Errorf("stat next message directory %q: %v", ndir, err)
1282 // Find highest numbered file within the directory.
1283 entries, err := os.ReadDir(dir)
1284 if err != nil && !errors.Is(err, fs.ErrNotExist) {
1285 return fmt.Errorf("read message directory %q: %v", dir, err)
1287 dirFirstID := maxFSID &^ (msgFilesPerDir - 1)
1288 for _, e := range entries {
1289 id, err := strconv.ParseInt(e.Name(), 10, 64)
1290 if err == nil && (id < dirFirstID || id >= dirFirstID+msgFilesPerDir) {
1291 err = fmt.Errorf("directory %s has message id %d outside of range [%d - %d), ignoring", dir, id, dirFirstID, dirFirstID+msgFilesPerDir)
1294 p := filepath.Join(dir, e.Name())
1295 log.Errorx("unrecognized file in message directory, parsing filename as number", err, slog.String("path", p))
1297 maxFSID = max(maxFSID, id)
1300 // Warn if we need to increase the message ID in the database.
1302 if maxFSID > maxDBID {
1303 log.Warn("unexpected message file with higher message id than highest id in database, moving database id sequence forward to prevent clashes during future deliveries", slog.Int64("maxdbmsgid", maxDBID), slog.Int64("maxfilemsgid", maxFSID))
1305 mb, err := bstore.QueryTx[Mailbox](tx).Limit(1).Get()
1307 return fmt.Errorf("get a mailbox: %v", err)
1311 for maxFSID > maxDBID {
1312 // Set fields that must be non-zero.
1315 MailboxID: mailboxID,
1317 // Insert and delete to increase the sequence, silly but effective.
1318 if err := tx.Insert(&m); err != nil {
1319 return fmt.Errorf("inserting message to increase id: %v", err)
1321 if err := tx.Delete(&m); err != nil {
1322 return fmt.Errorf("deleting message after increasing id: %v", err)
1330 return nil, fmt.Errorf("calculating counts for mailbox, inserting settings, expunging messages: %v", err)
1333 up := Upgrade{ID: 1}
1334 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
1336 if err == bstore.ErrAbsent {
1337 if err := tx.Insert(&up); err != nil {
1338 return fmt.Errorf("inserting initial upgrade record: %v", err)
1345 return nil, fmt.Errorf("checking message threading: %v", err)
1348 // Ensure all mailboxes have a modseq based on highest modseq message in each
1349 // mailbox, and a createseq.
1350 if !up.MailboxModSeq {
1351 log.Debug("upgrade: adding modseq to each mailbox")
1352 err := acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1355 mbl, err := bstore.QueryTx[Mailbox](tx).FilterEqual("Expunged", false).List()
1357 return fmt.Errorf("listing mailboxes: %v", err)
1359 for _, mb := range mbl {
1360 // Get current highest modseq of message in account.
1361 qms := bstore.QueryTx[Message](tx)
1362 qms.FilterNonzero(Message{MailboxID: mb.ID})
1363 qms.SortDesc("ModSeq")
1367 mb.ModSeq = ModSeq(m.ModSeq.Client())
1368 } else if err == bstore.ErrAbsent {
1370 modseq, err = acc.NextModSeq(tx)
1372 return fmt.Errorf("get next mod seq for mailbox without messages: %v", err)
1377 return fmt.Errorf("looking up highest modseq for mailbox: %v", err)
1380 if err := tx.Update(&mb); err != nil {
1381 return fmt.Errorf("updating mailbox with modseq: %v", err)
1385 up.MailboxModSeq = true
1386 if err := tx.Update(&up); err != nil {
1387 return fmt.Errorf("marking upgrade done: %v", err)
1393 return nil, fmt.Errorf("upgrade: adding modseq to each mailbox: %v", err)
1397 // Add ParentID to mailboxes.
1398 if !up.MailboxParentID {
1399 log.Debug("upgrade: setting parentid on each mailbox")
1401 err := acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1402 mbl, err := bstore.QueryTx[Mailbox](tx).FilterEqual("Expunged", false).SortAsc("Name").List()
1404 return fmt.Errorf("listing mailboxes: %w", err)
1407 names := map[string]Mailbox{}
1408 for _, mb := range mbl {
1414 // Ensure a parent mailbox for name exists, creating it if needed, including any
1415 // grandparents, up to the top.
1416 var ensureParentMailboxID func(name string) (int64, error)
1417 ensureParentMailboxID = func(name string) (int64, error) {
1418 parentName := mox.ParentMailboxName(name)
1419 if parentName == "" {
1422 parent := names[parentName]
1424 return parent.ID, nil
1427 parentParentID, err := ensureParentMailboxID(parentName)
1429 return 0, fmt.Errorf("creating parent mailbox %q: %w", parentName, err)
1433 modseq, err = a.NextModSeq(tx)
1435 return 0, fmt.Errorf("get next modseq: %w", err)
1439 uidvalidity, err := a.NextUIDValidity(tx)
1441 return 0, fmt.Errorf("next uid validity: %w", err)
1447 ParentID: parentParentID,
1449 UIDValidity: uidvalidity,
1451 SpecialUse: SpecialUse{},
1454 if err := tx.Insert(&parent); err != nil {
1455 return 0, fmt.Errorf("creating parent mailbox: %w", err)
1457 return parent.ID, nil
1460 for _, mb := range mbl {
1461 parentID, err := ensureParentMailboxID(mb.Name)
1463 return fmt.Errorf("creating missing parent mailbox for mailbox %q: %w", mb.Name, err)
1465 mb.ParentID = parentID
1466 if err := tx.Update(&mb); err != nil {
1467 return fmt.Errorf("update mailbox with parentid: %w", err)
1471 up.MailboxParentID = true
1472 if err := tx.Update(&up); err != nil {
1473 return fmt.Errorf("marking upgrade done: %w", err)
1478 return nil, fmt.Errorf("upgrade: setting parentid on each mailbox: %w", err)
1482 if !up.MailboxCounts {
1483 log.Debug("upgrade: ensuring all mailboxes have message counts")
1485 err := acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1486 err := bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
1487 mc, err := mb.CalculateCounts(tx)
1491 mb.HaveCounts = true
1492 mb.MailboxCounts = mc
1493 return tx.Update(&mb)
1499 up.MailboxCounts = true
1500 if err := tx.Update(&up); err != nil {
1501 return fmt.Errorf("marking upgrade done: %w", err)
1506 return nil, fmt.Errorf("upgrade: ensuring message counts on all mailboxes")
1510 if up.MessageParseVersion != MessageParseVersionLatest {
1511 log.Debug("upgrade: reparsing message for mime structures for new message parse version", slog.Int("current", up.MessageParseVersion), slog.Int("latest", MessageParseVersionLatest))
1513 // Unless we also need to upgrade threading, we'll be reparsing messages in the
1514 // background so opening of the account is quick.
1515 done := make(chan error, 1)
1516 bg := up.Threads == 2
1518 // Increase account use before holding on to account in background.
1519 // Caller holds the lock. The goroutine below decreases nused by calling
1530 rerr = fmt.Errorf("unhandled panic: %v", x)
1531 log.Error("unhandled panic reparsing messages", slog.Any("err", x))
1533 metrics.PanicInc(metrics.Store)
1536 if bg && rerr != nil {
1537 log.Errorx("upgrade failed: reparsing message for mime structures for new message parse version", rerr, slog.Duration("duration", time.Since(start)))
1541 // Must be done at end of defer. Our parent context/goroutine has openAccounts lock
1542 // held, so we won't make progress until after the enclosing method has returned.
1543 err := closeAccount(acc)
1544 log.Check(err, "closing account after reparsing messages")
1548 total, rerr = acc.ReparseMessages(mox.Shutdown, log)
1550 rerr = fmt.Errorf("reparsing messages and updating mime structures in message index: %w", rerr)
1554 up.MessageParseVersion = MessageParseVersionLatest
1555 rerr = acc.DB.Update(context.TODO(), &up)
1557 rerr = fmt.Errorf("marking latest message parse version: %w", rerr)
1561 log.Info("upgrade completed: reparsing message for mime structures for new message parse version", slog.Int("total", total), slog.Duration("duration", time.Since(start)))
1572 if up.Threads == 2 {
1573 close(acc.threadsCompleted)
1577 // Increase account use before holding on to account in background.
1578 // Caller holds the lock. The goroutine below decreases nused by calling
1582 // Ensure all messages have a MessageID and SubjectBase, which are needed when
1583 // matching threads.
1584 // Then assign messages to threads, in the same way we do during imports.
1585 log.Info("upgrading account for threading, in background")
1588 err := closeAccount(acc)
1589 log.Check(err, "closing use of account after upgrading account storage for threads")
1591 // Mark that upgrade has finished, possibly error is indicated in threadsErr.
1592 close(acc.threadsCompleted)
1596 x := recover() // Should not happen, but don't take program down if it does.
1598 log.Error("upgradeThreads panic", slog.Any("err", x))
1600 metrics.PanicInc(metrics.Upgradethreads)
1601 acc.threadsErr = fmt.Errorf("panic during upgradeThreads: %v", x)
1605 err := upgradeThreads(mox.Shutdown, log, acc, up)
1608 log.Errorx("upgrading account for threading, aborted", err)
1610 log.Info("upgrading account for threading, completed")
1616// ThreadingWait blocks until the one-time account threading upgrade for the
1617// account has completed, and returns an error if not successful.
1619// To be used before starting an import of messages.
1620func (a *Account) ThreadingWait(log mlog.Log) error {
1622 case <-a.threadsCompleted:
1626 log.Debug("waiting for account upgrade to complete")
1628 <-a.threadsCompleted
1632func initAccount(db *bstore.DB) error {
1633 return db.Write(context.TODO(), func(tx *bstore.Tx) error {
1634 uidvalidity := InitialUIDValidity()
1636 if err := tx.Insert(&upgradeInit); err != nil {
1639 if err := tx.Insert(&DiskUsage{ID: 1}); err != nil {
1642 if err := tx.Insert(&Settings{ID: 1}); err != nil {
1646 modseq, err := nextModSeq(tx)
1648 return fmt.Errorf("get next modseq: %v", err)
1651 if len(mox.Conf.Static.DefaultMailboxes) > 0 {
1652 // Deprecated in favor of InitialMailboxes.
1653 defaultMailboxes := mox.Conf.Static.DefaultMailboxes
1654 mailboxes := []string{"Inbox"}
1655 for _, name := range defaultMailboxes {
1656 if strings.EqualFold(name, "Inbox") {
1659 mailboxes = append(mailboxes, name)
1661 for _, name := range mailboxes {
1667 UIDValidity: uidvalidity,
1671 if strings.HasPrefix(name, "Archive") {
1673 } else if strings.HasPrefix(name, "Drafts") {
1675 } else if strings.HasPrefix(name, "Junk") {
1677 } else if strings.HasPrefix(name, "Sent") {
1679 } else if strings.HasPrefix(name, "Trash") {
1682 if err := tx.Insert(&mb); err != nil {
1683 return fmt.Errorf("creating mailbox: %w", err)
1685 if err := tx.Insert(&Subscription{name}); err != nil {
1686 return fmt.Errorf("adding subscription: %w", err)
1690 mailboxes := mox.Conf.Static.InitialMailboxes
1691 var zerouse config.SpecialUseMailboxes
1692 if mailboxes.SpecialUse == zerouse && len(mailboxes.Regular) == 0 {
1693 mailboxes = DefaultInitialMailboxes
1696 add := func(name string, use SpecialUse) error {
1702 UIDValidity: uidvalidity,
1707 if err := tx.Insert(&mb); err != nil {
1708 return fmt.Errorf("creating mailbox: %w", err)
1710 if err := tx.Insert(&Subscription{name}); err != nil {
1711 return fmt.Errorf("adding subscription: %w", err)
1715 addSpecialOpt := func(nameOpt string, use SpecialUse) error {
1719 return add(nameOpt, use)
1725 {"Inbox", SpecialUse{}},
1726 {mailboxes.SpecialUse.Archive, SpecialUse{Archive: true}},
1727 {mailboxes.SpecialUse.Draft, SpecialUse{Draft: true}},
1728 {mailboxes.SpecialUse.Junk, SpecialUse{Junk: true}},
1729 {mailboxes.SpecialUse.Sent, SpecialUse{Sent: true}},
1730 {mailboxes.SpecialUse.Trash, SpecialUse{Trash: true}},
1732 for _, e := range l {
1733 if err := addSpecialOpt(e.nameOpt, e.use); err != nil {
1737 for _, name := range mailboxes.Regular {
1738 if err := add(name, SpecialUse{}); err != nil {
1745 if err := tx.Insert(&NextUIDValidity{1, uidvalidity}); err != nil {
1746 return fmt.Errorf("inserting nextuidvalidity: %w", err)
1752// Remove schedules an account for removal. New opens will fail. When the last
1753// reference is closed, the account files are removed.
1754func (a *Account) Remove(ctx context.Context) error {
1756 defer openAccounts.Unlock()
1758 if err := AuthDB.Insert(ctx, &AccountRemove{AccountName: a.Name}); err != nil {
1759 return fmt.Errorf("inserting account removal: %w", err)
1766// WaitClosed waits until the last reference to this account is gone and the
1767// account is closed. Used during tests, to ensure the consistency checks run after
1768// expunged messages have been erased.
1769func (a *Account) WaitClosed() {
1773// Close reduces the reference count, and closes the database connection when
1774// it was the last user.
1775func (a *Account) Close() error {
1776 if CheckConsistencyOnClose {
1777 xerr := a.CheckConsistency()
1778 err := closeAccount(a)
1784 return closeAccount(a)
1787// SetSkipMessageModSeqZeroCheck skips consistency checks for Message.ModSeq and
1788// Message.CreateSeq being zero.
1789func (a *Account) SetSkipMessageModSeqZeroCheck(skip bool) {
1792 a.skipMessageZeroSeqCheck = true
1795// CheckConsistency checks the consistency of the database and returns a non-nil
1796// error for these cases:
1798// - Missing or unexpected on-disk message files.
1799// - Mismatch between message size and length of MsgPrefix and on-disk file.
1800// - Incorrect mailbox counts.
1801// - Incorrect total message size.
1802// - Message with UID >= mailbox uid next.
1803// - Mailbox uidvalidity >= account uid validity.
1804// - Mailbox ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq, and Modseq >= highest message ModSeq.
1805// - Mailbox must have a live parent ID if they are live themselves, live names must be unique.
1806// - Message ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq.
1807// - All messages have a nonzero ThreadID, and no cycles in ThreadParentID, and parent messages the same ThreadParentIDs tail.
1808// - Annotations must have ModSeq > 0, CreateSeq > 0, ModSeq >= CreateSeq and live keys must be unique per mailbox.
1809// - Recalculate junk filter (words and counts) and check they are the same.
1810func (a *Account) CheckConsistency() error {
1814 var uidErrors []string // With a limit, could be many.
1815 var modseqErrors []string // With limit.
1816 var fileErrors []string // With limit.
1817 var threadidErrors []string // With limit.
1818 var threadParentErrors []string // With limit.
1819 var threadAncestorErrors []string // With limit.
1820 var errmsgs []string
1822 ctx := context.Background()
1823 log := mlog.New("store", nil)
1825 err := a.DB.Read(ctx, func(tx *bstore.Tx) error {
1826 nuv := NextUIDValidity{ID: 1}
1829 return fmt.Errorf("fetching next uid validity: %v", err)
1832 mailboxes := map[int64]Mailbox{} // Also expunged mailboxes.
1833 mailboxNames := map[string]Mailbox{} // Only live names.
1834 err = bstore.QueryTx[Mailbox](tx).ForEach(func(mb Mailbox) error {
1835 mailboxes[mb.ID] = mb
1837 if xmb, ok := mailboxNames[mb.Name]; ok {
1838 errmsg := fmt.Sprintf("mailbox %q exists as id %d and id %d", mb.Name, mb.ID, xmb.ID)
1839 errmsgs = append(errmsgs, errmsg)
1841 mailboxNames[mb.Name] = mb
1844 if mb.UIDValidity >= nuv.Next {
1845 errmsg := fmt.Sprintf("mailbox %q (id %d) has uidvalidity %d >= account next uidvalidity %d", mb.Name, mb.ID, mb.UIDValidity, nuv.Next)
1846 errmsgs = append(errmsgs, errmsg)
1849 if mb.ModSeq == 0 || mb.CreateSeq == 0 || mb.CreateSeq > mb.ModSeq {
1850 errmsg := fmt.Sprintf("mailbox %q (id %d) has invalid modseq %d or createseq %d, both must be > 0 and createseq <= modseq", mb.Name, mb.ID, mb.ModSeq, mb.CreateSeq)
1851 errmsgs = append(errmsgs, errmsg)
1854 m, err := bstore.QueryTx[Message](tx).FilterNonzero(Message{MailboxID: mb.ID}).SortDesc("ModSeq").Limit(1).Get()
1855 if err == bstore.ErrAbsent {
1857 } else if err != nil {
1858 return fmt.Errorf("get message with highest modseq for mailbox: %v", err)
1859 } else if mb.ModSeq < m.ModSeq {
1860 errmsg := fmt.Sprintf("mailbox %q (id %d) has modseq %d < highest message modseq is %d", mb.Name, mb.ID, mb.ModSeq, m.ModSeq)
1861 errmsgs = append(errmsgs, errmsg)
1866 return fmt.Errorf("checking mailboxes: %v", err)
1869 // Check ParentID and name of parent.
1870 for _, mb := range mailboxNames {
1871 if mox.ParentMailboxName(mb.Name) == "" {
1872 if mb.ParentID == 0 {
1875 errmsg := fmt.Sprintf("mailbox %q (id %d) is a root mailbox but has parentid %d", mb.Name, mb.ID, mb.ParentID)
1876 errmsgs = append(errmsgs, errmsg)
1877 } else if mb.ParentID == 0 {
1878 errmsg := fmt.Sprintf("mailbox %q (id %d) is not a root mailbox but has a zero parentid", mb.Name, mb.ID)
1879 errmsgs = append(errmsgs, errmsg)
1880 } else if mox.ParentMailboxName(mb.Name) != mailboxes[mb.ParentID].Name {
1881 errmsg := fmt.Sprintf("mailbox %q (id %d) has parent mailbox id %d with name %q, but parent name should be %q", mb.Name, mb.ID, mb.ParentID, mailboxes[mb.ParentID].Name, mox.ParentMailboxName(mb.Name))
1882 errmsgs = append(errmsgs, errmsg)
1886 type annotation struct {
1887 mailboxID int64 // Can be 0.
1890 annotations := map[annotation]struct{}{}
1891 err = bstore.QueryTx[Annotation](tx).ForEach(func(a Annotation) error {
1893 k := annotation{a.MailboxID, a.Key}
1894 if _, ok := annotations[k]; ok {
1895 errmsg := fmt.Sprintf("duplicate live annotation key %q for mailbox id %d", a.Key, a.MailboxID)
1896 errmsgs = append(errmsgs, errmsg)
1898 annotations[k] = struct{}{}
1900 if a.ModSeq == 0 || a.CreateSeq == 0 || a.CreateSeq > a.ModSeq {
1901 errmsg := fmt.Sprintf("annotation %d in mailbox %q (id %d) has invalid modseq %d or createseq %d, both must be > 0 and modseq >= createseq", a.ID, mailboxes[a.MailboxID].Name, a.MailboxID, a.ModSeq, a.CreateSeq)
1902 errmsgs = append(errmsgs, errmsg)
1903 } else if a.MailboxID > 0 && mailboxes[a.MailboxID].ModSeq < a.ModSeq {
1904 errmsg := fmt.Sprintf("annotation %d in mailbox %q (id %d) has invalid modseq %d > mailbox modseq %d", a.ID, mailboxes[a.MailboxID].Name, a.MailboxID, a.ModSeq, mailboxes[a.MailboxID].ModSeq)
1905 errmsgs = append(errmsgs, errmsg)
1910 return fmt.Errorf("checking mailbox annotations: %v", err)
1913 // All message id's from database. For checking for unexpected files afterwards.
1914 messageIDs := map[int64]struct{}{}
1915 eraseMessageIDs := map[int64]bool{} // Value indicates whether to skip updating disk usage.
1917 // If configured, we'll be building up the junk filter for the messages, to compare
1918 // against the on-disk junk filter.
1921 if conf.JunkFilter != nil {
1922 random := make([]byte, 16)
1923 if _, err := cryptorand.Read(random); err != nil {
1924 return fmt.Errorf("reading random: %v", err)
1926 dbpath := filepath.Join(mox.DataDirPath("tmp"), fmt.Sprintf("junkfilter-check-%x.db", random))
1927 bloompath := filepath.Join(mox.DataDirPath("tmp"), fmt.Sprintf("junkfilter-check-%x.bloom", random))
1928 os.MkdirAll(filepath.Dir(dbpath), 0700)
1930 err := os.Remove(bloompath)
1931 log.Check(err, "removing temp bloom file")
1932 err = os.Remove(dbpath)
1933 log.Check(err, "removing temp junk filter database file")
1935 jf, err = junk.NewFilter(ctx, log, conf.JunkFilter.Params, dbpath, bloompath)
1937 return fmt.Errorf("new junk filter: %v", err)
1941 log.Check(err, "closing junk filter")
1946 // Get IDs of erase messages not yet removed, they'll have a message file.
1947 err = bstore.QueryTx[MessageErase](tx).ForEach(func(me MessageErase) error {
1948 eraseMessageIDs[me.ID] = me.SkipUpdateDiskUsage
1952 return fmt.Errorf("listing message erase records")
1955 counts := map[int64]MailboxCounts{}
1956 var totalExpungedSize int64
1957 err = bstore.QueryTx[Message](tx).ForEach(func(m Message) error {
1958 mc := counts[m.MailboxID]
1959 mc.Add(m.MailboxCounts())
1960 counts[m.MailboxID] = mc
1962 mb := mailboxes[m.MailboxID]
1964 if (!a.skipMessageZeroSeqCheck && (m.ModSeq == 0 || m.CreateSeq == 0) || m.CreateSeq > m.ModSeq) && len(modseqErrors) < 20 {
1965 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)
1966 modseqErrors = append(modseqErrors, modseqerr)
1968 if m.UID >= mb.UIDNext && len(uidErrors) < 20 {
1969 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)
1970 uidErrors = append(uidErrors, uiderr)
1973 if skip := eraseMessageIDs[m.ID]; !skip {
1974 totalExpungedSize += m.Size
1979 messageIDs[m.ID] = struct{}{}
1980 p := a.MessagePath(m.ID)
1981 st, err := os.Stat(p)
1983 existserr := fmt.Sprintf("message %d in mailbox %q (id %d) on-disk file %s: %v", m.ID, mb.Name, mb.ID, p, err)
1984 fileErrors = append(fileErrors, existserr)
1985 } else if len(fileErrors) < 20 && m.Size != int64(len(m.MsgPrefix))+st.Size() {
1986 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())
1987 fileErrors = append(fileErrors, sizeerr)
1990 if m.ThreadID <= 0 && len(threadidErrors) < 20 {
1991 err := fmt.Sprintf("message %d in mailbox %q (id %d) has threadid 0", m.ID, mb.Name, mb.ID)
1992 threadidErrors = append(threadidErrors, err)
1994 if slices.Contains(m.ThreadParentIDs, m.ID) && len(threadParentErrors) < 20 {
1995 err := fmt.Sprintf("message %d in mailbox %q (id %d) references itself in threadparentids", m.ID, mb.Name, mb.ID)
1996 threadParentErrors = append(threadParentErrors, err)
1998 for i, pid := range m.ThreadParentIDs {
1999 am := Message{ID: pid}
2000 if err := tx.Get(&am); err == bstore.ErrAbsent || err == nil && am.Expunged {
2002 } else if err != nil {
2003 return fmt.Errorf("get ancestor message: %v", err)
2004 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) && len(threadAncestorErrors) < 20 {
2005 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)
2006 threadAncestorErrors = append(threadAncestorErrors, err)
2013 if m.Junk != m.Notjunk {
2015 if _, err := a.TrainMessage(ctx, log, jf, m.Notjunk, m); err != nil {
2016 return fmt.Errorf("train message: %v", err)
2018 // We are not setting m.TrainedJunk, we were only recalculating the words.
2025 return fmt.Errorf("reading messages: %v", err)
2028 msgdir := filepath.Join(a.Dir, "msg")
2029 err = filepath.WalkDir(msgdir, func(path string, entry fs.DirEntry, err error) error {
2031 if path == msgdir && errors.Is(err, fs.ErrNotExist) {
2039 id, err := strconv.ParseInt(filepath.Base(path), 10, 64)
2041 return fmt.Errorf("parsing message id from path %q: %v", path, err)
2043 _, mok := messageIDs[id]
2044 _, meok := eraseMessageIDs[id]
2046 return fmt.Errorf("unexpected message file %q", path)
2051 return fmt.Errorf("walking message dir: %v", err)
2054 var totalMailboxSize int64
2055 for _, mb := range mailboxNames {
2056 totalMailboxSize += mb.Size
2057 if mb.MailboxCounts != counts[mb.ID] {
2058 mbcounterr := fmt.Sprintf("mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, counts[mb.ID])
2059 errmsgs = append(errmsgs, mbcounterr)
2063 du := DiskUsage{ID: 1}
2064 if err := tx.Get(&du); err != nil {
2065 return fmt.Errorf("get diskusage")
2067 if du.MessageSize != totalMailboxSize+totalExpungedSize {
2068 errmsg := fmt.Sprintf("total disk usage message size in database is %d != sum of mailbox message sizes %d + sum unerased expunged message sizes %d", du.MessageSize, totalMailboxSize, totalExpungedSize)
2069 errmsgs = append(errmsgs, errmsg)
2072 // Compare on-disk junk filter with our recalculated filter.
2074 load := func(f *junk.Filter) (map[junk.Wordscore]struct{}, error) {
2075 words := map[junk.Wordscore]struct{}{}
2076 err := bstore.QueryDB[junk.Wordscore](ctx, f.DB()).ForEach(func(w junk.Wordscore) error {
2077 if w.Ham != 0 || w.Spam != 0 {
2078 words[w] = struct{}{}
2083 return nil, fmt.Errorf("read junk filter wordscores: %v", err)
2087 if err := jf.Save(); err != nil {
2088 return fmt.Errorf("save recalculated junk filter: %v", err)
2090 wordsExp, err := load(jf)
2092 return fmt.Errorf("read recalculated junk filter: %v", err)
2095 ajf, _, err := a.OpenJunkFilter(ctx, log)
2097 return fmt.Errorf("open account junk filter: %v", err)
2101 log.Check(err, "closing junk filter")
2103 wordsGot, err := load(ajf)
2105 return fmt.Errorf("read account junk filter: %v", err)
2108 if !reflect.DeepEqual(wordsGot, wordsExp) {
2109 errmsg := fmt.Sprintf("unexpected values in junk filter, trained %d of %d\ngot:\n%v\nexpected:\n%v", ntrained, len(messageIDs), wordsGot, wordsExp)
2110 errmsgs = append(errmsgs, errmsg)
2119 errmsgs = append(errmsgs, uidErrors...)
2120 errmsgs = append(errmsgs, modseqErrors...)
2121 errmsgs = append(errmsgs, fileErrors...)
2122 errmsgs = append(errmsgs, threadidErrors...)
2123 errmsgs = append(errmsgs, threadParentErrors...)
2124 errmsgs = append(errmsgs, threadAncestorErrors...)
2125 if len(errmsgs) > 0 {
2126 return fmt.Errorf("%s", strings.Join(errmsgs, "; "))
2131// Conf returns the configuration for this account if it still exists. During
2132// an SMTP session, a configuration update may drop an account.
2133func (a *Account) Conf() (config.Account, bool) {
2134 return mox.Conf.Account(a.Name)
2137// NextUIDValidity returns the next new/unique uidvalidity to use for this account.
2138func (a *Account) NextUIDValidity(tx *bstore.Tx) (uint32, error) {
2139 nuv := NextUIDValidity{ID: 1}
2140 if err := tx.Get(&nuv); err != nil {
2145 if err := tx.Update(&nuv); err != nil {
2151// NextModSeq returns the next modification sequence, which is global per account,
2153func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) {
2154 return nextModSeq(tx)
2157func nextModSeq(tx *bstore.Tx) (ModSeq, error) {
2158 v := SyncState{ID: 1}
2159 if err := tx.Get(&v); err == bstore.ErrAbsent {
2160 // We start assigning from modseq 2. Modseq 0 is not usable, so returned as 1, so
2162 // HighestDeletedModSeq is -1 so comparison against the default ModSeq zero value
2164 v = SyncState{1, 2, -1}
2165 return v.LastModSeq, tx.Insert(&v)
2166 } else if err != nil {
2170 return v.LastModSeq, tx.Update(&v)
2173func (a *Account) HighestDeletedModSeq(tx *bstore.Tx) (ModSeq, error) {
2174 v := SyncState{ID: 1}
2176 if err == bstore.ErrAbsent {
2179 return v.HighestDeletedModSeq, err
2182// WithWLock runs fn with account writelock held. Necessary for account/mailbox
2183// modification. For message delivery, a read lock is required.
2184func (a *Account) WithWLock(fn func()) {
2190// WithRLock runs fn with account read lock held. Needed for message delivery.
2191func (a *Account) WithRLock(fn func()) {
2197// AddOpts influence which work MessageAdd does. Some callers can batch
2198// checks/operations efficiently. For convenience and safety, a zero AddOpts does
2199// all the checks and work.
2200type AddOpts struct {
2203 // If set, the message size is not added to the disk usage. Caller must do that,
2204 // e.g. for many messages at once. If used together with SkipCheckQuota, the
2205 // DiskUsage is not read for database when adding a message.
2206 SkipUpdateDiskUsage bool
2208 // Do not fsync the delivered message file. Useful when copying message files from
2209 // another mailbox. The hardlink created during delivery only needs a directory
2211 SkipSourceFileSync bool
2213 // The directory in which the message file is delivered, typically with a hard
2214 // link, is not fsynced. Useful when delivering many files. A single or few
2215 // directory fsyncs are more efficient.
2218 // Do not assign thread information to a message. Useful when importing many
2219 // messages and assigning threads efficiently after importing messages.
2222 // If JunkFilter is set, it is used for training. If not set, and the filter must
2223 // be trained for a message, the junk filter is opened, modified and saved to disk.
2224 JunkFilter *junk.Filter
2228 // If true, a preview will be generated if the Message doesn't already have one.
2232// todo optimization: when moving files, we open the original, call MessageAdd() which hardlinks it and close the file gain. when passing the filename, we could just use os.Link, saves 2 syscalls.
2234// MessageAdd delivers a mail message to the account.
2236// The file is hardlinked or copied, the caller must clean up the original file. If
2237// this call succeeds, but the database transaction with the change can't be
2238// committed, the caller must clean up the delivered message file identified by
2241// If the message does not fit in the quota, an error with ErrOverQuota is returned
2242// and the mailbox and message are unchanged and the transaction can continue. For
2243// other errors, the caller must abort the transaction.
2245// The message, with msg.MsgPrefix and msgFile combined, must have a header
2246// section. The caller is responsible for adding a header separator to
2247// msg.MsgPrefix if missing from an incoming message.
2249// If UID is not set, it is assigned automatically.
2251// If the message ModSeq is zero, it is assigned automatically. If the message
2252// CreateSeq is zero, it is set to ModSeq. The mailbox ModSeq is set to the message
2255// If the message does not fit in the quota, an error with ErrOverQuota is returned
2256// and the mailbox and message are unchanged and the transaction can continue. For
2257// other errors, the caller must abort the transaction.
2259// If the destination mailbox has the Sent special-use flag, the message is parsed
2260// for its recipients (to/cc/bcc). Their domains are added to Recipients for use in
2261// reputation classification.
2263// Must be called with account write lock held.
2265// Caller must save the mailbox after MessageAdd returns, and broadcast changes for
2266// new the message, updated mailbox counts and possibly new mailbox keywords.
2267func (a *Account) MessageAdd(log mlog.Log, tx *bstore.Tx, mb *Mailbox, m *Message, msgFile *os.File, opts AddOpts) (rerr error) {
2269 return fmt.Errorf("cannot deliver expunged message")
2272 if !opts.SkipUpdateDiskUsage || !opts.SkipCheckQuota {
2273 du := DiskUsage{ID: 1}
2274 if err := tx.Get(&du); err != nil {
2275 return fmt.Errorf("get disk usage: %v", err)
2278 if !opts.SkipCheckQuota {
2279 maxSize := a.QuotaMessageSize()
2280 if maxSize > 0 && m.Size > maxSize-du.MessageSize {
2281 return fmt.Errorf("%w: max size %d bytes", ErrOverQuota, maxSize)
2285 if !opts.SkipUpdateDiskUsage {
2286 du.MessageSize += m.Size
2287 if err := tx.Update(&du); err != nil {
2288 return fmt.Errorf("update disk usage: %v", err)
2294 if m.MailboxOrigID == 0 {
2295 m.MailboxOrigID = mb.ID
2299 if err := mb.UIDNextAdd(1); err != nil {
2300 return fmt.Errorf("adding uid: %v", err)
2304 modseq, err := a.NextModSeq(tx)
2306 return fmt.Errorf("assigning next modseq: %w", err)
2309 } else if m.ModSeq < mb.ModSeq {
2310 return fmt.Errorf("cannot deliver message with modseq %d < mailbox modseq %d", m.ModSeq, mb.ModSeq)
2312 if m.CreateSeq == 0 {
2313 m.CreateSeq = m.ModSeq
2315 mb.ModSeq = m.ModSeq
2317 if m.SaveDate == nil {
2321 if m.Received.IsZero() {
2322 m.Received = time.Now()
2325 if len(m.Keywords) > 0 {
2326 mb.Keywords, _ = MergeKeywords(mb.Keywords, m.Keywords)
2330 m.JunkFlagsForMailbox(*mb, conf)
2332 var part *message.Part
2333 if m.ParsedBuf == nil {
2334 mr := FileMsgReader(m.MsgPrefix, msgFile) // We don't close, it would close the msgFile.
2335 p, err := message.EnsurePart(log.Logger, false, mr, m.Size)
2337 log.Infox("parsing delivered message", err, slog.String("parse", ""), slog.Int64("message", m.ID))
2338 // We continue, p is still valid.
2341 buf, err := json.Marshal(part)
2343 return fmt.Errorf("marshal parsed message: %w", err)
2349 getPart := func() *message.Part {
2358 if err := json.Unmarshal(m.ParsedBuf, &p); err != nil {
2359 log.Errorx("unmarshal parsed message, continuing", err, slog.String("parse", ""))
2361 mr := FileMsgReader(m.MsgPrefix, msgFile)
2368 // If we are delivering to the originally intended mailbox, no need to store the mailbox ID again.
2369 if m.MailboxDestinedID != 0 && m.MailboxDestinedID == m.MailboxOrigID {
2370 m.MailboxDestinedID = 0
2373 if m.MessageID == "" && m.SubjectBase == "" && getPart() != nil {
2374 m.PrepareThreading(log, part)
2377 if !opts.SkipPreview && m.Preview == nil {
2378 if p := getPart(); p != nil {
2379 s, err := p.Preview(log)
2381 return fmt.Errorf("generating preview: %v", err)
2387 // Assign to thread (if upgrade has completed).
2388 noThreadID := opts.SkipThreads
2389 if m.ThreadID == 0 && !opts.SkipThreads && getPart() != nil {
2391 case <-a.threadsCompleted:
2392 if a.threadsErr != nil {
2393 log.Info("not assigning threads for new delivery, upgrading to threads failed")
2396 if err := assignThread(log, tx, m, part); err != nil {
2397 return fmt.Errorf("assigning thread: %w", err)
2401 // note: since we have a write transaction to get here, we can't wait for the
2402 // thread upgrade to finish.
2403 // If we don't assign a threadid the upgrade process will do it.
2404 log.Info("not assigning threads for new delivery, upgrading to threads in progress which will assign this message")
2409 if err := tx.Insert(m); err != nil {
2410 return fmt.Errorf("inserting message: %w", err)
2412 if !noThreadID && m.ThreadID == 0 {
2414 if err := tx.Update(m); err != nil {
2415 return fmt.Errorf("updating message for its own thread id: %w", err)
2419 // todo: perhaps we should match the recipients based on smtp submission and a matching message-id? we now miss the addresses in bcc's if the mail client doesn't save a message that includes the bcc header in the sent mailbox.
2420 if mb.Sent && getPart() != nil && part.Envelope != nil {
2429 addrs := append(append(e.To, e.CC...), e.BCC...)
2430 for _, addr := range addrs {
2431 if addr.User == "" {
2432 // Would trigger error because Recipient.Localpart must be nonzero. todo: we could allow empty localpart in db, and filter by not using FilterNonzero.
2433 log.Info("to/cc/bcc address with empty localpart, not inserting as recipient", slog.Any("address", addr))
2436 d, err := dns.ParseDomain(addr.Host)
2438 log.Debugx("parsing domain in to/cc/bcc address", err, slog.Any("address", addr))
2441 lp, err := smtp.ParseLocalpart(addr.User)
2443 log.Debugx("parsing localpart in to/cc/bcc address", err, slog.Any("address", addr))
2448 Localpart: lp.String(),
2450 OrgDomain: publicsuffix.Lookup(context.TODO(), log.Logger, d).Name(),
2453 if err := tx.Insert(&mr); err != nil {
2454 return fmt.Errorf("inserting sent message recipients: %w", err)
2459 msgPath := a.MessagePath(m.ID)
2460 msgDir := filepath.Dir(msgPath)
2461 if a.lastMsgDir != msgDir {
2462 os.MkdirAll(msgDir, 0770)
2463 if err := moxio.SyncDir(log, msgDir); err != nil {
2464 return fmt.Errorf("sync message dir: %w", err)
2466 a.lastMsgDir = msgDir
2469 // Sync file data to disk.
2470 if !opts.SkipSourceFileSync {
2471 if err := msgFile.Sync(); err != nil {
2472 return fmt.Errorf("fsync message file: %w", err)
2476 if err := moxio.LinkOrCopy(log, msgPath, msgFile.Name(), &moxio.AtReader{R: msgFile}, true); err != nil {
2477 return fmt.Errorf("linking/copying message to new file: %w", err)
2482 err := os.Remove(msgPath)
2483 log.Check(err, "removing delivered message file", slog.String("path", msgPath))
2487 if !opts.SkipDirSync {
2488 if err := moxio.SyncDir(log, msgDir); err != nil {
2489 return fmt.Errorf("sync directory: %w", err)
2493 if !opts.SkipTraining && m.NeedsTraining() && a.HasJunkFilter() {
2494 jf, opened, err := a.ensureJunkFilter(context.TODO(), log, opts.JunkFilter)
2496 return fmt.Errorf("open junk filter: %w", err)
2499 if jf != nil && opened {
2500 err := jf.CloseDiscard()
2501 log.Check(err, "closing junk filter without saving")
2505 // todo optimize: should let us do the tx.Update of m if needed. we should at least merge it with the common case of setting a thread id. and we should try to merge that with the insert by expliciting getting the next id from bstore.
2507 if err := a.RetrainMessage(context.TODO(), log, tx, jf, m); err != nil {
2508 return fmt.Errorf("training junkfilter: %w", err)
2515 return fmt.Errorf("close junk filter: %w", err)
2520 mb.MailboxCounts.Add(m.MailboxCounts())
2525// SetPassword saves a new password for this account. This password is used for
2526// IMAP, SMTP (submission) sessions and the HTTP account web page.
2528// Callers are responsible for checking if the account has NoCustomPassword set.
2529func (a *Account) SetPassword(log mlog.Log, password string) error {
2530 password, err := precis.OpaqueString.String(password)
2532 return fmt.Errorf(`password not allowed by "precis"`)
2535 if len(password) < 8 {
2536 // We actually check for bytes...
2537 return fmt.Errorf("password must be at least 8 characters long")
2540 hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
2542 return fmt.Errorf("generating password hash: %w", err)
2545 err = a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2546 if _, err := bstore.QueryTx[Password](tx).Delete(); err != nil {
2547 return fmt.Errorf("deleting existing password: %v", err)
2550 pw.Hash = string(hash)
2552 // CRAM-MD5 calculates an HMAC-MD5, with the password as key, over a per-attempt
2553 // unique text that includes a timestamp. HMAC performs two hashes. Both times, the
2554 // first block is based on the key/password. We hash those first blocks now, and
2555 // store the hash state in the database. When we actually authenticate, we'll
2556 // complete the HMAC by hashing only the text. We cannot store crypto/hmac's hash,
2557 // because it does not expose its internal state and isn't a BinaryMarshaler.
2559 pw.CRAMMD5.Ipad = md5.New()
2560 pw.CRAMMD5.Opad = md5.New()
2561 key := []byte(password)
2566 ipad := make([]byte, md5.BlockSize)
2567 opad := make([]byte, md5.BlockSize)
2570 for i := range ipad {
2574 pw.CRAMMD5.Ipad.Write(ipad)
2575 pw.CRAMMD5.Opad.Write(opad)
2577 pw.SCRAMSHA1.Salt = scram.MakeRandom()
2578 pw.SCRAMSHA1.Iterations = 2 * 4096
2579 pw.SCRAMSHA1.SaltedPassword = scram.SaltPassword(sha1.New, password, pw.SCRAMSHA1.Salt, pw.SCRAMSHA1.Iterations)
2581 pw.SCRAMSHA256.Salt = scram.MakeRandom()
2582 pw.SCRAMSHA256.Iterations = 4096
2583 pw.SCRAMSHA256.SaltedPassword = scram.SaltPassword(sha256.New, password, pw.SCRAMSHA256.Salt, pw.SCRAMSHA256.Iterations)
2585 if err := tx.Insert(&pw); err != nil {
2586 return fmt.Errorf("inserting new password: %v", err)
2589 return sessionRemoveAll(context.TODO(), log, tx, a.Name)
2592 log.Info("new password set for account", slog.String("account", a.Name))
2597// SessionsClear invalidates all (web) login sessions for the account.
2598func (a *Account) SessionsClear(ctx context.Context, log mlog.Log) error {
2599 return a.DB.Write(ctx, func(tx *bstore.Tx) error {
2600 return sessionRemoveAll(ctx, log, tx, a.Name)
2604// Subjectpass returns the signing key for use with subjectpass for the given
2605// email address with canonical localpart.
2606func (a *Account) Subjectpass(email string) (key string, err error) {
2607 return key, a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2608 v := Subjectpass{Email: email}
2614 if !errors.Is(err, bstore.ErrAbsent) {
2615 return fmt.Errorf("get subjectpass key from accounts database: %w", err)
2618 const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
2619 buf := make([]byte, 16)
2620 if _, err := cryptorand.Read(buf); err != nil {
2623 for _, b := range buf {
2624 key += string(chars[int(b)%len(chars)])
2627 return tx.Insert(&v)
2631// Ensure mailbox is present in database, adding records for the mailbox and its
2632// parents if they aren't present.
2634// If subscribe is true, any mailboxes that were created will also be subscribed to.
2636// The leaf mailbox is created with special-use flags, taking the flags away from
2637// other mailboxes, and reflecting that in the returned changes.
2639// Modseq is used, and initialized if 0, for created mailboxes.
2641// Name must be in normalized form, see CheckMailboxName.
2643// Caller must hold account wlock.
2644// Caller must propagate changes if any.
2645func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, specialUse SpecialUse, modseq *ModSeq) (mb Mailbox, changes []Change, rerr error) {
2646 if norm.NFC.String(name) != name {
2647 return Mailbox{}, nil, fmt.Errorf("mailbox name not normalized")
2650 // Quick sanity check.
2651 if strings.EqualFold(name, "inbox") && name != "Inbox" {
2652 return Mailbox{}, nil, fmt.Errorf("bad casing for inbox")
2655 // Get mailboxes with same name or prefix (parents).
2656 elems := strings.Split(name, "/")
2657 q := bstore.QueryTx[Mailbox](tx)
2658 q.FilterEqual("Expunged", false)
2659 q.FilterFn(func(xmb Mailbox) bool {
2660 t := strings.Split(xmb.Name, "/")
2661 return len(t) <= len(elems) && slices.Equal(t, elems[:len(t)])
2665 return Mailbox{}, nil, fmt.Errorf("list mailboxes: %v", err)
2668 mailboxes := map[string]Mailbox{}
2669 for _, xmb := range l {
2670 mailboxes[xmb.Name] = xmb
2676 for _, elem := range elems {
2681 mb, exists = mailboxes[p]
2686 uidval, err := a.NextUIDValidity(tx)
2688 return Mailbox{}, nil, fmt.Errorf("next uid validity: %v", err)
2691 *modseq, err = a.NextModSeq(tx)
2693 return Mailbox{}, nil, fmt.Errorf("next modseq: %v", err)
2701 UIDValidity: uidval,
2705 err = tx.Insert(&mb)
2707 return Mailbox{}, nil, fmt.Errorf("creating new mailbox %q: %v", p, err)
2713 if tx.Get(&Subscription{p}) != nil {
2714 err := tx.Insert(&Subscription{p})
2716 return Mailbox{}, nil, fmt.Errorf("subscribing to mailbox %q: %v", p, err)
2719 flags = []string{`\Subscribed`}
2720 } else if err := tx.Get(&Subscription{p}); err == nil {
2721 flags = []string{`\Subscribed`}
2722 } else if err != bstore.ErrAbsent {
2723 return Mailbox{}, nil, fmt.Errorf("looking up subscription for %q: %v", p, err)
2726 changes = append(changes, ChangeAddMailbox{mb, flags})
2729 // Clear any special-use flags from existing mailboxes and assign them to this mailbox.
2730 var zeroSpecialUse SpecialUse
2731 if !exists && specialUse != zeroSpecialUse {
2733 clearSpecialUse := func(b bool, fn func(*Mailbox) *bool) {
2734 if !b || qerr != nil {
2737 qs := bstore.QueryTx[Mailbox](tx)
2738 qs.FilterFn(func(xmb Mailbox) bool {
2741 xmb, err := qs.Get()
2742 if err == bstore.ErrAbsent {
2744 } else if err != nil {
2745 qerr = fmt.Errorf("looking up mailbox with special-use flag: %v", err)
2750 xmb.ModSeq = *modseq
2751 if err := tx.Update(&xmb); err != nil {
2752 qerr = fmt.Errorf("clearing special-use flag: %v", err)
2754 changes = append(changes, xmb.ChangeSpecialUse())
2757 clearSpecialUse(specialUse.Archive, func(xmb *Mailbox) *bool { return &xmb.Archive })
2758 clearSpecialUse(specialUse.Draft, func(xmb *Mailbox) *bool { return &xmb.Draft })
2759 clearSpecialUse(specialUse.Junk, func(xmb *Mailbox) *bool { return &xmb.Junk })
2760 clearSpecialUse(specialUse.Sent, func(xmb *Mailbox) *bool { return &xmb.Sent })
2761 clearSpecialUse(specialUse.Trash, func(xmb *Mailbox) *bool { return &xmb.Trash })
2763 return Mailbox{}, nil, qerr
2766 mb.SpecialUse = specialUse
2768 if err := tx.Update(&mb); err != nil {
2769 return Mailbox{}, nil, fmt.Errorf("setting special-use flag for new mailbox: %v", err)
2771 changes = append(changes, mb.ChangeSpecialUse())
2773 return mb, changes, nil
2776// MailboxExists checks if mailbox exists.
2777// Caller must hold account rlock.
2778func (a *Account) MailboxExists(tx *bstore.Tx, name string) (bool, error) {
2779 q := bstore.QueryTx[Mailbox](tx)
2780 q.FilterEqual("Expunged", false)
2781 q.FilterEqual("Name", name)
2785// MailboxFind finds a mailbox by name, returning a nil mailbox and nil error if mailbox does not exist.
2786func (a *Account) MailboxFind(tx *bstore.Tx, name string) (*Mailbox, error) {
2787 q := bstore.QueryTx[Mailbox](tx)
2788 q.FilterEqual("Expunged", false)
2789 q.FilterEqual("Name", name)
2791 if err == bstore.ErrAbsent {
2795 return nil, fmt.Errorf("looking up mailbox: %w", err)
2800// SubscriptionEnsure ensures a subscription for name exists. The mailbox does not
2801// have to exist. Any parents are not automatically subscribed.
2802// Changes are returned and must be broadcasted by the caller.
2803func (a *Account) SubscriptionEnsure(tx *bstore.Tx, name string) ([]Change, error) {
2804 if err := tx.Get(&Subscription{name}); err == nil {
2808 if err := tx.Insert(&Subscription{name}); err != nil {
2809 return nil, fmt.Errorf("inserting subscription: %w", err)
2812 q := bstore.QueryTx[Mailbox](tx)
2813 q.FilterEqual("Expunged", false)
2814 q.FilterEqual("Name", name)
2817 return []Change{ChangeAddSubscription{name, nil}}, nil
2818 } else if err != bstore.ErrAbsent {
2819 return nil, fmt.Errorf("looking up mailbox for subscription: %w", err)
2821 return []Change{ChangeAddSubscription{name, []string{`\NonExistent`}}}, nil
2824// MessageRuleset returns the first ruleset (if any) that matches the message
2825// represented by msgPrefix and msgFile, with smtp and validation fields from m.
2826func MessageRuleset(log mlog.Log, dest config.Destination, m *Message, msgPrefix []byte, msgFile *os.File) *config.Ruleset {
2827 if len(dest.Rulesets) == 0 {
2831 mr := FileMsgReader(msgPrefix, msgFile) // We don't close, it would close the msgFile.
2832 p, err := message.Parse(log.Logger, false, mr)
2834 log.Errorx("parsing message for evaluating rulesets, continuing with headers", err, slog.String("parse", ""))
2835 // note: part is still set.
2837 // todo optimize: only parse header if needed for rulesets. and probably reuse an earlier parsing.
2838 header, err := p.Header()
2840 log.Errorx("parsing message headers for evaluating rulesets, delivering to default mailbox", err, slog.String("parse", ""))
2841 // todo: reject message?
2846 for _, rs := range dest.Rulesets {
2847 if rs.SMTPMailFromRegexpCompiled != nil {
2848 if !rs.SMTPMailFromRegexpCompiled.MatchString(m.MailFrom) {
2852 if rs.MsgFromRegexpCompiled != nil {
2853 if m.MsgFromLocalpart == "" && m.MsgFromDomain == "" || !rs.MsgFromRegexpCompiled.MatchString(m.MsgFromLocalpart.String()+"@"+m.MsgFromDomain) {
2858 if !rs.VerifiedDNSDomain.IsZero() {
2859 d := rs.VerifiedDNSDomain.Name()
2861 matchDomain := func(s string) bool {
2862 return s == d || strings.HasSuffix(s, suffix)
2865 if m.EHLOValidated && matchDomain(m.EHLODomain) {
2868 if m.MailFromValidated && matchDomain(m.MailFromDomain) {
2871 for _, d := range m.DKIMDomains {
2883 for _, t := range rs.HeadersRegexpCompiled {
2884 for k, vl := range header {
2885 k = strings.ToLower(k)
2886 if !t[0].MatchString(k) {
2889 for _, v := range vl {
2890 v = strings.ToLower(strings.TrimSpace(v))
2891 if t[1].MatchString(v) {
2903// MessagePath returns the file system path of a message.
2904func (a *Account) MessagePath(messageID int64) string {
2905 return strings.Join(append([]string{a.Dir, "msg"}, messagePathElems(messageID)...), string(filepath.Separator))
2908// MessageReader opens a message for reading, transparently combining the
2909// message prefix with the original incoming message.
2910func (a *Account) MessageReader(m Message) *MsgReader {
2911 return &MsgReader{prefix: m.MsgPrefix, path: a.MessagePath(m.ID), size: m.Size}
2914// DeliverDestination delivers an email to dest, based on the configured rulesets.
2916// Returns ErrOverQuota when account would be over quota after adding message.
2918// Caller must hold account wlock (mailbox may be created).
2919// Message delivery, possible mailbox creation, and updated mailbox counts are
2921func (a *Account) DeliverDestination(log mlog.Log, dest config.Destination, m *Message, msgFile *os.File) error {
2923 rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
2925 mailbox = rs.Mailbox
2926 } else if dest.Mailbox == "" {
2929 mailbox = dest.Mailbox
2931 return a.DeliverMailbox(log, mailbox, m, msgFile)
2934// DeliverMailbox delivers an email to the specified mailbox.
2936// Returns ErrOverQuota when account would be over quota after adding message.
2938// Caller must hold account wlock (mailbox may be created).
2939// Message delivery, possible mailbox creation, and updated mailbox counts are
2941func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFile *os.File) (rerr error) {
2942 var changes []Change
2946 if !commit && m.ID != 0 {
2947 p := a.MessagePath(m.ID)
2949 log.Check(err, "remove delivered message file", slog.String("path", p))
2954 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2955 mb, chl, err := a.MailboxEnsure(tx, mailbox, true, SpecialUse{}, &m.ModSeq)
2957 return fmt.Errorf("ensuring mailbox: %w", err)
2959 if m.CreateSeq == 0 {
2960 m.CreateSeq = m.ModSeq
2963 nmbkeywords := len(mb.Keywords)
2965 if err := a.MessageAdd(log, tx, &mb, m, msgFile, AddOpts{}); err != nil {
2969 if err := tx.Update(&mb); err != nil {
2970 return fmt.Errorf("updating mailbox for delivery: %w", err)
2973 changes = append(changes, chl...)
2974 changes = append(changes, m.ChangeAddUID(mb), mb.ChangeCounts())
2975 if nmbkeywords != len(mb.Keywords) {
2976 changes = append(changes, mb.ChangeKeywords())
2984 BroadcastChanges(a, changes)
2988type RemoveOpts struct {
2989 JunkFilter *junk.Filter // If set, this filter is used for training, instead of opening and saving the junk filter.
2992// MessageRemove markes messages as expunged, updates mailbox counts for the
2993// messages, sets a new modseq on the messages and mailbox, untrains the junk
2994// filter and queues the messages for erasing when the last reference has gone.
2996// Caller must save the modified mailbox to the database.
2998// The disk usage is not immediately updated. That will happen when the message
2999// is actually removed from disk.
3001// The junk filter is untrained for the messages if it was trained.
3002// Useful as optimization when messages are moved and the junk/nonjunk flags do not
3003// change (which can happen due to automatic junk/nonjunk flags for mailboxes).
3005// An empty list of messages results in an error.
3007// Caller must broadcast changes.
3009// Must be called with wlock held.
3010func (a *Account) MessageRemove(log mlog.Log, tx *bstore.Tx, modseq ModSeq, mb *Mailbox, opts RemoveOpts, l ...Message) (chremuids ChangeRemoveUIDs, chmbc ChangeMailboxCounts, rerr error) {
3012 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("must expunge at least one message")
3017 // Remove any message recipients.
3018 anyIDs := make([]any, len(l))
3019 for i, m := range l {
3022 qmr := bstore.QueryTx[Recipient](tx)
3023 qmr.FilterEqual("MessageID", anyIDs...)
3024 if _, err := qmr.Delete(); err != nil {
3025 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("deleting message recipients for messages: %w", err)
3029 jf := opts.JunkFilter
3031 // Mark messages expunged.
3032 ids := make([]int64, 0, len(l))
3033 uids := make([]UID, 0, len(l))
3034 for _, m := range l {
3035 ids = append(ids, m.ID)
3036 uids = append(uids, m.UID)
3039 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("message %d is already expunged", m.ID)
3042 mb.Sub(m.MailboxCounts())
3049 if err := tx.Update(&m); err != nil {
3050 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("marking message %d expunged: %v", m.ID, err)
3053 // Ensure message gets erased in future.
3054 if err := tx.Insert(&MessageErase{m.ID, false}); err != nil {
3055 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("inserting message erase %d : %v", m.ID, err)
3058 if m.TrainedJunk == nil || !a.HasJunkFilter() {
3061 // Untrain, as needed by updated flags Junk/Notjunk to false.
3064 jf, _, err = a.OpenJunkFilter(context.TODO(), log)
3066 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("open junk filter: %v", err)
3073 log.Check(err, "closing junk filter")
3077 if err := a.RetrainMessage(context.TODO(), log, tx, jf, &m); err != nil {
3078 return ChangeRemoveUIDs{}, ChangeMailboxCounts{}, fmt.Errorf("retraining expunged messages: %w", err)
3082 return ChangeRemoveUIDs{mb.ID, uids, modseq, ids, mb.UIDNext, mb.MessageCountIMAP(), uint32(mb.MailboxCounts.Unseen)}, mb.ChangeCounts(), nil
3085// TidyRejectsMailbox removes old reject emails, and returns whether there is space for a new delivery.
3087// The changed mailbox is saved to the database.
3089// Caller most hold account wlock.
3090// Caller must broadcast changes.
3091func (a *Account) TidyRejectsMailbox(log mlog.Log, tx *bstore.Tx, mbRej *Mailbox) (changes []Change, hasSpace bool, rerr error) {
3092 // Gather old messages to expunge.
3093 old := time.Now().Add(-14 * 24 * time.Hour)
3094 qdel := bstore.QueryTx[Message](tx)
3095 qdel.FilterNonzero(Message{MailboxID: mbRej.ID})
3096 qdel.FilterEqual("Expunged", false)
3097 qdel.FilterLess("Received", old)
3099 expunge, err := qdel.List()
3101 return nil, false, fmt.Errorf("listing old messages: %w", err)
3104 if len(expunge) > 0 {
3105 modseq, err := a.NextModSeq(tx)
3107 return nil, false, fmt.Errorf("next mod seq: %v", err)
3110 chremuids, chmbcounts, err := a.MessageRemove(log, tx, modseq, mbRej, RemoveOpts{}, expunge...)
3112 return nil, false, fmt.Errorf("removing messages: %w", err)
3114 if err := tx.Update(mbRej); err != nil {
3115 return nil, false, fmt.Errorf("updating mailbox: %v", err)
3117 changes = append(changes, chremuids, chmbcounts)
3120 // We allow up to n messages.
3121 qcount := bstore.QueryTx[Message](tx)
3122 qcount.FilterNonzero(Message{MailboxID: mbRej.ID})
3123 qcount.FilterEqual("Expunged", false)
3125 n, err := qcount.Count()
3127 return nil, false, fmt.Errorf("counting rejects: %w", err)
3131 return changes, hasSpace, nil
3134// RejectsRemove removes a message from the rejects mailbox if present.
3136// Caller most hold account wlock.
3137// Changes are broadcasted.
3138func (a *Account) RejectsRemove(log mlog.Log, rejectsMailbox, messageID string) error {
3139 var changes []Change
3141 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
3142 mb, err := a.MailboxFind(tx, rejectsMailbox)
3144 return fmt.Errorf("finding mailbox: %w", err)
3150 q := bstore.QueryTx[Message](tx)
3151 q.FilterNonzero(Message{MailboxID: mb.ID, MessageID: messageID})
3152 q.FilterEqual("Expunged", false)
3153 expunge, err := q.List()
3155 return fmt.Errorf("listing messages to remove: %w", err)
3158 if len(expunge) == 0 {
3162 modseq, err := a.NextModSeq(tx)
3164 return fmt.Errorf("get next mod seq: %v", err)
3167 chremuids, chmbcounts, err := a.MessageRemove(log, tx, modseq, mb, RemoveOpts{}, expunge...)
3169 return fmt.Errorf("removing messages: %w", err)
3171 changes = append(changes, chremuids, chmbcounts)
3173 if err := tx.Update(mb); err != nil {
3174 return fmt.Errorf("saving mailbox: %w", err)
3183 BroadcastChanges(a, changes)
3188// AddMessageSize adjusts the DiskUsage.MessageSize by size.
3189func (a *Account) AddMessageSize(log mlog.Log, tx *bstore.Tx, size int64) error {
3190 du := DiskUsage{ID: 1}
3191 if err := tx.Get(&du); err != nil {
3192 return fmt.Errorf("get diskusage: %v", err)
3194 du.MessageSize += size
3195 if du.MessageSize < 0 {
3196 log.Error("negative total message size", slog.Int64("delta", size), slog.Int64("newtotalsize", du.MessageSize))
3198 if err := tx.Update(&du); err != nil {
3199 return fmt.Errorf("update total message size: %v", err)
3204// QuotaMessageSize returns the effective maximum total message size for an
3205// account. Returns 0 if there is no maximum.
3206func (a *Account) QuotaMessageSize() int64 {
3208 size := conf.QuotaMessageSize
3210 size = mox.Conf.Static.QuotaMessageSize
3218// CanAddMessageSize checks if a message of size bytes can be added, depending on
3219// total message size and configured quota for account.
3220func (a *Account) CanAddMessageSize(tx *bstore.Tx, size int64) (ok bool, maxSize int64, err error) {
3221 maxSize = a.QuotaMessageSize()
3226 du := DiskUsage{ID: 1}
3227 if err := tx.Get(&du); err != nil {
3228 return false, maxSize, fmt.Errorf("get diskusage: %v", err)
3230 return du.MessageSize+size <= maxSize, maxSize, nil
3233// We keep a cache of recent successful authentications, so we don't have to bcrypt successful calls each time.
3234var authCache = struct {
3236 success map[authKey]string
3238 success: map[authKey]string{},
3241type authKey struct {
3245// StartAuthCache starts a goroutine that regularly clears the auth cache.
3246func StartAuthCache() {
3247 go manageAuthCache()
3250func manageAuthCache() {
3253 authCache.success = map[authKey]string{}
3255 time.Sleep(15 * time.Minute)
3259// OpenEmailAuth opens an account given an email address and password.
3261// The email address may contain a catchall separator.
3262// For invalid credentials, a nil account is returned, but accName may be
3264func OpenEmailAuth(log mlog.Log, email string, password string, checkLoginDisabled bool) (racc *Account, raccName string, rerr error) {
3265 // We check for LoginDisabled after verifying the password. Otherwise users can get
3266 // messages about the account being disabled without knowing the password.
3267 acc, accName, _, err := OpenEmail(log, email, false)
3275 log.Check(err, "closing account after open auth failure")
3280 password, err = precis.OpaqueString.String(password)
3282 return nil, "", ErrUnknownCredentials
3285 pw, err := bstore.QueryDB[Password](context.TODO(), acc.DB).Get()
3287 if err == bstore.ErrAbsent {
3288 return nil, "", ErrUnknownCredentials
3290 return nil, "", fmt.Errorf("looking up password: %v", err)
3293 ok := len(password) >= 8 && authCache.success[authKey{email, pw.Hash}] == password
3296 if err := bcrypt.CompareHashAndPassword([]byte(pw.Hash), []byte(password)); err != nil {
3297 return nil, "", ErrUnknownCredentials
3300 if checkLoginDisabled {
3301 conf, aok := acc.Conf()
3303 return nil, "", fmt.Errorf("cannot find config for account")
3304 } else if conf.LoginDisabled != "" {
3305 return nil, "", fmt.Errorf("%w: %s", ErrLoginDisabled, conf.LoginDisabled)
3309 authCache.success[authKey{email, pw.Hash}] = password
3311 return acc, accName, nil
3314// OpenEmail opens an account given an email address.
3316// The email address may contain a catchall separator.
3318// Returns account on success, may return non-empty account name even on error.
3319func OpenEmail(log mlog.Log, email string, checkLoginDisabled bool) (*Account, string, config.Destination, error) {
3320 addr, err := smtp.ParseAddress(email)
3322 return nil, "", config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
3324 accountName, _, _, dest, err := mox.LookupAddress(addr.Localpart, addr.Domain, false, false, false)
3325 if err != nil && (errors.Is(err, mox.ErrAddressNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
3326 return nil, accountName, config.Destination{}, ErrUnknownCredentials
3327 } else if err != nil {
3328 return nil, accountName, config.Destination{}, fmt.Errorf("looking up address: %v", err)
3330 acc, err := OpenAccount(log, accountName, checkLoginDisabled)
3332 return nil, accountName, config.Destination{}, err
3334 return acc, accountName, dest, nil
3337// We store max 1<<shift files in each subdir of an account "msg" directory.
3338// Defaults to 1 for easy use in tests. Set to 13, for 8k message files, in main
3339// for normal operation.
3340var msgFilesPerDirShift = 1
3341var msgFilesPerDir int64 = 1 << msgFilesPerDirShift
3343func MsgFilesPerDirShiftSet(shift int) {
3344 msgFilesPerDirShift = shift
3345 msgFilesPerDir = 1 << shift
3348// 64 characters, must be power of 2 for MessagePath
3349const msgDirChars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
3351// MessagePath returns the filename of the on-disk filename, relative to the
3352// containing directory such as <account>/msg or queue.
3353// Returns names like "AB/1".
3354func MessagePath(messageID int64) string {
3355 return strings.Join(messagePathElems(messageID), string(filepath.Separator))
3358// messagePathElems returns the elems, for a single join without intermediate
3359// string allocations.
3360func messagePathElems(messageID int64) []string {
3361 v := messageID >> msgFilesPerDirShift
3364 dir += string(msgDirChars[int(v)&(len(msgDirChars)-1)])
3370 return []string{dir, strconv.FormatInt(messageID, 10)}
3373// Set returns a copy of f, with each flag that is true in mask set to the
3375func (f Flags) Set(mask, flags Flags) Flags {
3376 set := func(d *bool, m, v bool) {
3382 set(&r.Seen, mask.Seen, flags.Seen)
3383 set(&r.Answered, mask.Answered, flags.Answered)
3384 set(&r.Flagged, mask.Flagged, flags.Flagged)
3385 set(&r.Forwarded, mask.Forwarded, flags.Forwarded)
3386 set(&r.Junk, mask.Junk, flags.Junk)
3387 set(&r.Notjunk, mask.Notjunk, flags.Notjunk)
3388 set(&r.Deleted, mask.Deleted, flags.Deleted)
3389 set(&r.Draft, mask.Draft, flags.Draft)
3390 set(&r.Phishing, mask.Phishing, flags.Phishing)
3391 set(&r.MDNSent, mask.MDNSent, flags.MDNSent)
3395// Changed returns a mask of flags that have been between f and other.
3396func (f Flags) Changed(other Flags) (mask Flags) {
3397 mask.Seen = f.Seen != other.Seen
3398 mask.Answered = f.Answered != other.Answered
3399 mask.Flagged = f.Flagged != other.Flagged
3400 mask.Forwarded = f.Forwarded != other.Forwarded
3401 mask.Junk = f.Junk != other.Junk
3402 mask.Notjunk = f.Notjunk != other.Notjunk
3403 mask.Deleted = f.Deleted != other.Deleted
3404 mask.Draft = f.Draft != other.Draft
3405 mask.Phishing = f.Phishing != other.Phishing
3406 mask.MDNSent = f.MDNSent != other.MDNSent
3410// Strings returns the flags that are set in their string form.
3411func (f Flags) Strings() []string {
3412 fields := []struct {
3416 {`$forwarded`, f.Forwarded},
3418 {`$mdnsent`, f.MDNSent},
3419 {`$notjunk`, f.Notjunk},
3420 {`$phishing`, f.Phishing},
3421 {`\answered`, f.Answered},
3422 {`\deleted`, f.Deleted},
3423 {`\draft`, f.Draft},
3424 {`\flagged`, f.Flagged},
3428 for _, fh := range fields {
3430 l = append(l, fh.word)
3436var systemWellKnownFlags = map[string]bool{
3449// ParseFlagsKeywords parses a list of textual flags into system/known flags, and
3450// other keywords. Keywords are lower-cased and sorted and check for valid syntax.
3451func ParseFlagsKeywords(l []string) (flags Flags, keywords []string, rerr error) {
3452 fields := map[string]*bool{
3453 `\answered`: &flags.Answered,
3454 `\flagged`: &flags.Flagged,
3455 `\deleted`: &flags.Deleted,
3456 `\seen`: &flags.Seen,
3457 `\draft`: &flags.Draft,
3458 `$junk`: &flags.Junk,
3459 `$notjunk`: &flags.Notjunk,
3460 `$forwarded`: &flags.Forwarded,
3461 `$phishing`: &flags.Phishing,
3462 `$mdnsent`: &flags.MDNSent,
3464 seen := map[string]bool{}
3465 for _, f := range l {
3466 f = strings.ToLower(f)
3467 if field, ok := fields[f]; ok {
3471 return Flags{}, nil, fmt.Errorf("duplicate keyword %s", f)
3474 if err := CheckKeyword(f); err != nil {
3475 return Flags{}, nil, fmt.Errorf("invalid keyword %s", f)
3477 keywords = append(keywords, f)
3481 sort.Strings(keywords)
3482 return flags, keywords, nil
3485// RemoveKeywords removes keywords from l, returning whether any modifications were
3486// made, and a slice, a new slice in case of modifications. Keywords must have been
3487// validated earlier, e.g. through ParseFlagKeywords or CheckKeyword. Should only
3488// be used with valid keywords, not with system flags like \Seen.
3489func RemoveKeywords(l, remove []string) ([]string, bool) {
3492 for _, k := range remove {
3493 if i := slices.Index(l, k); i >= 0 {
3498 copy(l[i:], l[i+1:])
3506// MergeKeywords adds keywords from add into l, returning whether it added any
3507// keyword, and the slice with keywords, a new slice if modifications were made.
3508// Keywords are only added if they aren't already present. Should only be used with
3509// keywords, not with system flags like \Seen.
3510func MergeKeywords(l, add []string) ([]string, bool) {
3513 for _, k := range add {
3514 if !slices.Contains(l, k) {
3529// CheckKeyword returns an error if kw is not a valid keyword. Kw should
3530// already be in lower-case.
3531func CheckKeyword(kw string) error {
3533 return fmt.Errorf("keyword cannot be empty")
3535 if systemWellKnownFlags[kw] {
3536 return fmt.Errorf("cannot use well-known flag as keyword")
3538 for _, c := range kw {
3540 if c <= ' ' || c > 0x7e || c >= 'A' && c <= 'Z' || strings.ContainsRune(`(){%*"\]`, c) {
3541 return errors.New(`not a valid keyword, must be lower-case ascii without spaces and without any of these characters: (){%*"\]`)
3547// SendLimitReached checks whether sending a message to recipients would reach
3548// the limit of outgoing messages for the account. If so, the message should
3549// not be sent. If the returned numbers are >= 0, the limit was reached and the
3550// values are the configured limits.
3552// To limit damage to the internet and our reputation in case of account
3553// compromise, we limit the max number of messages sent in a 24 hour window, both
3554// total number of messages and number of first-time recipients.
3555func (a *Account) SendLimitReached(tx *bstore.Tx, recipients []smtp.Path) (msglimit, rcptlimit int, rerr error) {
3557 msgmax := conf.MaxOutgoingMessagesPerDay
3559 // For human senders, 1000 recipients in a day is quite a lot.
3562 rcptmax := conf.MaxFirstTimeRecipientsPerDay
3564 // Human senders may address a new human-sized list of people once in a while. In
3565 // case of a compromise, a spammer will probably try to send to many new addresses.
3569 rcpts := map[string]time.Time{}
3571 err := bstore.QueryTx[Outgoing](tx).FilterGreater("Submitted", time.Now().Add(-24*time.Hour)).ForEach(func(o Outgoing) error {
3573 if rcpts[o.Recipient].IsZero() || o.Submitted.Before(rcpts[o.Recipient]) {
3574 rcpts[o.Recipient] = o.Submitted
3579 return -1, -1, fmt.Errorf("querying message recipients in past 24h: %w", err)
3581 if n+len(recipients) > msgmax {
3582 return msgmax, -1, nil
3585 // Only check if max first-time recipients is reached if there are enough messages
3586 // to trigger the limit.
3587 if n+len(recipients) < rcptmax {
3591 isFirstTime := func(rcpt string, before time.Time) (bool, error) {
3592 exists, err := bstore.QueryTx[Outgoing](tx).FilterNonzero(Outgoing{Recipient: rcpt}).FilterLess("Submitted", before).Exists()
3598 for _, r := range recipients {
3599 if first, err := isFirstTime(r.XString(true), now); err != nil {
3600 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
3605 for r, t := range rcpts {
3606 if first, err := isFirstTime(r, t); err != nil {
3607 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
3612 if firsttime > rcptmax {
3613 return -1, rcptmax, nil
3618var ErrMailboxExpunged = errors.New("mailbox was deleted")
3620// MailboxID gets a mailbox by ID.
3622// Returns bstore.ErrAbsent if the mailbox does not exist.
3623// Returns ErrMailboxExpunged if the mailbox is expunged.
3624func MailboxID(tx *bstore.Tx, id int64) (Mailbox, error) {
3625 mb := Mailbox{ID: id}
3627 if err == nil && mb.Expunged {
3628 return Mailbox{}, ErrMailboxExpunged
3633// MailboxCreate creates a new mailbox, including any missing parent mailboxes,
3634// the total list of created mailboxes is returned in created. On success, if
3635// exists is false and rerr nil, the changes must be broadcasted by the caller.
3637// The mailbox is created with special-use flags, with those flags taken away from
3638// other mailboxes if they have them, reflected in the returned changes.
3640// Name must be in normalized form, see CheckMailboxName.
3641func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUse) (nmb Mailbox, changes []Change, created []string, exists bool, rerr error) {
3642 elems := strings.Split(name, "/")
3645 for i, elem := range elems {
3650 exists, err := a.MailboxExists(tx, p)
3652 return Mailbox{}, nil, nil, false, fmt.Errorf("checking if mailbox exists")
3655 if i == len(elems)-1 {
3656 return Mailbox{}, nil, nil, true, fmt.Errorf("mailbox already exists")
3660 mb, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse, &modseq)
3662 return Mailbox{}, nil, nil, false, fmt.Errorf("ensuring mailbox exists: %v", err)
3665 changes = append(changes, nchanges...)
3666 created = append(created, p)
3668 return nmb, changes, created, false, nil
3671// MailboxRename renames mailbox mbsrc to dst, including children of mbsrc, and
3672// adds missing parents for dst.
3674// Name must be in normalized form, see CheckMailboxName, and cannot be Inbox.
3675func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc *Mailbox, dst string, modseq *ModSeq) (changes []Change, isInbox, alreadyExists bool, rerr error) {
3676 if mbsrc.Name == "Inbox" || dst == "Inbox" {
3677 return nil, true, false, fmt.Errorf("inbox cannot be renamed")
3680 // Check if destination mailbox already exists.
3681 if exists, err := a.MailboxExists(tx, dst); err != nil {
3682 return nil, false, false, fmt.Errorf("checking if destination mailbox exists: %v", err)
3684 return nil, false, true, fmt.Errorf("destination mailbox already exists")
3689 *modseq, err = a.NextModSeq(tx)
3691 return nil, false, false, fmt.Errorf("get next modseq: %v", err)
3695 origName := mbsrc.Name
3697 // Move children to their new name.
3698 srcPrefix := mbsrc.Name + "/"
3699 q := bstore.QueryTx[Mailbox](tx)
3700 q.FilterEqual("Expunged", false)
3701 q.FilterFn(func(mb Mailbox) bool {
3702 return strings.HasPrefix(mb.Name, srcPrefix)
3704 q.SortDesc("Name") // From leaf towards dst.
3705 kids, err := q.List()
3707 return nil, false, false, fmt.Errorf("listing child mailboxes")
3710 // Rename children, from leaf towards dst (because sorted reverse by name).
3711 for _, mb := range kids {
3712 nname := dst + "/" + mb.Name[len(mbsrc.Name)+1:]
3714 if err := tx.Get(&Subscription{nname}); err == nil {
3715 flags = []string{`\Subscribed`}
3716 } else if err != bstore.ErrAbsent {
3717 return nil, false, false, fmt.Errorf("look up subscription for new name of child %q: %v", nname, err)
3720 changes = append(changes, ChangeRenameMailbox{mb.ID, mb.Name, nname, flags, *modseq})
3724 if err := tx.Update(&mb); err != nil {
3725 return nil, false, false, fmt.Errorf("rename child mailbox %q: %v", mb.Name, err)
3729 // Move name out of the way. We may have to create it again, as our new parent.
3731 if err := tx.Get(&Subscription{dst}); err == nil {
3732 flags = []string{`\Subscribed`}
3733 } else if err != bstore.ErrAbsent {
3734 return nil, false, false, fmt.Errorf("look up subscription for new name %q: %v", dst, err)
3736 changes = append(changes, ChangeRenameMailbox{mbsrc.ID, mbsrc.Name, dst, flags, *modseq})
3737 mbsrc.ModSeq = *modseq
3739 if err := tx.Update(mbsrc); err != nil {
3740 return nil, false, false, fmt.Errorf("rename mailbox: %v", err)
3743 // Add any missing parents for the new name. A mailbox may have been renamed from
3744 // a/b to a/b/x/y, and we'll have to add a new "a" and a/b.
3745 t := strings.Split(dst, "/")
3748 var parentChanges []Change
3750 s := strings.Join(t[:i+1], "/")
3751 q := bstore.QueryTx[Mailbox](tx)
3752 q.FilterEqual("Expunged", false)
3753 q.FilterNonzero(Mailbox{Name: s})
3758 } else if err != bstore.ErrAbsent {
3759 return nil, false, false, fmt.Errorf("lookup destination parent mailbox %q: %v", s, err)
3762 uidval, err := a.NextUIDValidity(tx)
3764 return nil, false, false, fmt.Errorf("next uid validity: %v", err)
3769 ParentID: parent.ID,
3771 UIDValidity: uidval,
3775 if err := tx.Insert(&parent); err != nil {
3776 return nil, false, false, fmt.Errorf("inserting destination parent mailbox %q: %v", s, err)
3780 if err := tx.Get(&Subscription{parent.Name}); err == nil {
3781 flags = []string{`\Subscribed`}
3782 } else if err != bstore.ErrAbsent {
3783 return nil, false, false, fmt.Errorf("look up subscription for new parent %q: %v", parent.Name, err)
3785 parentChanges = append(parentChanges, ChangeAddMailbox{parent, flags})
3788 mbsrc.ParentID = parent.ID
3789 if err := tx.Update(mbsrc); err != nil {
3790 return nil, false, false, fmt.Errorf("set parent id on rename mailbox: %v", err)
3793 // If we were moved from a/b to a/b/x, we mention the creation of a/b after we mentioned the rename.
3794 if strings.HasPrefix(dst, origName+"/") {
3795 changes = append(changes, parentChanges...)
3797 changes = slices.Concat(parentChanges, changes)
3800 return changes, false, false, nil
3803// MailboxDelete marks a mailbox as deleted, including its annotations. If it has
3804// children, the return value indicates that and an error is returned.
3806// Caller should broadcast the changes (deleting all messages in the mailbox and
3807// deleting the mailbox itself).
3808func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx, mb *Mailbox) (changes []Change, hasChildren bool, rerr error) {
3809 // Look for existence of child mailboxes. There is a lot of text in the IMAP RFCs about
3810 // NoInferior and NoSelect. We just require only leaf mailboxes are deleted.
3811 qmb := bstore.QueryTx[Mailbox](tx)
3812 qmb.FilterEqual("Expunged", false)
3813 mbprefix := mb.Name + "/"
3814 qmb.FilterFn(func(xmb Mailbox) bool {
3815 return strings.HasPrefix(xmb.Name, mbprefix)
3817 if childExists, err := qmb.Exists(); err != nil {
3818 return nil, false, fmt.Errorf("checking if mailbox has child: %v", err)
3819 } else if childExists {
3820 return nil, true, fmt.Errorf("mailbox has a child, only leaf mailboxes can be deleted")
3823 modseq, err := a.NextModSeq(tx)
3825 return nil, false, fmt.Errorf("get next modseq: %v", err)
3828 qm := bstore.QueryTx[Message](tx)
3829 qm.FilterNonzero(Message{MailboxID: mb.ID})
3830 qm.FilterEqual("Expunged", false)
3834 return nil, false, fmt.Errorf("listing messages in mailbox to remove; %v", err)
3838 chrem, _, err := a.MessageRemove(log, tx, modseq, mb, RemoveOpts{}, l...)
3840 return nil, false, fmt.Errorf("marking messages removed: %v", err)
3842 changes = append(changes, chrem)
3846 qa := bstore.QueryTx[Annotation](tx)
3847 qa.FilterNonzero(Annotation{MailboxID: mb.ID})
3848 qa.FilterEqual("Expunged", false)
3849 if _, err := qa.UpdateFields(map[string]any{"ModSeq": modseq, "Expunged": true, "IsString": false, "Value": []byte(nil)}); err != nil {
3850 return nil, false, fmt.Errorf("removing annotations for mailbox: %v", err)
3852 // Not sending changes about annotations on this mailbox, since the entire mailbox
3853 // is being removed.
3857 mb.SpecialUse = SpecialUse{}
3859 if err := tx.Update(mb); err != nil {
3860 return nil, false, fmt.Errorf("updating mailbox: %v", err)
3863 changes = append(changes, mb.ChangeRemoveMailbox())
3864 return changes, false, nil
3867// CheckMailboxName checks if name is valid, returning an INBOX-normalized name.
3868// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
3869// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
3870// unicode-normalized, or when empty or has special characters.
3872// If name is the inbox, and allowInbox is false, this is indicated with the isInbox return parameter.
3873// For that case, and for other invalid names, an error is returned.
3874func CheckMailboxName(name string, allowInbox bool) (normalizedName string, isInbox bool, rerr error) {
3875 t := strings.Split(name, "/")
3876 if strings.EqualFold(t[0], "inbox") {
3877 if len(name) == len("inbox") && !allowInbox {
3878 return "", true, fmt.Errorf("special mailbox name Inbox not allowed")
3880 name = "Inbox" + name[len("Inbox"):]
3883 if norm.NFC.String(name) != name {
3884 return "", false, errors.New("non-unicode-normalized mailbox names not allowed")
3887 for _, e := range t {
3890 return "", false, errors.New("empty mailbox name")
3892 return "", false, errors.New(`"." not allowed`)
3894 return "", false, errors.New(`".." not allowed`)
3897 if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.Contains(name, "//") {
3898 return "", false, errors.New("bad slashes in mailbox name")
3901 // "%" and "*" are difficult to use with the IMAP LIST command, but we allow mostly
3903 if strings.HasPrefix(name, "#") {
3904 return "", false, errors.New("mailbox name cannot start with hash due to conflict with imap namespaces")
3907 // "#" and "&" are special in IMAP mailbox names. "#" for namespaces, "&" for
3910 for _, c := range name {
3912 if c <= 0x1f || c >= 0x7f && c <= 0x9f || c == 0x2028 || c == 0x2029 {
3913 return "", false, errors.New("control characters not allowed in mailbox name")
3916 return name, false, nil