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. Messages
12are stored in the msg/ subdirectory, each in their own file. The on-disk message
13does not contain headers generated during an incoming SMTP transaction, such as
14Received and Authentication-Results headers. Those are in the database to
15prevent having to rewrite incoming messages (e.g. Authentication-Result for DKIM
16signatures can only be determined after having read the message). Messages must
17be read through MsgReader, which transparently adds the prefix from the
22// todo: make up a function naming scheme that indicates whether caller should broadcast changes.
27 cryptorand "crypto/rand"
47 "golang.org/x/crypto/bcrypt"
48 "golang.org/x/text/secure/precis"
49 "golang.org/x/text/unicode/norm"
51 "github.com/mjl-/bstore"
53 "github.com/mjl-/mox/config"
54 "github.com/mjl-/mox/dns"
55 "github.com/mjl-/mox/message"
56 "github.com/mjl-/mox/metrics"
57 "github.com/mjl-/mox/mlog"
58 "github.com/mjl-/mox/mox-"
59 "github.com/mjl-/mox/moxio"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/publicsuffix"
62 "github.com/mjl-/mox/scram"
63 "github.com/mjl-/mox/smtp"
66// If true, each time an account is closed its database file is checked for
67// consistency. If an inconsistency is found, panic is called. Set by default
68// because of all the packages with tests, the mox main function sets it to
70var CheckConsistencyOnClose = true
73 ErrUnknownMailbox = errors.New("no such mailbox")
74 ErrUnknownCredentials = errors.New("credentials not found")
75 ErrAccountUnknown = errors.New("no such account")
76 ErrOverQuota = errors.New("account over quota")
77 ErrLoginDisabled = errors.New("login disabled for account")
80var DefaultInitialMailboxes = config.InitialMailboxes{
81 SpecialUse: config.SpecialUseMailboxes{
96// CRAMMD5 holds HMAC ipad and opad hashes that are initialized with the first
97// block with (a derivation of) the key/password, so we don't store the password in plain
104// BinaryMarshal is used by bstore to store the ipad/opad hash states.
105func (c CRAMMD5) MarshalBinary() ([]byte, error) {
106 if c.Ipad == nil || c.Opad == nil {
110 ipad, err := c.Ipad.(encoding.BinaryMarshaler).MarshalBinary()
112 return nil, fmt.Errorf("marshal ipad: %v", err)
114 opad, err := c.Opad.(encoding.BinaryMarshaler).MarshalBinary()
116 return nil, fmt.Errorf("marshal opad: %v", err)
118 buf := make([]byte, 2+len(ipad)+len(opad))
119 ipadlen := uint16(len(ipad))
120 buf[0] = byte(ipadlen >> 8)
121 buf[1] = byte(ipadlen >> 0)
123 copy(buf[2+len(ipad):], opad)
127// BinaryUnmarshal is used by bstore to restore the ipad/opad hash states.
128func (c *CRAMMD5) UnmarshalBinary(buf []byte) error {
134 return fmt.Errorf("short buffer")
136 ipadlen := int(uint16(buf[0])<<8 | uint16(buf[1])<<0)
137 if len(buf) < 2+ipadlen {
138 return fmt.Errorf("buffer too short for ipadlen")
142 if err := ipad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2 : 2+ipadlen]); err != nil {
143 return fmt.Errorf("unmarshal ipad: %v", err)
145 if err := opad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2+ipadlen:]); err != nil {
146 return fmt.Errorf("unmarshal opad: %v", err)
148 *c = CRAMMD5{ipad, opad}
152// Password holds credentials in various forms, for logging in with SMTP/IMAP.
153type Password struct {
154 Hash string // bcrypt hash for IMAP LOGIN, SASL PLAIN and HTTP basic authentication.
155 CRAMMD5 CRAMMD5 // For SASL CRAM-MD5.
156 SCRAMSHA1 SCRAM // For SASL SCRAM-SHA-1.
157 SCRAMSHA256 SCRAM // For SASL SCRAM-SHA-256.
160// Subjectpass holds the secret key used to sign subjectpass tokens.
161type Subjectpass struct {
162 Email string // Our destination address (canonical, with catchall localpart stripped).
166// NextUIDValidity is a singleton record in the database with the next UIDValidity
167// to use for the next mailbox.
168type NextUIDValidity struct {
169 ID int // Just a single record with ID 1.
173// SyncState track ModSeqs.
174type SyncState struct {
175 ID int // Just a single record with ID 1.
177 // Last used, next assigned will be one higher. The first value we hand out is 2.
178 // That's because 0 (the default value for old existing messages, from before the
179 // Message.ModSeq field) is special in IMAP, so we return it as 1.
180 LastModSeq ModSeq `bstore:"nonzero"`
182 // Highest ModSeq of expunged record that we deleted. When a clients synchronizes
183 // and requests changes based on a modseq before this one, we don't have the
184 // history to provide information about deletions. We normally keep these expunged
185 // records around, but we may periodically truly delete them to reclaim storage
186 // space. Initially set to -1 because we don't want to match with any ModSeq in the
187 // database, which can be zero values.
188 HighestDeletedModSeq ModSeq
191// Mailbox is collection of messages, e.g. Inbox or Sent.
195 // "Inbox" is the name for the special IMAP "INBOX". Slash separated
197 Name string `bstore:"nonzero,unique"`
199 // If UIDs are invalidated, e.g. when renaming a mailbox to a previously existing
200 // name, UIDValidity must be changed. Used by IMAP for synchronization.
203 // UID likely to be assigned to next message. Used by IMAP to detect messages
204 // delivered to a mailbox.
209 // Keywords as used in messages. Storing a non-system keyword for a message
210 // automatically adds it to this list. Used in the IMAP FLAGS response. Only
211 // "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in
212 // lower case (for JMAP), sorted.
215 HaveCounts bool // Whether MailboxCounts have been initialized.
216 MailboxCounts // Statistics about messages, kept up to date whenever a change happens.
219// Annotation is a per-mailbox or global (per-account) annotation for the IMAP
220// metadata extension, currently always a private annotation.
221type Annotation struct {
224 // Can be zero, indicates global (per-account) annotation.
225 MailboxID int64 `bstore:"ref Mailbox,unique MailboxID+Key"`
227 // "Entry name", always starts with "/private/". Stored lower-case, comparisons
228 // must be done case-insensitively.
229 Key string `bstore:"nonzero"`
231 IsString bool // If true, the value is a string instead of bytes.
235// Change returns a broadcastable change for the annotation.
236func (a Annotation) Change(mailboxName string) ChangeAnnotation {
237 return ChangeAnnotation{a.MailboxID, mailboxName, a.Key}
240// MailboxCounts tracks statistics about messages for a mailbox.
241type MailboxCounts struct {
242 Total int64 // Total number of messages, excluding \Deleted. For JMAP.
243 Deleted int64 // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted.
244 Unread int64 // Messages without \Seen, excluding those with \Deleted, for JMAP.
245 Unseen int64 // Messages without \Seen, including those with \Deleted, for IMAP.
246 Size int64 // Number of bytes for all messages.
249func (mc MailboxCounts) String() string {
250 return fmt.Sprintf("%d total, %d deleted, %d unread, %d unseen, size %d bytes", mc.Total, mc.Deleted, mc.Unread, mc.Unseen, mc.Size)
253// Add increases mailbox counts mc with those of delta.
254func (mc *MailboxCounts) Add(delta MailboxCounts) {
255 mc.Total += delta.Total
256 mc.Deleted += delta.Deleted
257 mc.Unread += delta.Unread
258 mc.Unseen += delta.Unseen
259 mc.Size += delta.Size
262// Add decreases mailbox counts mc with those of delta.
263func (mc *MailboxCounts) Sub(delta MailboxCounts) {
264 mc.Total -= delta.Total
265 mc.Deleted -= delta.Deleted
266 mc.Unread -= delta.Unread
267 mc.Unseen -= delta.Unseen
268 mc.Size -= delta.Size
271// SpecialUse identifies a specific role for a mailbox, used by clients to
272// understand where messages should go.
273type SpecialUse struct {
275 Draft bool // "Drafts"
281// CalculateCounts calculates the full current counts for messages in the mailbox.
282func (mb *Mailbox) CalculateCounts(tx *bstore.Tx) (mc MailboxCounts, err error) {
283 q := bstore.QueryTx[Message](tx)
284 q.FilterNonzero(Message{MailboxID: mb.ID})
285 q.FilterEqual("Expunged", false)
286 err = q.ForEach(func(m Message) error {
287 mc.Add(m.MailboxCounts())
293// ChangeSpecialUse returns a change for special-use flags, for broadcasting to
295func (mb Mailbox) ChangeSpecialUse() ChangeMailboxSpecialUse {
296 return ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse}
299// ChangeKeywords returns a change with new keywords for a mailbox (e.g. after
300// setting a new keyword on a message in the mailbox), for broadcasting to other
302func (mb Mailbox) ChangeKeywords() ChangeMailboxKeywords {
303 return ChangeMailboxKeywords{mb.ID, mb.Name, mb.Keywords}
306// KeywordsChanged returns whether the keywords in a mailbox have changed.
307func (mb Mailbox) KeywordsChanged(origmb Mailbox) bool {
308 if len(mb.Keywords) != len(origmb.Keywords) {
311 // Keywords are stored sorted.
312 for i, kw := range mb.Keywords {
313 if origmb.Keywords[i] != kw {
320// CountsChange returns a change with mailbox counts.
321func (mb Mailbox) ChangeCounts() ChangeMailboxCounts {
322 return ChangeMailboxCounts{mb.ID, mb.Name, mb.MailboxCounts}
325// Subscriptions are separate from existence of mailboxes.
326type Subscription struct {
330// Flags for a mail message.
344// FlagsAll is all flags set, for use as mask.
345var FlagsAll = Flags{true, true, true, true, true, true, true, true, true, true}
347// Validation of "message From" domain.
351 ValidationUnknown Validation = 0
352 ValidationStrict Validation = 1 // Like DMARC, with strict policies.
353 ValidationDMARC Validation = 2 // Actual DMARC policy.
354 ValidationRelaxed Validation = 3 // Like DMARC, with relaxed policies.
355 ValidationPass Validation = 4 // For SPF.
356 ValidationNeutral Validation = 5 // For SPF.
357 ValidationTemperror Validation = 6
358 ValidationPermerror Validation = 7
359 ValidationFail Validation = 8
360 ValidationSoftfail Validation = 9 // For SPF.
361 ValidationNone Validation = 10 // E.g. No records.
364// Message stored in database and per-message file on disk.
366// Contents are always the combined data from MsgPrefix and the on-disk file named
369// Messages always have a header section, even if empty. Incoming messages without
370// header section must get an empty header section added before inserting.
372 // ID, unchanged over lifetime, determines path to on-disk msg file.
373 // Set during deliver.
376 UID UID `bstore:"nonzero"` // UID, for IMAP. Set during deliver.
377 MailboxID int64 `bstore:"nonzero,unique MailboxID+UID,index MailboxID+Received,index MailboxID+ModSeq,ref Mailbox"`
379 // Modification sequence, for faster syncing with IMAP QRESYNC and JMAP.
380 // ModSeq is the last modification. CreateSeq is the Seq the message was inserted,
381 // always <= ModSeq. If Expunged is set, the message has been removed and should not
382 // be returned to the user. In this case, ModSeq is the Seq where the message is
383 // removed, and will never be changed again.
384 // We have an index on both ModSeq (for JMAP that synchronizes per account) and
385 // MailboxID+ModSeq (for IMAP that synchronizes per mailbox).
386 // The index on CreateSeq helps efficiently finding created messages for JMAP.
387 // The value of ModSeq is special for IMAP. Messages that existed before ModSeq was
388 // added have 0 as value. But modseq 0 in IMAP is special, so we return it as 1. If
389 // we get modseq 1 from a client, the IMAP server will translate it to 0. When we
390 // return modseq to clients, we turn 0 into 1.
391 ModSeq ModSeq `bstore:"index"`
392 CreateSeq ModSeq `bstore:"index"`
395 // If set, this message was delivered to a Rejects mailbox. When it is moved to a
396 // different mailbox, its MailboxOrigID is set to the destination mailbox and this
400 // If set, this is a forwarded message (through a ruleset with IsForward). This
401 // causes fields used during junk analysis to be moved to their Orig variants, and
402 // masked IP fields cleared, so they aren't used in junk classifications for
403 // incoming messages. This ensures the forwarded messages don't cause negative
404 // reputation for the forwarding mail server, which may also be sending regular
408 // MailboxOrigID is the mailbox the message was originally delivered to. Typically
409 // Inbox or Rejects, but can also be a mailbox configured in a Ruleset, or
410 // Postmaster, TLS/DMARC reporting addresses. MailboxOrigID is not changed when the
411 // message is moved to another mailbox, e.g. Archive/Trash/Junk. Used for
412 // per-mailbox reputation.
414 // MailboxDestinedID is normally 0, but when a message is delivered to the Rejects
415 // mailbox, it is set to the intended mailbox according to delivery rules,
416 // typically that of Inbox. When such a message is moved out of Rejects, the
417 // MailboxOrigID is corrected by setting it to MailboxDestinedID. This ensures the
418 // message is used for reputation calculation for future deliveries to that
421 // These are not bstore references to prevent having to update all messages in a
422 // mailbox when the original mailbox is removed. Use of these fields requires
423 // checking if the mailbox still exists.
425 MailboxDestinedID int64
427 // Received indicates time of receival over SMTP, or of IMAP APPEND.
428 Received time.Time `bstore:"default now,index"`
430 // SaveDate is the time of copy/move/save to a mailbox, used with IMAP SAVEDATE
431 // extension. Must be updated each time a message is copied/moved to another
432 // mailbox. Can be nil for messages from before this functionality was introduced.
433 SaveDate *time.Time `bstore:"default now"`
435 // Full IP address of remote SMTP server. Empty if not delivered over SMTP. The
436 // masked IPs are used to classify incoming messages. They are left empty for
437 // messages matching a ruleset for forwarded messages.
439 RemoteIPMasked1 string `bstore:"index RemoteIPMasked1+Received"` // For IPv4 /32, for IPv6 /64, for reputation.
440 RemoteIPMasked2 string `bstore:"index RemoteIPMasked2+Received"` // For IPv4 /26, for IPv6 /48.
441 RemoteIPMasked3 string `bstore:"index RemoteIPMasked3+Received"` // For IPv4 /21, for IPv6 /32.
443 // Only set if present and not an IP address. Unicode string. Empty for forwarded
445 EHLODomain string `bstore:"index EHLODomain+Received"`
446 MailFrom string // With localpart and domain. Can be empty.
447 MailFromLocalpart smtp.Localpart // SMTP "MAIL FROM", can be empty.
448 // Only set if it is a domain, not an IP. Unicode string. Empty for forwarded
449 // messages, but see OrigMailFromDomain.
450 MailFromDomain string `bstore:"index MailFromDomain+Received"`
451 RcptToLocalpart smtp.Localpart // SMTP "RCPT TO", can be empty.
452 RcptToDomain string // Unicode string.
454 // Parsed "From" message header, used for reputation along with domain validation.
455 MsgFromLocalpart smtp.Localpart
456 MsgFromDomain string `bstore:"index MsgFromDomain+Received"` // Unicode string.
457 MsgFromOrgDomain string `bstore:"index MsgFromOrgDomain+Received"` // Unicode string.
459 // Simplified statements of the Validation fields below, used for incoming messages
460 // to check reputation.
462 MailFromValidated bool
463 MsgFromValidated bool
465 EHLOValidation Validation // Validation can also take reverse IP lookup into account, not only SPF.
466 MailFromValidation Validation // Can have SPF-specific validations like ValidationSoftfail.
467 MsgFromValidation Validation // Desirable validations: Strict, DMARC, Relaxed. Will not be just Pass.
469 // Domains with verified DKIM signatures. Unicode string. For forwarded messages, a
470 // DKIM domain that matched a ruleset's verified domain is left out, but included
471 // in OrigDKIMDomains.
472 DKIMDomains []string `bstore:"index DKIMDomains+Received"`
474 // For forwarded messages,
475 OrigEHLODomain string
476 OrigDKIMDomains []string
478 // Canonicalized Message-Id, always lower-case and normalized quoting, without
479 // <>'s. Empty if missing. Used for matching message threads, and to prevent
480 // duplicate reject delivery.
481 MessageID string `bstore:"index"`
484 // For matching threads in case there is no References/In-Reply-To header. It is
485 // lower-cased, white-space collapsed, mailing list tags and re/fwd tags removed.
486 SubjectBase string `bstore:"index"`
489 // Hash of message. For rejects delivery in case there is no Message-ID, only set
490 // when delivered as reject.
493 // ID of message starting this thread.
494 ThreadID int64 `bstore:"index"`
495 // IDs of parent messages, from closest parent to the root message. Parent messages
496 // may be in a different mailbox, or may no longer exist. ThreadParentIDs must
497 // never contain the message id itself (a cycle), and parent messages must
498 // reference the same ancestors.
499 ThreadParentIDs []int64
500 // ThreadMissingLink is true if there is no match with a direct parent. E.g. first
501 // ID in ThreadParentIDs is not the direct ancestor (an intermediate message may
502 // have been deleted), or subject-based matching was done.
503 ThreadMissingLink bool
504 // If set, newly delivered child messages are automatically marked as read. This
505 // field is copied to new child messages. Changes are propagated to the webmail
508 // If set, this (sub)thread is collapsed in the webmail client, for threading mode
509 // "on" (mode "unread" ignores it). This field is copied to new child message.
510 // Changes are propagated to the webmail client.
513 // If received message was known to match a mailing list rule (with modified junk
517 // If this message is a DSN, generated by us or received. For DSNs, we don't look
518 // at the subject when matching threads.
521 ReceivedTLSVersion uint16 // 0 if unknown, 1 if plaintext/no TLS, otherwise TLS cipher suite.
522 ReceivedTLSCipherSuite uint16
523 ReceivedRequireTLS bool // Whether RequireTLS was known to be used for incoming delivery.
526 // For keywords other than system flags or the basic well-known $-flags. Only in
527 // "atom" syntax (IMAP), they are case-insensitive, always stored in lower-case
528 // (for JMAP), sorted.
529 Keywords []string `bstore:"index"`
531 TrainedJunk *bool // If nil, no training done yet. Otherwise, true is trained as junk, false trained as nonjunk.
532 MsgPrefix []byte // Typically holds received headers and/or header separator.
534 // ParsedBuf message structure. Currently saved as JSON of message.Part because bstore
535 // cannot yet store recursive types. Created when first needed, and saved in the
537 // todo: once replaced with non-json storage, remove date fixup in ../message/part.go.
541// MailboxCounts returns the delta to counts this message means for its
543func (m Message) MailboxCounts() (mc MailboxCounts) {
562func (m Message) ChangeAddUID() ChangeAddUID {
563 return ChangeAddUID{m.MailboxID, m.UID, m.ModSeq, m.Flags, m.Keywords}
566func (m Message) ChangeFlags(orig Flags) ChangeFlags {
567 mask := m.Flags.Changed(orig)
568 return ChangeFlags{MailboxID: m.MailboxID, UID: m.UID, ModSeq: m.ModSeq, Mask: mask, Flags: m.Flags, Keywords: m.Keywords}
571func (m Message) ChangeThread() ChangeThread {
572 return ChangeThread{[]int64{m.ID}, m.ThreadMuted, m.ThreadCollapsed}
575// ModSeq represents a modseq as stored in the database. ModSeq 0 in the
576// database is sent to the client as 1, because modseq 0 is special in IMAP.
577// ModSeq coming from the client are of type int64.
580func (ms ModSeq) Client() int64 {
587// ModSeqFromClient converts a modseq from a client to a modseq for internal
588// use, e.g. in a database query.
589// ModSeq 1 is turned into 0 (the Go zero value for ModSeq).
590func ModSeqFromClient(modseq int64) ModSeq {
594 return ModSeq(modseq)
597// PrepareExpunge clears fields that are no longer needed after an expunge, so
598// almost all fields. Does not change ModSeq, but does set Expunged.
599func (m *Message) PrepareExpunge() {
603 MailboxID: m.MailboxID,
604 CreateSeq: m.CreateSeq,
607 ThreadID: m.ThreadID,
611// PrepareThreading sets MessageID, SubjectBase and DSN (used in threading) based
613func (m *Message) PrepareThreading(log mlog.Log, part *message.Part) {
616 if part.Envelope == nil {
619 messageID, raw, err := message.MessageIDCanonical(part.Envelope.MessageID)
621 log.Debugx("parsing message-id, ignoring", err, slog.String("messageid", part.Envelope.MessageID))
623 log.Debug("could not parse message-id as address, continuing with raw value", slog.String("messageid", part.Envelope.MessageID))
625 m.MessageID = messageID
626 m.SubjectBase, _ = message.ThreadSubject(part.Envelope.Subject, false)
629// LoadPart returns a message.Part by reading from m.ParsedBuf.
630func (m Message) LoadPart(r io.ReaderAt) (message.Part, error) {
631 if m.ParsedBuf == nil {
632 return message.Part{}, fmt.Errorf("message not parsed")
635 err := json.Unmarshal(m.ParsedBuf, &p)
637 return p, fmt.Errorf("unmarshal message part")
643// NeedsTraining returns whether message needs a training update, based on
644// TrainedJunk (current training status) and new Junk/Notjunk flags.
645func (m Message) NeedsTraining() bool {
646 untrain := m.TrainedJunk != nil
647 untrainJunk := untrain && *m.TrainedJunk
648 train := m.Junk || m.Notjunk && !(m.Junk && m.Notjunk)
650 return untrain != train || untrain && train && untrainJunk != trainJunk
653// JunkFlagsForMailbox sets Junk and Notjunk flags based on mailbox name if configured. Often
654// used when delivering/moving/copying messages to a mailbox. Mail clients are not
655// very helpful with setting junk/notjunk flags. But clients can move/copy messages
656// to other mailboxes. So we set flags when clients move a message.
657func (m *Message) JunkFlagsForMailbox(mb Mailbox, conf config.Account) {
664 if !conf.AutomaticJunkFlags.Enabled {
668 lmailbox := strings.ToLower(mb.Name)
670 if conf.JunkMailbox != nil && conf.JunkMailbox.MatchString(lmailbox) {
673 } else if conf.NeutralMailbox != nil && conf.NeutralMailbox.MatchString(lmailbox) {
676 } else if conf.NotJunkMailbox != nil && conf.NotJunkMailbox.MatchString(lmailbox) {
679 } else if conf.JunkMailbox == nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox != nil {
682 } else if conf.JunkMailbox != nil && conf.NeutralMailbox == nil && conf.NotJunkMailbox != nil {
685 } else if conf.JunkMailbox != nil && conf.NeutralMailbox != nil && conf.NotJunkMailbox == nil {
691// Recipient represents the recipient of a message. It is tracked to allow
692// first-time incoming replies from users this account has sent messages to. When a
693// mailbox is added to the Sent mailbox the message is parsed and recipients are
694// inserted as recipient. Recipients are never removed other than for removing the
695// message. On move/copy of a message, recipients aren't modified either. For IMAP,
696// this assumes a client simply appends messages to the Sent mailbox (as opposed to
697// copying messages from some place).
698type Recipient struct {
700 MessageID int64 `bstore:"nonzero,ref Message"` // Ref gives it its own index, useful for fast removal as well.
701 Localpart string `bstore:"nonzero"` // Encoded localpart.
702 Domain string `bstore:"nonzero,index Domain+Localpart"` // Unicode string.
703 OrgDomain string `bstore:"nonzero,index"` // Unicode string.
704 Sent time.Time `bstore:"nonzero"`
707// Outgoing is a message submitted for delivery from the queue. Used to enforce
708// maximum outgoing messages.
709type Outgoing struct {
711 Recipient string `bstore:"nonzero,index"` // Canonical international address with utf8 domain.
712 Submitted time.Time `bstore:"nonzero,default now"`
715// RecipientDomainTLS stores TLS capabilities of a recipient domain as encountered
716// during most recent connection (delivery attempt).
717type RecipientDomainTLS struct {
718 Domain string // Unicode.
719 Updated time.Time `bstore:"default now"`
720 STARTTLS bool // Supports STARTTLS.
721 RequireTLS bool // Supports RequireTLS SMTP extension.
724// DiskUsage tracks quota use.
725type DiskUsage struct {
726 ID int64 // Always one record with ID 1.
727 MessageSize int64 // Sum of all messages, for quota accounting.
730// SessionToken and CSRFToken are types to prevent mixing them up.
731// Base64 raw url encoded.
732type SessionToken string
735// LoginSession represents a login session. We keep a limited number of sessions
736// for a user, removing the oldest session when a new one is created.
737type LoginSession struct {
739 Created time.Time `bstore:"nonzero,default now"` // Of original login.
740 Expires time.Time `bstore:"nonzero"` // Extended each time it is used.
741 SessionTokenBinary [16]byte `bstore:"nonzero"` // Stored in cookie, like "webmailsession" or "webaccountsession".
742 CSRFTokenBinary [16]byte // For API requests, in "x-mox-csrf" header.
743 AccountName string `bstore:"nonzero"`
744 LoginAddress string `bstore:"nonzero"`
746 // Set when loading from database.
747 sessionToken SessionToken
751// Quoting is a setting for how to quote in replies/forwards.
755 Default Quoting = "" // Bottom-quote if text is selected, top-quote otherwise.
756 Bottom Quoting = "bottom"
760// Settings are webmail client settings.
761type Settings struct {
762 ID uint8 // Singleton ID 1.
767 // Whether to show the bars underneath the address input fields indicating
768 // starttls/dnssec/dane/mtasts/requiretls support by address.
769 ShowAddressSecurity bool
771 // Show HTML version of message by default, instead of plain text.
774 // If true, don't show shortcuts in webmail after mouse interaction.
777 // Additional headers to display in message view. E.g. Delivered-To, User-Agent, X-Mox-Reason.
781// ViewMode how a message should be viewed: its text parts, html parts, or html
782// with loading external resources.
786 ModeText ViewMode = "text"
787 ModeHTML ViewMode = "html"
788 ModeHTMLExt ViewMode = "htmlext" // HTML with external resources.
791// FromAddressSettings are webmail client settings per "From" address.
792type FromAddressSettings struct {
793 FromAddress string // Unicode.
797// RulesetNoListID records a user "no" response to the question of
798// creating/removing a ruleset after moving a message with list-id header from/to
800type RulesetNoListID struct {
802 RcptToAddress string `bstore:"nonzero"`
803 ListID string `bstore:"nonzero"`
804 ToInbox bool // Otherwise from Inbox to other mailbox.
807// RulesetNoMsgFrom records a user "no" response to the question of
808// creating/moveing a ruleset after moving a mesage with message "from" address
810type RulesetNoMsgFrom struct {
812 RcptToAddress string `bstore:"nonzero"`
813 MsgFromAddress string `bstore:"nonzero"` // Unicode.
814 ToInbox bool // Otherwise from Inbox to other mailbox.
817// RulesetNoMailbox represents a "never from/to this mailbox" response to the
818// question of adding/removing a ruleset after moving a message.
819type RulesetNoMailbox struct {
822 // The mailbox from/to which the move has happened.
823 // Not a references, if mailbox is deleted, an entry becomes ineffective.
824 MailboxID int64 `bstore:"nonzero"`
825 ToMailbox bool // Whether MailboxID is the destination of the move (instead of source).
828// Types stored in DB.
840 RecipientDomainTLS{},
844 FromAddressSettings{},
851// Account holds the information about a user, includings mailboxes, messages, imap subscriptions.
853 Name string // Name, according to configuration.
854 Dir string // Directory where account files, including the database, bloom filter, and mail messages, are stored for this account.
855 DBPath string // Path to database with mailboxes, messages, etc.
856 DB *bstore.DB // Open database connection.
858 // Channel that is closed if/when account has/gets "threads" accounting (see
860 threadsCompleted chan struct{}
861 // If threads upgrade completed with error, this is set. Used for warning during
862 // delivery, or aborting when importing.
865 // Write lock must be held for account/mailbox modifications including message delivery.
866 // Read lock for reading mailboxes/messages.
867 // When making changes to mailboxes/messages, changes must be broadcasted before
868 // releasing the lock to ensure proper UID ordering.
871 nused int // Reference count, while >0, this account is alive and shared.
876 Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed.
879// InitialUIDValidity returns a UIDValidity used for initializing an account.
880// It can be replaced during tests with a predictable value.
881var InitialUIDValidity = func() uint32 {
882 return uint32(time.Now().Unix() >> 1) // A 2-second resolution will get us far enough beyond 2038.
885var openAccounts = struct {
886 names map[string]*Account
889 names: map[string]*Account{},
892func closeAccount(acc *Account) (rerr error) {
895 defer openAccounts.Unlock()
897 // threadsCompleted must be closed now because it increased nused.
898 rerr = acc.DB.Close()
900 delete(openAccounts.names, acc.Name)
905// OpenAccount opens an account by name.
907// No additional data path prefix or ".db" suffix should be added to the name.
908// A single shared account exists per name.
909func OpenAccount(log mlog.Log, name string, checkLoginDisabled bool) (*Account, error) {
911 defer openAccounts.Unlock()
912 if acc, ok := openAccounts.names[name]; ok {
917 if a, ok := mox.Conf.Account(name); !ok {
918 return nil, ErrAccountUnknown
919 } else if checkLoginDisabled && a.LoginDisabled != "" {
920 return nil, fmt.Errorf("%w: %s", ErrLoginDisabled, a.LoginDisabled)
923 acc, err := openAccount(log, name)
927 openAccounts.names[name] = acc
931// openAccount opens an existing account, or creates it if it is missing.
932func openAccount(log mlog.Log, name string) (a *Account, rerr error) {
933 dir := filepath.Join(mox.DataDirPath("accounts"), name)
934 return OpenAccountDB(log, dir, name)
937// OpenAccountDB opens an account database file and returns an initialized account
938// or error. Only exported for use by subcommands that verify the database file.
939// Almost all account opens must go through OpenAccount/OpenEmail/OpenEmailAuth.
940func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, rerr error) {
941 dbpath := filepath.Join(accountDir, "index.db")
943 // Create account if it doesn't exist yet.
945 if _, err := os.Stat(dbpath); err != nil && os.IsNotExist(err) {
947 os.MkdirAll(accountDir, 0770)
950 opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: moxvar.RegisterLogger(dbpath, log.Logger)}
951 db, err := bstore.Open(context.TODO(), dbpath, &opts, DBTypes...)
971 threadsCompleted: make(chan struct{}),
975 if err := initAccount(db); err != nil {
976 return nil, fmt.Errorf("initializing account: %v", err)
978 close(acc.threadsCompleted)
982 // Ensure singletons are present. Mailbox counts and total message size, Settings.
984 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
985 if tx.Get(&Settings{ID: 1}) == bstore.ErrAbsent {
986 if err := tx.Insert(&Settings{ID: 1, ShowAddressSecurity: true}); err != nil {
991 err := bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
994 log.Info("first calculation of mailbox counts for account", slog.String("account", accountName))
996 mc, err := mb.CalculateCounts(tx)
1000 mb.HaveCounts = true
1001 mb.MailboxCounts = mc
1002 return tx.Update(&mb)
1008 du := DiskUsage{ID: 1}
1010 if err == nil || !errors.Is(err, bstore.ErrAbsent) {
1013 // No DiskUsage record yet, calculate total size and insert.
1014 err = bstore.QueryTx[Mailbox](tx).ForEach(func(mb Mailbox) error {
1015 du.MessageSize += mb.Size
1021 return tx.Insert(&du)
1024 return nil, fmt.Errorf("calculating counts for mailbox or inserting settings: %v", err)
1027 // Start adding threading if needed.
1028 up := Upgrade{ID: 1}
1029 err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
1031 if err == bstore.ErrAbsent {
1032 if err := tx.Insert(&up); err != nil {
1033 return fmt.Errorf("inserting initial upgrade record: %v", err)
1040 return nil, fmt.Errorf("checking message threading: %v", err)
1042 if up.Threads == 2 {
1043 close(acc.threadsCompleted)
1047 // Increase account use before holding on to account in background.
1048 // Caller holds the lock. The goroutine below decreases nused by calling
1052 // Ensure all messages have a MessageID and SubjectBase, which are needed when
1053 // matching threads.
1054 // Then assign messages to threads, in the same way we do during imports.
1055 log.Info("upgrading account for threading, in background", slog.String("account", acc.Name))
1058 err := closeAccount(acc)
1059 log.Check(err, "closing use of account after upgrading account storage for threads", slog.String("account", a.Name))
1061 // Mark that upgrade has finished, possibly error is indicated in threadsErr.
1062 close(acc.threadsCompleted)
1066 x := recover() // Should not happen, but don't take program down if it does.
1068 log.Error("upgradeThreads panic", slog.Any("err", x))
1070 metrics.PanicInc(metrics.Upgradethreads)
1071 acc.threadsErr = fmt.Errorf("panic during upgradeThreads: %v", x)
1075 err := upgradeThreads(mox.Shutdown, log, acc, &up)
1078 log.Errorx("upgrading account for threading, aborted", err, slog.String("account", a.Name))
1080 log.Info("upgrading account for threading, completed", slog.String("account", a.Name))
1086// ThreadingWait blocks until the one-time account threading upgrade for the
1087// account has completed, and returns an error if not successful.
1089// To be used before starting an import of messages.
1090func (a *Account) ThreadingWait(log mlog.Log) error {
1092 case <-a.threadsCompleted:
1096 log.Debug("waiting for account upgrade to complete")
1098 <-a.threadsCompleted
1102func initAccount(db *bstore.DB) error {
1103 return db.Write(context.TODO(), func(tx *bstore.Tx) error {
1104 uidvalidity := InitialUIDValidity()
1106 if err := tx.Insert(&Upgrade{ID: 1, Threads: 2}); err != nil {
1109 if err := tx.Insert(&DiskUsage{ID: 1}); err != nil {
1112 if err := tx.Insert(&Settings{ID: 1}); err != nil {
1116 if len(mox.Conf.Static.DefaultMailboxes) > 0 {
1117 // Deprecated in favor of InitialMailboxes.
1118 defaultMailboxes := mox.Conf.Static.DefaultMailboxes
1119 mailboxes := []string{"Inbox"}
1120 for _, name := range defaultMailboxes {
1121 if strings.EqualFold(name, "Inbox") {
1124 mailboxes = append(mailboxes, name)
1126 for _, name := range mailboxes {
1127 mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true}
1128 if strings.HasPrefix(name, "Archive") {
1130 } else if strings.HasPrefix(name, "Drafts") {
1132 } else if strings.HasPrefix(name, "Junk") {
1134 } else if strings.HasPrefix(name, "Sent") {
1136 } else if strings.HasPrefix(name, "Trash") {
1139 if err := tx.Insert(&mb); err != nil {
1140 return fmt.Errorf("creating mailbox: %w", err)
1142 if err := tx.Insert(&Subscription{name}); err != nil {
1143 return fmt.Errorf("adding subscription: %w", err)
1147 mailboxes := mox.Conf.Static.InitialMailboxes
1148 var zerouse config.SpecialUseMailboxes
1149 if mailboxes.SpecialUse == zerouse && len(mailboxes.Regular) == 0 {
1150 mailboxes = DefaultInitialMailboxes
1153 add := func(name string, use SpecialUse) error {
1154 mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, SpecialUse: use, HaveCounts: true}
1155 if err := tx.Insert(&mb); err != nil {
1156 return fmt.Errorf("creating mailbox: %w", err)
1158 if err := tx.Insert(&Subscription{name}); err != nil {
1159 return fmt.Errorf("adding subscription: %w", err)
1163 addSpecialOpt := func(nameOpt string, use SpecialUse) error {
1167 return add(nameOpt, use)
1173 {"Inbox", SpecialUse{}},
1174 {mailboxes.SpecialUse.Archive, SpecialUse{Archive: true}},
1175 {mailboxes.SpecialUse.Draft, SpecialUse{Draft: true}},
1176 {mailboxes.SpecialUse.Junk, SpecialUse{Junk: true}},
1177 {mailboxes.SpecialUse.Sent, SpecialUse{Sent: true}},
1178 {mailboxes.SpecialUse.Trash, SpecialUse{Trash: true}},
1180 for _, e := range l {
1181 if err := addSpecialOpt(e.nameOpt, e.use); err != nil {
1185 for _, name := range mailboxes.Regular {
1186 if err := add(name, SpecialUse{}); err != nil {
1193 if err := tx.Insert(&NextUIDValidity{1, uidvalidity}); err != nil {
1194 return fmt.Errorf("inserting nextuidvalidity: %w", err)
1200// CheckClosed asserts that the account has a zero reference count. For use in tests.
1201func (a *Account) CheckClosed() {
1203 defer openAccounts.Unlock()
1205 panic(fmt.Sprintf("account still in use, %d refs", a.nused))
1209// Close reduces the reference count, and closes the database connection when
1210// it was the last user.
1211func (a *Account) Close() error {
1212 if CheckConsistencyOnClose {
1213 xerr := a.CheckConsistency()
1214 err := closeAccount(a)
1220 return closeAccount(a)
1223// CheckConsistency checks the consistency of the database and returns a non-nil
1224// error for these cases:
1226// - Missing on-disk file for message.
1227// - Mismatch between message size and length of MsgPrefix and on-disk file.
1228// - Missing HaveCounts.
1229// - Incorrect mailbox counts.
1230// - Incorrect total message size.
1231// - Message with UID >= mailbox uid next.
1232// - Mailbox uidvalidity >= account uid validity.
1233// - ModSeq > 0, CreateSeq > 0, CreateSeq <= ModSeq.
1234// - All messages have a nonzero ThreadID, and no cycles in ThreadParentID, and parent messages the same ThreadParentIDs tail.
1235func (a *Account) CheckConsistency() error {
1236 var uidErrors []string // With a limit, could be many.
1237 var modseqErrors []string // With limit.
1238 var fileErrors []string // With limit.
1239 var threadidErrors []string // With limit.
1240 var threadParentErrors []string // With limit.
1241 var threadAncestorErrors []string // With limit.
1244 err := a.DB.Read(context.Background(), func(tx *bstore.Tx) error {
1245 nuv := NextUIDValidity{ID: 1}
1248 return fmt.Errorf("fetching next uid validity: %v", err)
1251 mailboxes := map[int64]Mailbox{}
1252 err = bstore.QueryTx[Mailbox](tx).ForEach(func(mb Mailbox) error {
1253 mailboxes[mb.ID] = mb
1255 if mb.UIDValidity >= nuv.Next {
1256 errmsg := fmt.Sprintf("mailbox %q (id %d) has uidvalidity %d >= account next uidvalidity %d", mb.Name, mb.ID, mb.UIDValidity, nuv.Next)
1257 errors = append(errors, errmsg)
1262 return fmt.Errorf("listing mailboxes: %v", err)
1265 counts := map[int64]MailboxCounts{}
1266 err = bstore.QueryTx[Message](tx).ForEach(func(m Message) error {
1267 mc := counts[m.MailboxID]
1268 mc.Add(m.MailboxCounts())
1269 counts[m.MailboxID] = mc
1271 mb := mailboxes[m.MailboxID]
1273 if (m.ModSeq == 0 || m.CreateSeq == 0 || m.CreateSeq > m.ModSeq) && len(modseqErrors) < 20 {
1274 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)
1275 modseqErrors = append(modseqErrors, modseqerr)
1277 if m.UID >= mb.UIDNext && len(uidErrors) < 20 {
1278 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)
1279 uidErrors = append(uidErrors, uiderr)
1284 p := a.MessagePath(m.ID)
1285 st, err := os.Stat(p)
1287 existserr := fmt.Sprintf("message %d in mailbox %q (id %d) on-disk file %s: %v", m.ID, mb.Name, mb.ID, p, err)
1288 fileErrors = append(fileErrors, existserr)
1289 } else if len(fileErrors) < 20 && m.Size != int64(len(m.MsgPrefix))+st.Size() {
1290 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())
1291 fileErrors = append(fileErrors, sizeerr)
1294 if m.ThreadID <= 0 && len(threadidErrors) < 20 {
1295 err := fmt.Sprintf("message %d in mailbox %q (id %d) has threadid 0", m.ID, mb.Name, mb.ID)
1296 threadidErrors = append(threadidErrors, err)
1298 if slices.Contains(m.ThreadParentIDs, m.ID) && len(threadParentErrors) < 20 {
1299 err := fmt.Sprintf("message %d in mailbox %q (id %d) references itself in threadparentids", m.ID, mb.Name, mb.ID)
1300 threadParentErrors = append(threadParentErrors, err)
1302 for i, pid := range m.ThreadParentIDs {
1303 am := Message{ID: pid}
1304 if err := tx.Get(&am); err == bstore.ErrAbsent {
1306 } else if err != nil {
1307 return fmt.Errorf("get ancestor message: %v", err)
1308 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) && len(threadAncestorErrors) < 20 {
1309 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)
1310 threadAncestorErrors = append(threadAncestorErrors, err)
1318 return fmt.Errorf("reading messages: %v", err)
1322 for _, mb := range mailboxes {
1323 totalSize += mb.Size
1325 errmsg := fmt.Sprintf("mailbox %q (id %d) does not have counts, should be %#v", mb.Name, mb.ID, counts[mb.ID])
1326 errors = append(errors, errmsg)
1327 } else if mb.MailboxCounts != counts[mb.ID] {
1328 mbcounterr := fmt.Sprintf("mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, counts[mb.ID])
1329 errors = append(errors, mbcounterr)
1333 du := DiskUsage{ID: 1}
1334 if err := tx.Get(&du); err != nil {
1335 return fmt.Errorf("get diskusage")
1337 if du.MessageSize != totalSize {
1338 errmsg := fmt.Sprintf("total message size in database is %d, sum of mailbox message sizes is %d", du.MessageSize, totalSize)
1339 errors = append(errors, errmsg)
1347 errors = append(errors, uidErrors...)
1348 errors = append(errors, modseqErrors...)
1349 errors = append(errors, fileErrors...)
1350 errors = append(errors, threadidErrors...)
1351 errors = append(errors, threadParentErrors...)
1352 errors = append(errors, threadAncestorErrors...)
1353 if len(errors) > 0 {
1354 return fmt.Errorf("%s", strings.Join(errors, "; "))
1359// Conf returns the configuration for this account if it still exists. During
1360// an SMTP session, a configuration update may drop an account.
1361func (a *Account) Conf() (config.Account, bool) {
1362 return mox.Conf.Account(a.Name)
1365// NextUIDValidity returns the next new/unique uidvalidity to use for this account.
1366func (a *Account) NextUIDValidity(tx *bstore.Tx) (uint32, error) {
1367 nuv := NextUIDValidity{ID: 1}
1368 if err := tx.Get(&nuv); err != nil {
1373 if err := tx.Update(&nuv); err != nil {
1379// NextModSeq returns the next modification sequence, which is global per account,
1381func (a *Account) NextModSeq(tx *bstore.Tx) (ModSeq, error) {
1382 v := SyncState{ID: 1}
1383 if err := tx.Get(&v); err == bstore.ErrAbsent {
1384 // We start assigning from modseq 2. Modseq 0 is not usable, so returned as 1, so
1386 // HighestDeletedModSeq is -1 so comparison against the default ModSeq zero value
1388 v = SyncState{1, 2, -1}
1389 return v.LastModSeq, tx.Insert(&v)
1390 } else if err != nil {
1394 return v.LastModSeq, tx.Update(&v)
1397func (a *Account) HighestDeletedModSeq(tx *bstore.Tx) (ModSeq, error) {
1398 v := SyncState{ID: 1}
1400 if err == bstore.ErrAbsent {
1403 return v.HighestDeletedModSeq, err
1406// WithWLock runs fn with account writelock held. Necessary for account/mailbox
1407// modification. For message delivery, a read lock is required.
1408func (a *Account) WithWLock(fn func()) {
1414// WithRLock runs fn with account read lock held. Needed for message delivery.
1415func (a *Account) WithRLock(fn func()) {
1421// DeliverMessage delivers a mail message to the account.
1423// The message, with msg.MsgPrefix and msgFile combined, must have a header
1424// section. The caller is responsible for adding a header separator to
1425// msg.MsgPrefix if missing from an incoming message.
1427// If the destination mailbox has the Sent special-use flag, the message is parsed
1428// for its recipients (to/cc/bcc). Their domains are added to Recipients for use in
1431// If sync is true, the message file and its directory are synced. Should be true
1432// for regular mail delivery, but can be false when importing many messages.
1434// If updateDiskUsage is true, the account total message size (for quota) is
1435// updated. Callers must check if a message can be added within quota before
1436// calling DeliverMessage.
1438// If CreateSeq/ModSeq is not set, it is assigned automatically.
1440// Must be called with account rlock or wlock.
1442// Caller must broadcast new message.
1444// Caller must update mailbox counts.
1445func (a *Account) DeliverMessage(log mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, sync, notrain, nothreads, updateDiskUsage bool) error {
1447 return fmt.Errorf("cannot deliver expunged message")
1450 mb := Mailbox{ID: m.MailboxID}
1451 if err := tx.Get(&mb); err != nil {
1452 return fmt.Errorf("get mailbox: %w", err)
1456 if err := tx.Update(&mb); err != nil {
1457 return fmt.Errorf("updating mailbox nextuid: %w", err)
1460 if updateDiskUsage {
1461 du := DiskUsage{ID: 1}
1462 if err := tx.Get(&du); err != nil {
1463 return fmt.Errorf("get disk usage: %v", err)
1465 du.MessageSize += m.Size
1466 if err := tx.Update(&du); err != nil {
1467 return fmt.Errorf("update disk usage: %v", err)
1472 m.JunkFlagsForMailbox(mb, conf)
1474 mr := FileMsgReader(m.MsgPrefix, msgFile) // We don't close, it would close the msgFile.
1475 var part *message.Part
1476 if m.ParsedBuf == nil {
1477 p, err := message.EnsurePart(log.Logger, false, mr, m.Size)
1479 log.Infox("parsing delivered message", err, slog.String("parse", ""), slog.Int64("message", m.ID))
1480 // We continue, p is still valid.
1483 buf, err := json.Marshal(part)
1485 return fmt.Errorf("marshal parsed message: %w", err)
1490 if err := json.Unmarshal(m.ParsedBuf, &p); err != nil {
1491 log.Errorx("unmarshal parsed message, continuing", err, slog.String("parse", ""))
1497 // If we are delivering to the originally intended mailbox, no need to store the mailbox ID again.
1498 if m.MailboxDestinedID != 0 && m.MailboxDestinedID == m.MailboxOrigID {
1499 m.MailboxDestinedID = 0
1501 if m.CreateSeq == 0 || m.ModSeq == 0 {
1502 modseq, err := a.NextModSeq(tx)
1504 return fmt.Errorf("assigning next modseq: %w", err)
1506 m.CreateSeq = modseq
1510 if part != nil && m.MessageID == "" && m.SubjectBase == "" {
1511 m.PrepareThreading(log, part)
1514 // Assign to thread (if upgrade has completed).
1515 noThreadID := nothreads
1516 if m.ThreadID == 0 && !nothreads && part != nil {
1518 case <-a.threadsCompleted:
1519 if a.threadsErr != nil {
1520 log.Info("not assigning threads for new delivery, upgrading to threads failed")
1523 if err := assignThread(log, tx, m, part); err != nil {
1524 return fmt.Errorf("assigning thread: %w", err)
1528 // note: since we have a write transaction to get here, we can't wait for the
1529 // thread upgrade to finish.
1530 // If we don't assign a threadid the upgrade process will do it.
1531 log.Info("not assigning threads for new delivery, upgrading to threads in progress which will assign this message")
1536 if err := tx.Insert(m); err != nil {
1537 return fmt.Errorf("inserting message: %w", err)
1539 if !noThreadID && m.ThreadID == 0 {
1541 if err := tx.Update(m); err != nil {
1542 return fmt.Errorf("updating message for its own thread id: %w", err)
1546 // 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.
1547 if mb.Sent && part != nil && part.Envelope != nil {
1556 addrs := append(append(e.To, e.CC...), e.BCC...)
1557 for _, addr := range addrs {
1558 if addr.User == "" {
1559 // Would trigger error because Recipient.Localpart must be nonzero. todo: we could allow empty localpart in db, and filter by not using FilterNonzero.
1560 log.Info("to/cc/bcc address with empty localpart, not inserting as recipient", slog.Any("address", addr))
1563 d, err := dns.ParseDomain(addr.Host)
1565 log.Debugx("parsing domain in to/cc/bcc address", err, slog.Any("address", addr))
1568 lp, err := smtp.ParseLocalpart(addr.User)
1570 log.Debugx("parsing localpart in to/cc/bcc address", err, slog.Any("address", addr))
1575 Localpart: lp.String(),
1577 OrgDomain: publicsuffix.Lookup(context.TODO(), log.Logger, d).Name(),
1580 if err := tx.Insert(&mr); err != nil {
1581 return fmt.Errorf("inserting sent message recipients: %w", err)
1586 msgPath := a.MessagePath(m.ID)
1587 msgDir := filepath.Dir(msgPath)
1588 os.MkdirAll(msgDir, 0770)
1590 // Sync file data to disk.
1592 if err := msgFile.Sync(); err != nil {
1593 return fmt.Errorf("fsync message file: %w", err)
1597 if err := moxio.LinkOrCopy(log, msgPath, msgFile.Name(), &moxio.AtReader{R: msgFile}, true); err != nil {
1598 return fmt.Errorf("linking/copying message to new file: %w", err)
1602 if err := moxio.SyncDir(log, msgDir); err != nil {
1603 xerr := os.Remove(msgPath)
1604 log.Check(xerr, "removing message after syncdir error", slog.String("path", msgPath))
1605 return fmt.Errorf("sync directory: %w", err)
1609 if !notrain && m.NeedsTraining() {
1611 if err := a.RetrainMessages(context.TODO(), log, tx, l, false); err != nil {
1612 xerr := os.Remove(msgPath)
1613 log.Check(xerr, "removing message after syncdir error", slog.String("path", msgPath))
1614 return fmt.Errorf("training junkfilter: %w", err)
1622// SetPassword saves a new password for this account. This password is used for
1623// IMAP, SMTP (submission) sessions and the HTTP account web page.
1625// Callers are responsible for checking if the account has NoCustomPassword set.
1626func (a *Account) SetPassword(log mlog.Log, password string) error {
1627 password, err := precis.OpaqueString.String(password)
1629 return fmt.Errorf(`password not allowed by "precis"`)
1632 if len(password) < 8 {
1633 // We actually check for bytes...
1634 return fmt.Errorf("password must be at least 8 characters long")
1637 hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1639 return fmt.Errorf("generating password hash: %w", err)
1642 err = a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1643 if _, err := bstore.QueryTx[Password](tx).Delete(); err != nil {
1644 return fmt.Errorf("deleting existing password: %v", err)
1647 pw.Hash = string(hash)
1649 // CRAM-MD5 calculates an HMAC-MD5, with the password as key, over a per-attempt
1650 // unique text that includes a timestamp. HMAC performs two hashes. Both times, the
1651 // first block is based on the key/password. We hash those first blocks now, and
1652 // store the hash state in the database. When we actually authenticate, we'll
1653 // complete the HMAC by hashing only the text. We cannot store crypto/hmac's hash,
1654 // because it does not expose its internal state and isn't a BinaryMarshaler.
1656 pw.CRAMMD5.Ipad = md5.New()
1657 pw.CRAMMD5.Opad = md5.New()
1658 key := []byte(password)
1663 ipad := make([]byte, md5.BlockSize)
1664 opad := make([]byte, md5.BlockSize)
1667 for i := range ipad {
1671 pw.CRAMMD5.Ipad.Write(ipad)
1672 pw.CRAMMD5.Opad.Write(opad)
1674 pw.SCRAMSHA1.Salt = scram.MakeRandom()
1675 pw.SCRAMSHA1.Iterations = 2 * 4096
1676 pw.SCRAMSHA1.SaltedPassword = scram.SaltPassword(sha1.New, password, pw.SCRAMSHA1.Salt, pw.SCRAMSHA1.Iterations)
1678 pw.SCRAMSHA256.Salt = scram.MakeRandom()
1679 pw.SCRAMSHA256.Iterations = 4096
1680 pw.SCRAMSHA256.SaltedPassword = scram.SaltPassword(sha256.New, password, pw.SCRAMSHA256.Salt, pw.SCRAMSHA256.Iterations)
1682 if err := tx.Insert(&pw); err != nil {
1683 return fmt.Errorf("inserting new password: %v", err)
1686 return sessionRemoveAll(context.TODO(), log, tx, a.Name)
1689 log.Info("new password set for account", slog.String("account", a.Name))
1694// SessionsClear invalidates all (web) login sessions for the account.
1695func (a *Account) SessionsClear(ctx context.Context, log mlog.Log) error {
1696 return a.DB.Write(ctx, func(tx *bstore.Tx) error {
1697 return sessionRemoveAll(ctx, log, tx, a.Name)
1701// Subjectpass returns the signing key for use with subjectpass for the given
1702// email address with canonical localpart.
1703func (a *Account) Subjectpass(email string) (key string, err error) {
1704 return key, a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
1705 v := Subjectpass{Email: email}
1711 if !errors.Is(err, bstore.ErrAbsent) {
1712 return fmt.Errorf("get subjectpass key from accounts database: %w", err)
1715 const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1716 buf := make([]byte, 16)
1717 if _, err := cryptorand.Read(buf); err != nil {
1720 for _, b := range buf {
1721 key += string(chars[int(b)%len(chars)])
1724 return tx.Insert(&v)
1728// Ensure mailbox is present in database, adding records for the mailbox and its
1729// parents if they aren't present.
1731// If subscribe is true, any mailboxes that were created will also be subscribed to.
1733// The leaf mailbox is created with special-use flags, taking the flags away from
1734// other mailboxes, and reflecting that in the returned changes.
1736// Caller must hold account wlock.
1737// Caller must propagate changes if any.
1738func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, specialUse SpecialUse) (mb Mailbox, changes []Change, rerr error) {
1739 if norm.NFC.String(name) != name {
1740 return Mailbox{}, nil, fmt.Errorf("mailbox name not normalized")
1743 // Quick sanity check.
1744 if strings.EqualFold(name, "inbox") && name != "Inbox" {
1745 return Mailbox{}, nil, fmt.Errorf("bad casing for inbox")
1748 elems := strings.Split(name, "/")
1749 q := bstore.QueryTx[Mailbox](tx)
1750 q.FilterFn(func(mb Mailbox) bool {
1751 return mb.Name == elems[0] || strings.HasPrefix(mb.Name, elems[0]+"/")
1755 return Mailbox{}, nil, fmt.Errorf("list mailboxes: %v", err)
1758 mailboxes := map[string]Mailbox{}
1759 for _, xmb := range l {
1760 mailboxes[xmb.Name] = xmb
1765 for _, elem := range elems {
1770 mb, existed = mailboxes[p]
1774 uidval, err := a.NextUIDValidity(tx)
1776 return Mailbox{}, nil, fmt.Errorf("next uid validity: %v", err)
1780 UIDValidity: uidval,
1784 err = tx.Insert(&mb)
1786 return Mailbox{}, nil, fmt.Errorf("creating new mailbox: %v", err)
1791 if tx.Get(&Subscription{p}) != nil {
1792 err := tx.Insert(&Subscription{p})
1794 return Mailbox{}, nil, fmt.Errorf("subscribing to mailbox: %v", err)
1797 flags = []string{`\Subscribed`}
1799 changes = append(changes, ChangeAddMailbox{mb, flags})
1802 // Clear any special-use flags from existing mailboxes and assign them to this mailbox.
1803 var zeroSpecialUse SpecialUse
1804 if !existed && specialUse != zeroSpecialUse {
1806 clearSpecialUse := func(b bool, fn func(*Mailbox) *bool) {
1807 if !b || qerr != nil {
1810 qs := bstore.QueryTx[Mailbox](tx)
1811 qs.FilterFn(func(xmb Mailbox) bool {
1814 xmb, err := qs.Get()
1815 if err == bstore.ErrAbsent {
1817 } else if err != nil {
1818 qerr = fmt.Errorf("looking up mailbox with special-use flag: %v", err)
1823 if err := tx.Update(&xmb); err != nil {
1824 qerr = fmt.Errorf("clearing special-use flag: %v", err)
1826 changes = append(changes, ChangeMailboxSpecialUse{xmb.ID, xmb.Name, xmb.SpecialUse})
1829 clearSpecialUse(specialUse.Archive, func(xmb *Mailbox) *bool { return &xmb.Archive })
1830 clearSpecialUse(specialUse.Draft, func(xmb *Mailbox) *bool { return &xmb.Draft })
1831 clearSpecialUse(specialUse.Junk, func(xmb *Mailbox) *bool { return &xmb.Junk })
1832 clearSpecialUse(specialUse.Sent, func(xmb *Mailbox) *bool { return &xmb.Sent })
1833 clearSpecialUse(specialUse.Trash, func(xmb *Mailbox) *bool { return &xmb.Trash })
1835 return Mailbox{}, nil, qerr
1838 mb.SpecialUse = specialUse
1839 if err := tx.Update(&mb); err != nil {
1840 return Mailbox{}, nil, fmt.Errorf("setting special-use flag for new mailbox: %v", err)
1842 changes = append(changes, ChangeMailboxSpecialUse{mb.ID, mb.Name, mb.SpecialUse})
1844 return mb, changes, nil
1847// MailboxExists checks if mailbox exists.
1848// Caller must hold account rlock.
1849func (a *Account) MailboxExists(tx *bstore.Tx, name string) (bool, error) {
1850 q := bstore.QueryTx[Mailbox](tx)
1851 q.FilterEqual("Name", name)
1855// MailboxFind finds a mailbox by name, returning a nil mailbox and nil error if mailbox does not exist.
1856func (a *Account) MailboxFind(tx *bstore.Tx, name string) (*Mailbox, error) {
1857 q := bstore.QueryTx[Mailbox](tx)
1858 q.FilterEqual("Name", name)
1860 if err == bstore.ErrAbsent {
1864 return nil, fmt.Errorf("looking up mailbox: %w", err)
1869// SubscriptionEnsure ensures a subscription for name exists. The mailbox does not
1870// have to exist. Any parents are not automatically subscribed.
1871// Changes are returned and must be broadcasted by the caller.
1872func (a *Account) SubscriptionEnsure(tx *bstore.Tx, name string) ([]Change, error) {
1873 if err := tx.Get(&Subscription{name}); err == nil {
1877 if err := tx.Insert(&Subscription{name}); err != nil {
1878 return nil, fmt.Errorf("inserting subscription: %w", err)
1881 q := bstore.QueryTx[Mailbox](tx)
1882 q.FilterEqual("Name", name)
1885 return []Change{ChangeAddSubscription{name, nil}}, nil
1886 } else if err != bstore.ErrAbsent {
1887 return nil, fmt.Errorf("looking up mailbox for subscription: %w", err)
1889 return []Change{ChangeAddSubscription{name, []string{`\NonExistent`}}}, nil
1892// MessageRuleset returns the first ruleset (if any) that matches the message
1893// represented by msgPrefix and msgFile, with smtp and validation fields from m.
1894func MessageRuleset(log mlog.Log, dest config.Destination, m *Message, msgPrefix []byte, msgFile *os.File) *config.Ruleset {
1895 if len(dest.Rulesets) == 0 {
1899 mr := FileMsgReader(msgPrefix, msgFile) // We don't close, it would close the msgFile.
1900 p, err := message.Parse(log.Logger, false, mr)
1902 log.Errorx("parsing message for evaluating rulesets, continuing with headers", err, slog.String("parse", ""))
1903 // note: part is still set.
1905 // todo optimize: only parse header if needed for rulesets. and probably reuse an earlier parsing.
1906 header, err := p.Header()
1908 log.Errorx("parsing message headers for evaluating rulesets, delivering to default mailbox", err, slog.String("parse", ""))
1909 // todo: reject message?
1914 for _, rs := range dest.Rulesets {
1915 if rs.SMTPMailFromRegexpCompiled != nil {
1916 if !rs.SMTPMailFromRegexpCompiled.MatchString(m.MailFrom) {
1920 if rs.MsgFromRegexpCompiled != nil {
1921 if m.MsgFromLocalpart == "" && m.MsgFromDomain == "" || !rs.MsgFromRegexpCompiled.MatchString(m.MsgFromLocalpart.String()+"@"+m.MsgFromDomain) {
1926 if !rs.VerifiedDNSDomain.IsZero() {
1927 d := rs.VerifiedDNSDomain.Name()
1929 matchDomain := func(s string) bool {
1930 return s == d || strings.HasSuffix(s, suffix)
1933 if m.EHLOValidated && matchDomain(m.EHLODomain) {
1936 if m.MailFromValidated && matchDomain(m.MailFromDomain) {
1939 for _, d := range m.DKIMDomains {
1951 for _, t := range rs.HeadersRegexpCompiled {
1952 for k, vl := range header {
1953 k = strings.ToLower(k)
1954 if !t[0].MatchString(k) {
1957 for _, v := range vl {
1958 v = strings.ToLower(strings.TrimSpace(v))
1959 if t[1].MatchString(v) {
1971// MessagePath returns the file system path of a message.
1972func (a *Account) MessagePath(messageID int64) string {
1973 return strings.Join(append([]string{a.Dir, "msg"}, messagePathElems(messageID)...), string(filepath.Separator))
1976// MessageReader opens a message for reading, transparently combining the
1977// message prefix with the original incoming message.
1978func (a *Account) MessageReader(m Message) *MsgReader {
1979 return &MsgReader{prefix: m.MsgPrefix, path: a.MessagePath(m.ID), size: m.Size}
1982// DeliverDestination delivers an email to dest, based on the configured rulesets.
1984// Returns ErrOverQuota when account would be over quota after adding message.
1986// Caller must hold account wlock (mailbox may be created).
1987// Message delivery, possible mailbox creation, and updated mailbox counts are
1989func (a *Account) DeliverDestination(log mlog.Log, dest config.Destination, m *Message, msgFile *os.File) error {
1991 rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
1993 mailbox = rs.Mailbox
1994 } else if dest.Mailbox == "" {
1997 mailbox = dest.Mailbox
1999 return a.DeliverMailbox(log, mailbox, m, msgFile)
2002// DeliverMailbox delivers an email to the specified mailbox.
2004// Returns ErrOverQuota when account would be over quota after adding message.
2006// Caller must hold account wlock (mailbox may be created).
2007// Message delivery, possible mailbox creation, and updated mailbox counts are
2009func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFile *os.File) error {
2010 var changes []Change
2011 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2012 if ok, _, err := a.CanAddMessageSize(tx, m.Size); err != nil {
2018 mb, chl, err := a.MailboxEnsure(tx, mailbox, true, SpecialUse{})
2020 return fmt.Errorf("ensuring mailbox: %w", err)
2023 m.MailboxOrigID = mb.ID
2025 // Update count early, DeliverMessage will update mb too and we don't want to fetch
2026 // it again before updating.
2027 mb.MailboxCounts.Add(m.MailboxCounts())
2028 if err := tx.Update(&mb); err != nil {
2029 return fmt.Errorf("updating mailbox for delivery: %w", err)
2032 if err := a.DeliverMessage(log, tx, m, msgFile, true, false, false, true); err != nil {
2036 changes = append(changes, chl...)
2037 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2040 // todo: if rename succeeded but transaction failed, we should remove the file.
2045 BroadcastChanges(a, changes)
2049// TidyRejectsMailbox removes old reject emails, and returns whether there is space for a new delivery.
2051// Caller most hold account wlock.
2052// Changes are broadcasted.
2053func (a *Account) TidyRejectsMailbox(log mlog.Log, rejectsMailbox string) (hasSpace bool, rerr error) {
2054 var changes []Change
2056 var remove []Message
2058 for _, m := range remove {
2059 p := a.MessagePath(m.ID)
2061 log.Check(err, "removing rejects message file", slog.String("path", p))
2065 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2066 mb, err := a.MailboxFind(tx, rejectsMailbox)
2068 return fmt.Errorf("finding mailbox: %w", err)
2071 // No messages have been delivered yet.
2076 // Gather old messages to remove.
2077 old := time.Now().Add(-14 * 24 * time.Hour)
2078 qdel := bstore.QueryTx[Message](tx)
2079 qdel.FilterNonzero(Message{MailboxID: mb.ID})
2080 qdel.FilterEqual("Expunged", false)
2081 qdel.FilterLess("Received", old)
2082 remove, err = qdel.List()
2084 return fmt.Errorf("listing old messages: %w", err)
2087 changes, err = a.rejectsRemoveMessages(context.TODO(), log, tx, mb, remove)
2089 return fmt.Errorf("removing messages: %w", err)
2092 // We allow up to n messages.
2093 qcount := bstore.QueryTx[Message](tx)
2094 qcount.FilterNonzero(Message{MailboxID: mb.ID})
2095 qcount.FilterEqual("Expunged", false)
2097 n, err := qcount.Count()
2099 return fmt.Errorf("counting rejects: %w", err)
2106 remove = nil // Don't remove files on failure.
2110 BroadcastChanges(a, changes)
2112 return hasSpace, nil
2115func (a *Account) rejectsRemoveMessages(ctx context.Context, log mlog.Log, tx *bstore.Tx, mb *Mailbox, l []Message) ([]Change, error) {
2119 ids := make([]int64, len(l))
2120 anyids := make([]any, len(l))
2121 for i, m := range l {
2126 // Remove any message recipients. Should not happen, but a user can move messages
2127 // from a Sent mailbox to the rejects mailbox...
2128 qdmr := bstore.QueryTx[Recipient](tx)
2129 qdmr.FilterEqual("MessageID", anyids...)
2130 if _, err := qdmr.Delete(); err != nil {
2131 return nil, fmt.Errorf("deleting from message recipient: %w", err)
2134 // Assign new modseq.
2135 modseq, err := a.NextModSeq(tx)
2137 return nil, fmt.Errorf("assign next modseq: %w", err)
2140 // Expunge the messages.
2141 qx := bstore.QueryTx[Message](tx)
2143 var expunged []Message
2144 qx.Gather(&expunged)
2145 if _, err := qx.UpdateNonzero(Message{ModSeq: modseq, Expunged: true}); err != nil {
2146 return nil, fmt.Errorf("expunging messages: %w", err)
2150 for _, m := range expunged {
2151 m.Expunged = false // Was set by update, but would cause wrong count.
2152 mb.MailboxCounts.Sub(m.MailboxCounts())
2155 if err := tx.Update(mb); err != nil {
2156 return nil, fmt.Errorf("updating mailbox counts: %w", err)
2158 if err := a.AddMessageSize(log, tx, -totalSize); err != nil {
2159 return nil, fmt.Errorf("updating disk usage: %w", err)
2162 // Mark as neutral and train so junk filter gets untrained with these (junk) messages.
2163 for i := range expunged {
2164 expunged[i].Junk = false
2165 expunged[i].Notjunk = false
2167 if err := a.RetrainMessages(ctx, log, tx, expunged, true); err != nil {
2168 return nil, fmt.Errorf("retraining expunged messages: %w", err)
2171 changes := make([]Change, len(l), len(l)+1)
2172 for i, m := range l {
2173 changes[i] = ChangeRemoveUIDs{mb.ID, []UID{m.UID}, modseq}
2175 changes = append(changes, mb.ChangeCounts())
2179// RejectsRemove removes a message from the rejects mailbox if present.
2180// Caller most hold account wlock.
2181// Changes are broadcasted.
2182func (a *Account) RejectsRemove(log mlog.Log, rejectsMailbox, messageID string) error {
2183 var changes []Change
2185 var remove []Message
2187 for _, m := range remove {
2188 p := a.MessagePath(m.ID)
2190 log.Check(err, "removing rejects message file", slog.String("path", p))
2194 err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
2195 mb, err := a.MailboxFind(tx, rejectsMailbox)
2197 return fmt.Errorf("finding mailbox: %w", err)
2203 q := bstore.QueryTx[Message](tx)
2204 q.FilterNonzero(Message{MailboxID: mb.ID, MessageID: messageID})
2205 q.FilterEqual("Expunged", false)
2206 remove, err = q.List()
2208 return fmt.Errorf("listing messages to remove: %w", err)
2211 changes, err = a.rejectsRemoveMessages(context.TODO(), log, tx, mb, remove)
2213 return fmt.Errorf("removing messages: %w", err)
2219 remove = nil // Don't remove files on failure.
2223 BroadcastChanges(a, changes)
2228// AddMessageSize adjusts the DiskUsage.MessageSize by size.
2229func (a *Account) AddMessageSize(log mlog.Log, tx *bstore.Tx, size int64) error {
2230 du := DiskUsage{ID: 1}
2231 if err := tx.Get(&du); err != nil {
2232 return fmt.Errorf("get diskusage: %v", err)
2234 du.MessageSize += size
2235 if du.MessageSize < 0 {
2236 log.Error("negative total message size", slog.Int64("delta", size), slog.Int64("newtotalsize", du.MessageSize))
2238 if err := tx.Update(&du); err != nil {
2239 return fmt.Errorf("update total message size: %v", err)
2244// QuotaMessageSize returns the effective maximum total message size for an
2245// account. Returns 0 if there is no maximum.
2246func (a *Account) QuotaMessageSize() int64 {
2248 size := conf.QuotaMessageSize
2250 size = mox.Conf.Static.QuotaMessageSize
2258// CanAddMessageSize checks if a message of size bytes can be added, depending on
2259// total message size and configured quota for account.
2260func (a *Account) CanAddMessageSize(tx *bstore.Tx, size int64) (ok bool, maxSize int64, err error) {
2261 maxSize = a.QuotaMessageSize()
2266 du := DiskUsage{ID: 1}
2267 if err := tx.Get(&du); err != nil {
2268 return false, maxSize, fmt.Errorf("get diskusage: %v", err)
2270 return du.MessageSize+size <= maxSize, maxSize, nil
2273// We keep a cache of recent successful authentications, so we don't have to bcrypt successful calls each time.
2274var authCache = struct {
2276 success map[authKey]string
2278 success: map[authKey]string{},
2281type authKey struct {
2285// StartAuthCache starts a goroutine that regularly clears the auth cache.
2286func StartAuthCache() {
2287 go manageAuthCache()
2290func manageAuthCache() {
2293 authCache.success = map[authKey]string{}
2295 time.Sleep(15 * time.Minute)
2299// OpenEmailAuth opens an account given an email address and password.
2301// The email address may contain a catchall separator.
2302// For invalid credentials, a nil account is returned, but accName may be
2304func OpenEmailAuth(log mlog.Log, email string, password string, checkLoginDisabled bool) (acc *Account, accName string, rerr error) {
2305 // We check for LoginDisabled after verifying the password. Otherwise users can get
2306 // messages about the account being disabled without knowing the password.
2307 acc, accName, _, rerr = OpenEmail(log, email, false)
2315 log.Check(err, "closing account after open auth failure")
2320 password, err := precis.OpaqueString.String(password)
2322 return nil, accName, ErrUnknownCredentials
2325 pw, err := bstore.QueryDB[Password](context.TODO(), acc.DB).Get()
2327 if err == bstore.ErrAbsent {
2328 return acc, accName, ErrUnknownCredentials
2330 return acc, accName, fmt.Errorf("looking up password: %v", err)
2333 ok := len(password) >= 8 && authCache.success[authKey{email, pw.Hash}] == password
2336 if err := bcrypt.CompareHashAndPassword([]byte(pw.Hash), []byte(password)); err != nil {
2337 return acc, accName, ErrUnknownCredentials
2340 if checkLoginDisabled {
2341 conf, aok := acc.Conf()
2343 return acc, accName, fmt.Errorf("cannot find config for account")
2344 } else if conf.LoginDisabled != "" {
2345 return acc, accName, fmt.Errorf("%w: %s", ErrLoginDisabled, conf.LoginDisabled)
2349 authCache.success[authKey{email, pw.Hash}] = password
2354// OpenEmail opens an account given an email address.
2356// The email address may contain a catchall separator.
2358// Returns account on success, may return non-empty account name even on error.
2359func OpenEmail(log mlog.Log, email string, checkLoginDisabled bool) (*Account, string, config.Destination, error) {
2360 addr, err := smtp.ParseAddress(email)
2362 return nil, "", config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
2364 accountName, _, _, dest, err := mox.LookupAddress(addr.Localpart, addr.Domain, false, false, false)
2365 if err != nil && (errors.Is(err, mox.ErrAddressNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
2366 return nil, accountName, config.Destination{}, ErrUnknownCredentials
2367 } else if err != nil {
2368 return nil, accountName, config.Destination{}, fmt.Errorf("looking up address: %v", err)
2370 acc, err := OpenAccount(log, accountName, checkLoginDisabled)
2372 return nil, accountName, config.Destination{}, err
2374 return acc, accountName, dest, nil
2377// 64 characters, must be power of 2 for MessagePath
2378const msgDirChars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
2380// MessagePath returns the filename of the on-disk filename, relative to the
2381// containing directory such as <account>/msg or queue.
2382// Returns names like "AB/1".
2383func MessagePath(messageID int64) string {
2384 return strings.Join(messagePathElems(messageID), string(filepath.Separator))
2387// messagePathElems returns the elems, for a single join without intermediate
2388// string allocations.
2389func messagePathElems(messageID int64) []string {
2390 v := messageID >> 13 // 8k files per directory.
2393 dir += string(msgDirChars[int(v)&(len(msgDirChars)-1)])
2399 return []string{dir, strconv.FormatInt(messageID, 10)}
2402// Set returns a copy of f, with each flag that is true in mask set to the
2404func (f Flags) Set(mask, flags Flags) Flags {
2405 set := func(d *bool, m, v bool) {
2411 set(&r.Seen, mask.Seen, flags.Seen)
2412 set(&r.Answered, mask.Answered, flags.Answered)
2413 set(&r.Flagged, mask.Flagged, flags.Flagged)
2414 set(&r.Forwarded, mask.Forwarded, flags.Forwarded)
2415 set(&r.Junk, mask.Junk, flags.Junk)
2416 set(&r.Notjunk, mask.Notjunk, flags.Notjunk)
2417 set(&r.Deleted, mask.Deleted, flags.Deleted)
2418 set(&r.Draft, mask.Draft, flags.Draft)
2419 set(&r.Phishing, mask.Phishing, flags.Phishing)
2420 set(&r.MDNSent, mask.MDNSent, flags.MDNSent)
2424// Changed returns a mask of flags that have been between f and other.
2425func (f Flags) Changed(other Flags) (mask Flags) {
2426 mask.Seen = f.Seen != other.Seen
2427 mask.Answered = f.Answered != other.Answered
2428 mask.Flagged = f.Flagged != other.Flagged
2429 mask.Forwarded = f.Forwarded != other.Forwarded
2430 mask.Junk = f.Junk != other.Junk
2431 mask.Notjunk = f.Notjunk != other.Notjunk
2432 mask.Deleted = f.Deleted != other.Deleted
2433 mask.Draft = f.Draft != other.Draft
2434 mask.Phishing = f.Phishing != other.Phishing
2435 mask.MDNSent = f.MDNSent != other.MDNSent
2439// Strings returns the flags that are set in their string form.
2440func (f Flags) Strings() []string {
2441 fields := []struct {
2445 {`$forwarded`, f.Forwarded},
2447 {`$mdnsent`, f.MDNSent},
2448 {`$notjunk`, f.Notjunk},
2449 {`$phishing`, f.Phishing},
2450 {`\answered`, f.Answered},
2451 {`\deleted`, f.Deleted},
2452 {`\draft`, f.Draft},
2453 {`\flagged`, f.Flagged},
2457 for _, fh := range fields {
2459 l = append(l, fh.word)
2465var systemWellKnownFlags = map[string]bool{
2478// ParseFlagsKeywords parses a list of textual flags into system/known flags, and
2479// other keywords. Keywords are lower-cased and sorted and check for valid syntax.
2480func ParseFlagsKeywords(l []string) (flags Flags, keywords []string, rerr error) {
2481 fields := map[string]*bool{
2482 `\answered`: &flags.Answered,
2483 `\flagged`: &flags.Flagged,
2484 `\deleted`: &flags.Deleted,
2485 `\seen`: &flags.Seen,
2486 `\draft`: &flags.Draft,
2487 `$junk`: &flags.Junk,
2488 `$notjunk`: &flags.Notjunk,
2489 `$forwarded`: &flags.Forwarded,
2490 `$phishing`: &flags.Phishing,
2491 `$mdnsent`: &flags.MDNSent,
2493 seen := map[string]bool{}
2494 for _, f := range l {
2495 f = strings.ToLower(f)
2496 if field, ok := fields[f]; ok {
2500 return Flags{}, nil, fmt.Errorf("duplicate keyword %s", f)
2503 if err := CheckKeyword(f); err != nil {
2504 return Flags{}, nil, fmt.Errorf("invalid keyword %s", f)
2506 keywords = append(keywords, f)
2510 sort.Strings(keywords)
2511 return flags, keywords, nil
2514// RemoveKeywords removes keywords from l, returning whether any modifications were
2515// made, and a slice, a new slice in case of modifications. Keywords must have been
2516// validated earlier, e.g. through ParseFlagKeywords or CheckKeyword. Should only
2517// be used with valid keywords, not with system flags like \Seen.
2518func RemoveKeywords(l, remove []string) ([]string, bool) {
2521 for _, k := range remove {
2522 if i := slices.Index(l, k); i >= 0 {
2524 l = append([]string{}, l...)
2527 copy(l[i:], l[i+1:])
2535// MergeKeywords adds keywords from add into l, returning whether it added any
2536// keyword, and the slice with keywords, a new slice if modifications were made.
2537// Keywords are only added if they aren't already present. Should only be used with
2538// keywords, not with system flags like \Seen.
2539func MergeKeywords(l, add []string) ([]string, bool) {
2542 for _, k := range add {
2543 if !slices.Contains(l, k) {
2545 l = append([]string{}, l...)
2558// CheckKeyword returns an error if kw is not a valid keyword. Kw should
2559// already be in lower-case.
2560func CheckKeyword(kw string) error {
2562 return fmt.Errorf("keyword cannot be empty")
2564 if systemWellKnownFlags[kw] {
2565 return fmt.Errorf("cannot use well-known flag as keyword")
2567 for _, c := range kw {
2569 if c <= ' ' || c > 0x7e || c >= 'A' && c <= 'Z' || strings.ContainsRune(`(){%*"\]`, c) {
2570 return errors.New(`not a valid keyword, must be lower-case ascii without spaces and without any of these characters: (){%*"\]`)
2576// SendLimitReached checks whether sending a message to recipients would reach
2577// the limit of outgoing messages for the account. If so, the message should
2578// not be sent. If the returned numbers are >= 0, the limit was reached and the
2579// values are the configured limits.
2581// To limit damage to the internet and our reputation in case of account
2582// compromise, we limit the max number of messages sent in a 24 hour window, both
2583// total number of messages and number of first-time recipients.
2584func (a *Account) SendLimitReached(tx *bstore.Tx, recipients []smtp.Path) (msglimit, rcptlimit int, rerr error) {
2586 msgmax := conf.MaxOutgoingMessagesPerDay
2588 // For human senders, 1000 recipients in a day is quite a lot.
2591 rcptmax := conf.MaxFirstTimeRecipientsPerDay
2593 // Human senders may address a new human-sized list of people once in a while. In
2594 // case of a compromise, a spammer will probably try to send to many new addresses.
2598 rcpts := map[string]time.Time{}
2600 err := bstore.QueryTx[Outgoing](tx).FilterGreater("Submitted", time.Now().Add(-24*time.Hour)).ForEach(func(o Outgoing) error {
2602 if rcpts[o.Recipient].IsZero() || o.Submitted.Before(rcpts[o.Recipient]) {
2603 rcpts[o.Recipient] = o.Submitted
2608 return -1, -1, fmt.Errorf("querying message recipients in past 24h: %w", err)
2610 if n+len(recipients) > msgmax {
2611 return msgmax, -1, nil
2614 // Only check if max first-time recipients is reached if there are enough messages
2615 // to trigger the limit.
2616 if n+len(recipients) < rcptmax {
2620 isFirstTime := func(rcpt string, before time.Time) (bool, error) {
2621 exists, err := bstore.QueryTx[Outgoing](tx).FilterNonzero(Outgoing{Recipient: rcpt}).FilterLess("Submitted", before).Exists()
2627 for _, r := range recipients {
2628 if first, err := isFirstTime(r.XString(true), now); err != nil {
2629 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
2634 for r, t := range rcpts {
2635 if first, err := isFirstTime(r, t); err != nil {
2636 return -1, -1, fmt.Errorf("checking whether recipient is first-time: %v", err)
2641 if firsttime > rcptmax {
2642 return -1, rcptmax, nil
2647// MailboxCreate creates a new mailbox, including any missing parent mailboxes,
2648// the total list of created mailboxes is returned in created. On success, if
2649// exists is false and rerr nil, the changes must be broadcasted by the caller.
2651// The mailbox is created with special-use flags, with those flags taken away from
2652// other mailboxes if they have them, reflected in the returned changes.
2654// Name must be in normalized form.
2655func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUse) (changes []Change, created []string, exists bool, rerr error) {
2656 elems := strings.Split(name, "/")
2658 for i, elem := range elems {
2663 exists, err := a.MailboxExists(tx, p)
2665 return nil, nil, false, fmt.Errorf("checking if mailbox exists")
2668 if i == len(elems)-1 {
2669 return nil, nil, true, fmt.Errorf("mailbox already exists")
2673 _, nchanges, err := a.MailboxEnsure(tx, p, true, specialUse)
2675 return nil, nil, false, fmt.Errorf("ensuring mailbox exists: %v", err)
2677 changes = append(changes, nchanges...)
2678 created = append(created, p)
2680 return changes, created, false, nil
2683// MailboxRename renames mailbox mbsrc to dst, and any missing parents for the
2684// destination, and any children of mbsrc and the destination.
2686// Names must be normalized and cannot be Inbox.
2687func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (changes []Change, isInbox, notExists, alreadyExists bool, rerr error) {
2688 if mbsrc.Name == "Inbox" || dst == "Inbox" {
2689 return nil, true, false, false, fmt.Errorf("inbox cannot be renamed")
2692 // We gather existing mailboxes that we need for deciding what to create/delete/update.
2693 q := bstore.QueryTx[Mailbox](tx)
2694 srcPrefix := mbsrc.Name + "/"
2695 dstRoot := strings.SplitN(dst, "/", 2)[0]
2696 dstRootPrefix := dstRoot + "/"
2697 q.FilterFn(func(mb Mailbox) bool {
2698 return mb.Name == mbsrc.Name || strings.HasPrefix(mb.Name, srcPrefix) || mb.Name == dstRoot || strings.HasPrefix(mb.Name, dstRootPrefix)
2700 q.SortAsc("Name") // We'll rename the parents before children.
2703 return nil, false, false, false, fmt.Errorf("listing relevant mailboxes: %v", err)
2706 mailboxes := map[string]Mailbox{}
2707 for _, mb := range l {
2708 mailboxes[mb.Name] = mb
2711 if _, ok := mailboxes[mbsrc.Name]; !ok {
2712 return nil, false, true, false, fmt.Errorf("mailbox does not exist")
2715 uidval, err := a.NextUIDValidity(tx)
2717 return nil, false, false, false, fmt.Errorf("next uid validity: %v", err)
2720 // Ensure parent mailboxes for the destination paths exist.
2722 dstElems := strings.Split(dst, "/")
2723 for i, elem := range dstElems[:len(dstElems)-1] {
2729 mb, ok := mailboxes[parent]
2737 UIDValidity: uidval,
2741 if err := tx.Insert(&mb); err != nil {
2742 return nil, false, false, false, fmt.Errorf("creating parent mailbox %q: %v", mb.Name, err)
2744 if err := tx.Get(&Subscription{Name: parent}); err != nil {
2745 if err := tx.Insert(&Subscription{Name: parent}); err != nil {
2746 return nil, false, false, false, fmt.Errorf("creating subscription for %q: %v", parent, err)
2749 changes = append(changes, ChangeAddMailbox{Mailbox: mb, Flags: []string{`\Subscribed`}})
2752 // Process src mailboxes, renaming them to dst.
2753 for _, srcmb := range l {
2754 if srcmb.Name != mbsrc.Name && !strings.HasPrefix(srcmb.Name, srcPrefix) {
2757 srcName := srcmb.Name
2758 dstName := dst + srcmb.Name[len(mbsrc.Name):]
2759 if _, ok := mailboxes[dstName]; ok {
2760 return nil, false, false, true, fmt.Errorf("destination mailbox %q already exists", dstName)
2763 srcmb.Name = dstName
2764 srcmb.UIDValidity = uidval
2765 if err := tx.Update(&srcmb); err != nil {
2766 return nil, false, false, false, fmt.Errorf("renaming mailbox: %v", err)
2769 var dstFlags []string
2770 if tx.Get(&Subscription{Name: dstName}) == nil {
2771 dstFlags = []string{`\Subscribed`}
2773 changes = append(changes, ChangeRenameMailbox{MailboxID: srcmb.ID, OldName: srcName, NewName: dstName, Flags: dstFlags})
2776 // If we renamed e.g. a/b to a/b/c/d, and a/b/c to a/b/c/d/c, we'll have to recreate a/b and a/b/c.
2777 srcElems := strings.Split(mbsrc.Name, "/")
2779 for i := 0; i < len(dstElems) && strings.HasPrefix(dst, xsrc+"/"); i++ {
2781 UIDValidity: uidval,
2786 if err := tx.Insert(&mb); err != nil {
2787 return nil, false, false, false, fmt.Errorf("creating mailbox at old path %q: %v", mb.Name, err)
2789 xsrc += "/" + dstElems[len(srcElems)+i]
2791 return changes, false, false, false, nil
2794// MailboxDelete deletes a mailbox by ID, including its annotations. If it has
2795// children, the return value indicates that and an error is returned.
2797// Caller should broadcast the changes and remove files for the removed message IDs.
2798func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx, mailbox Mailbox) (changes []Change, removeMessageIDs []int64, hasChildren bool, rerr error) {
2799 // Look for existence of child mailboxes. There is a lot of text in the IMAP RFCs about
2800 // NoInferior and NoSelect. We just require only leaf mailboxes are deleted.
2801 qmb := bstore.QueryTx[Mailbox](tx)
2802 mbprefix := mailbox.Name + "/"
2803 qmb.FilterFn(func(mb Mailbox) bool {
2804 return strings.HasPrefix(mb.Name, mbprefix)
2806 if childExists, err := qmb.Exists(); err != nil {
2807 return nil, nil, false, fmt.Errorf("checking if mailbox has child: %v", err)
2808 } else if childExists {
2809 return nil, nil, true, fmt.Errorf("mailbox has a child, only leaf mailboxes can be deleted")
2812 // todo jmap: instead of completely deleting a mailbox and its messages, we need to mark them all as expunged.
2814 qm := bstore.QueryTx[Message](tx)
2815 qm.FilterNonzero(Message{MailboxID: mailbox.ID})
2816 remove, err := qm.List()
2818 return nil, nil, false, fmt.Errorf("listing messages to remove: %v", err)
2821 if len(remove) > 0 {
2822 removeIDs := make([]any, len(remove))
2823 for i, m := range remove {
2826 qmr := bstore.QueryTx[Recipient](tx)
2827 qmr.FilterEqual("MessageID", removeIDs...)
2828 if _, err = qmr.Delete(); err != nil {
2829 return nil, nil, false, fmt.Errorf("removing message recipients for messages: %v", err)
2832 qm = bstore.QueryTx[Message](tx)
2833 qm.FilterNonzero(Message{MailboxID: mailbox.ID})
2834 if _, err := qm.Delete(); err != nil {
2835 return nil, nil, false, fmt.Errorf("removing messages: %v", err)
2839 for _, m := range remove {
2841 removeMessageIDs = append(removeMessageIDs, m.ID)
2845 if err := a.AddMessageSize(log, tx, -totalSize); err != nil {
2846 return nil, nil, false, fmt.Errorf("updating disk usage: %v", err)
2849 // Mark messages as not needing training. Then retrain them, so they are untrained if they were.
2852 for _, m := range remove {
2855 remove[o].Junk = false
2856 remove[o].Notjunk = false
2861 if err := a.RetrainMessages(ctx, log, tx, remove, true); err != nil {
2862 return nil, nil, false, fmt.Errorf("untraining deleted messages: %v", err)
2867 if _, err := bstore.QueryTx[Annotation](tx).FilterNonzero(Annotation{MailboxID: mailbox.ID}).Delete(); err != nil {
2868 return nil, nil, false, fmt.Errorf("removing annotations for mailbox: %v", err)
2870 // Not sending changes about annotations on this mailbox, since the entire mailbox
2871 // is being removed.
2873 if err := tx.Delete(&Mailbox{ID: mailbox.ID}); err != nil {
2874 return nil, nil, false, fmt.Errorf("removing mailbox: %v", err)
2876 return []Change{ChangeRemoveMailbox{MailboxID: mailbox.ID, Name: mailbox.Name}}, removeMessageIDs, false, nil
2879// CheckMailboxName checks if name is valid, returning an INBOX-normalized name.
2880// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
2881// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
2882// unicode-normalized, or when empty or has special characters.
2884// If name is the inbox, and allowInbox is false, this is indicated with the isInbox return parameter.
2885// For that case, and for other invalid names, an error is returned.
2886func CheckMailboxName(name string, allowInbox bool) (normalizedName string, isInbox bool, rerr error) {
2887 first := strings.SplitN(name, "/", 2)[0]
2888 if strings.EqualFold(first, "inbox") {
2889 if len(name) == len("inbox") && !allowInbox {
2890 return "", true, fmt.Errorf("special mailbox name Inbox not allowed")
2892 name = "Inbox" + name[len("Inbox"):]
2895 if norm.NFC.String(name) != name {
2896 return "", false, errors.New("non-unicode-normalized mailbox names not allowed")
2900 return "", false, errors.New("empty mailbox name")
2902 if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.Contains(name, "//") {
2903 return "", false, errors.New("bad slashes in mailbox name")
2906 // "%" and "*" are difficult to use with the IMAP LIST command, but we allow mostly
2908 if strings.HasPrefix(name, "#") {
2909 return "", false, errors.New("mailbox name cannot start with hash due to conflict with imap namespaces")
2912 // "#" and "&" are special in IMAP mailbox names. "#" for namespaces, "&" for
2915 for _, c := range name {
2917 if c <= 0x1f || c >= 0x7f && c <= 0x9f || c == 0x2028 || c == 0x2029 {
2918 return "", false, errors.New("control characters not allowed in mailbox name")
2921 return name, false, nil