remove last remnants of treating a mailbox named "Sent" specially, in favor of special-use mailbox flags

a few places still looked at the name "Sent". but since we have special-use
flags, we should always look at those. this also changes the config so admins
can specify different names for the special-use mailboxes to create for new
accounts, e.g. in a different language. the old config option is still
understood, just deprecated.
This commit is contained in:
Mechiel Lukkien 2023-08-09 09:31:23 +02:00
parent 19b819d222
commit 34ede1075d
No known key found for this signature in database
12 changed files with 176 additions and 52 deletions

View file

@ -57,7 +57,8 @@ type Static struct {
Account string Account string
Mailbox string `sconf-doc:"E.g. Postmaster or Inbox."` Mailbox string `sconf-doc:"E.g. Postmaster or Inbox."`
} `sconf-doc:"Destination for emails delivered to postmaster addresses: a plain 'postmaster' without domain, 'postmaster@<hostname>' (also for each listener with SMTP enabled), and as fallback for each domain without explicitly configured postmaster destination."` } `sconf-doc:"Destination for emails delivered to postmaster addresses: a plain 'postmaster' without domain, 'postmaster@<hostname>' (also for each listener with SMTP enabled), and as fallback for each domain without explicitly configured postmaster destination."`
DefaultMailboxes []string `sconf:"optional" sconf-doc:"Mailboxes to create when adding an account. Inbox is always created. If no mailboxes are specified, the following are automatically created: Sent, Archive, Trash, Drafts and Junk."` InitialMailboxes InitialMailboxes `sconf:"optional" sconf-doc:"Mailboxes to create for new accounts. Inbox is always created. Mailboxes can be given a 'special-use' role, which are understood by most mail clients. If absent/empty, the following mailboxes are created: Sent, Archive, Trash, Drafts and Junk."`
DefaultMailboxes []string `sconf:"optional" sconf-doc:"Deprecated in favor of InitialMailboxes. Mailboxes to create when adding an account. Inbox is always created. If no mailboxes are specified, the following are automatically created: Sent, Archive, Trash, Drafts and Junk."`
Transports map[string]Transport `sconf:"optional" sconf-doc:"Transport are mechanisms for delivering messages. Transports can be referenced from Routes in accounts, domains and the global configuration. There is always an implicit/fallback delivery transport doing direct delivery with SMTP from the outgoing message queue. Transports are typically only configured when using smarthosts, i.e. when delivering through another SMTP server. Zero or one transport methods must be set in a transport, never multiple. When using an external party to send email for a domain, keep in mind you may have to add their IP address to your domain's SPF record, and possibly additional DKIM records."` Transports map[string]Transport `sconf:"optional" sconf-doc:"Transport are mechanisms for delivering messages. Transports can be referenced from Routes in accounts, domains and the global configuration. There is always an implicit/fallback delivery transport doing direct delivery with SMTP from the outgoing message queue. Transports are typically only configured when using smarthosts, i.e. when delivering through another SMTP server. Zero or one transport methods must be set in a transport, never multiple. When using an external party to send email for a domain, keep in mind you may have to add their IP address to your domain's SPF record, and possibly additional DKIM records."`
// All IPs that were explicitly listen on for external SMTP. Only set when there // All IPs that were explicitly listen on for external SMTP. Only set when there
@ -74,6 +75,23 @@ type Static struct {
GID uint32 `sconf:"-" json:"-"` GID uint32 `sconf:"-" json:"-"`
} }
// InitialMailboxes are mailboxes created for a new account.
type InitialMailboxes struct {
SpecialUse SpecialUseMailboxes `sconf:"optional" sconf-doc:"Special-use roles to mailbox to create."`
Regular []string `sconf:"optional" sconf-doc:"Regular, non-special-use mailboxes to create."`
}
// SpecialUseMailboxes holds mailbox names for special-use roles. Mail clients
// recognize these special-use roles, e.g. appending sent messages to whichever
// mailbox has the Sent special-use flag.
type SpecialUseMailboxes struct {
Sent string `sconf:"optional"`
Archive string `sconf:"optional"`
Trash string `sconf:"optional"`
Draft string `sconf:"optional"`
Junk string `sconf:"optional"`
}
// Dynamic is the parsed form of domains.conf, and is automatically reloaded when changed. // Dynamic is the parsed form of domains.conf, and is automatically reloaded when changed.
type Dynamic struct { type Dynamic struct {
Domains map[string]Domain `sconf-doc:"Domains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."` Domains map[string]Domain `sconf-doc:"Domains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."`

View file

@ -359,9 +359,37 @@ describe-static" and "mox config describe-domains":
# E.g. Postmaster or Inbox. # E.g. Postmaster or Inbox.
Mailbox: Mailbox:
# Mailboxes to create when adding an account. Inbox is always created. If no # Mailboxes to create for new accounts. Inbox is always created. Mailboxes can be
# mailboxes are specified, the following are automatically created: Sent, Archive, # given a 'special-use' role, which are understood by most mail clients. If
# Trash, Drafts and Junk. (optional) # absent/empty, the following mailboxes are created: Sent, Archive, Trash, Drafts
# and Junk. (optional)
InitialMailboxes:
# Special-use roles to mailbox to create. (optional)
SpecialUse:
# (optional)
Sent:
# (optional)
Archive:
# (optional)
Trash:
# (optional)
Draft:
# (optional)
Junk:
# Regular, non-special-use mailboxes to create. (optional)
Regular:
-
# Deprecated in favor of InitialMailboxes. Mailboxes to create when adding an
# account. Inbox is always created. If no mailboxes are specified, the following
# are automatically created: Sent, Archive, Trash, Drafts and Junk. (optional)
DefaultMailboxes: DefaultMailboxes:
- -

12
doc.go
View file

@ -236,9 +236,9 @@ Import a maildir into an account.
By default, messages will train the junk filter based on their flags and, if By default, messages will train the junk filter based on their flags and, if
"automatic junk flags" configuration is set, based on mailbox naming. "automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added If the destination mailbox is the Sent mailbox, the recipients of the messages
to the message metadata, causing later incoming messages from these recipients are added to the message metadata, causing later incoming messages from these
to be accepted, unless other reputation signals prevent that. recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by Users can also import mailboxes/messages through the account web page by
uploading a zip or tgz file with mbox and/or maildirs. uploading a zip or tgz file with mbox and/or maildirs.
@ -260,9 +260,9 @@ Using mbox is not recommended, maildir is a better defined format.
By default, messages will train the junk filter based on their flags and, if By default, messages will train the junk filter based on their flags and, if
"automatic junk flags" configuration is set, based on mailbox naming. "automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added If the destination mailbox is the Sent mailbox, the recipients of the messages
to the message metadata, causing later incoming messages from these recipients are added to the message metadata, causing later incoming messages from these
to be accepted, unless other reputation signals prevent that. recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by Users can also import mailboxes/messages through the account web page by
uploading a zip or tgz file with mbox and/or maildirs. uploading a zip or tgz file with mbox and/or maildirs.

View file

@ -281,7 +281,7 @@ Accounts:
xcheckf(err, "creating temp file for delivery") xcheckf(err, "creating temp file for delivery")
_, err = fmt.Fprint(mf, msg) _, err = fmt.Fprint(mf, msg)
xcheckf(err, "writing deliver message to file") xcheckf(err, "writing deliver message to file")
err = accTest1.DeliverMessage(log, tx, &m, mf, true, false, false, true) err = accTest1.DeliverMessage(log, tx, &m, mf, true, false, true)
xcheckf(err, "add message to account test1") xcheckf(err, "add message to account test1")
err = mf.Close() err = mf.Close()
xcheckf(err, "closing file") xcheckf(err, "closing file")
@ -335,7 +335,7 @@ Accounts:
xcheckf(err, "creating temp file for delivery") xcheckf(err, "creating temp file for delivery")
_, err = fmt.Fprint(mf0, msg0) _, err = fmt.Fprint(mf0, msg0)
xcheckf(err, "writing deliver message to file") xcheckf(err, "writing deliver message to file")
err = accTest2.DeliverMessage(log, tx, &m0, mf0, true, false, false, false) err = accTest2.DeliverMessage(log, tx, &m0, mf0, true, false, false)
xcheckf(err, "add message to account test2") xcheckf(err, "add message to account test2")
err = mf0.Close() err = mf0.Close()
xcheckf(err, "closing file") xcheckf(err, "closing file")
@ -362,7 +362,7 @@ Accounts:
xcheckf(err, "creating temp file for delivery") xcheckf(err, "creating temp file for delivery")
_, err = fmt.Fprint(mf1, msg1) _, err = fmt.Fprint(mf1, msg1)
xcheckf(err, "writing deliver message to file") xcheckf(err, "writing deliver message to file")
err = accTest2.DeliverMessage(log, tx, &m1, mf1, true, true, false, false) err = accTest2.DeliverMessage(log, tx, &m1, mf1, true, false, false)
xcheckf(err, "add message to account test2") xcheckf(err, "add message to account test2")
err = mf1.Close() err = mf1.Close()
xcheckf(err, "closing file") xcheckf(err, "closing file")

View file

@ -71,7 +71,11 @@ func TestListExtended(t *testing.T) {
} }
uidvals := map[string]uint32{} uidvals := map[string]uint32{}
for _, name := range store.InitialMailboxes { use := store.DefaultInitialMailboxes.SpecialUse
for _, name := range []string{"Inbox", use.Archive, use.Draft, use.Junk, use.Sent, use.Trash} {
uidvals[name] = 1
}
for _, name := range store.DefaultInitialMailboxes.Regular {
uidvals[name] = 1 uidvals[name] = 1
} }
var uidvalnext uint32 = 2 var uidvalnext uint32 = 2

View file

@ -2741,8 +2741,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
err = tx.Update(&mb) err = tx.Update(&mb)
xcheckf(err, "updating mailbox counts") xcheckf(err, "updating mailbox counts")
isSent := name == "Sent" err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, true, false)
err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, isSent, true, false)
xcheckf(err, "delivering message") xcheckf(err, "delivering message")
}) })

View file

@ -30,9 +30,9 @@ import (
const importCommonHelp = `By default, messages will train the junk filter based on their flags and, if const importCommonHelp = `By default, messages will train the junk filter based on their flags and, if
"automatic junk flags" configuration is set, based on mailbox naming. "automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added If the destination mailbox is the Sent mailbox, the recipients of the messages
to the message metadata, causing later incoming messages from these recipients are added to the message metadata, causing later incoming messages from these
to be accepted, unless other reputation signals prevent that. recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by Users can also import mailboxes/messages through the account web page by
uploading a zip or tgz file with mbox and/or maildirs. uploading a zip or tgz file with mbox and/or maildirs.
@ -275,10 +275,9 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) {
// todo: possibly set dmarcdomain to the domain of the from address? at least for non-spams that have been seen. otherwise user would start without any reputations. the assumption would be that the user has accepted email and deemed it legit, coming from the indicated sender. // todo: possibly set dmarcdomain to the domain of the from address? at least for non-spams that have been seen. otherwise user would start without any reputations. the assumption would be that the user has accepted email and deemed it legit, coming from the indicated sender.
const consumeFile = true const consumeFile = true
isSent := mailbox == "Sent"
const sync = false const sync = false
const notrain = true const notrain = true
err := a.DeliverMessage(ctl.log, tx, m, mf, consumeFile, isSent, sync, notrain) err := a.DeliverMessage(ctl.log, tx, m, mf, consumeFile, sync, notrain)
ctl.xcheck(err, "delivering message") ctl.xcheck(err, "delivering message")
deliveredIDs = append(deliveredIDs, m.ID) deliveredIDs = append(deliveredIDs, m.ID)
ctl.log.Debug("delivered message", mlog.Field("id", m.ID)) ctl.log.Debug("delivered message", mlog.Field("id", m.ID))

View file

@ -644,9 +644,33 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
c.SpecifiedSMTPListenIPs = nil c.SpecifiedSMTPListenIPs = nil
} }
var zerouse config.SpecialUseMailboxes
if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
}
// DefaultMailboxes is deprecated.
for _, mb := range c.DefaultMailboxes { for _, mb := range c.DefaultMailboxes {
checkMailboxNormf(mb, "default mailbox") checkMailboxNormf(mb, "default mailbox")
} }
checkSpecialUseMailbox := func(nameOpt string) {
if nameOpt != "" {
checkMailboxNormf(nameOpt, "special-use initial mailbox")
if strings.EqualFold(nameOpt, "inbox") {
addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
}
}
}
checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
for _, name := range c.InitialMailboxes.Regular {
checkMailboxNormf(name, "regular initial mailbox")
if strings.EqualFold(name, "inbox") {
addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
}
}
checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) { checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
var err error var err error

View file

@ -73,7 +73,15 @@ var (
var subjectpassRand = mox.NewRand() var subjectpassRand = mox.NewRand()
var InitialMailboxes = []string{"Inbox", "Sent", "Archive", "Trash", "Drafts", "Junk"} var DefaultInitialMailboxes = config.InitialMailboxes{
SpecialUse: config.SpecialUseMailboxes{
Sent: "Sent",
Archive: "Archive",
Trash: "Trash",
Draft: "Drafts",
Junk: "Junk",
},
}
type SCRAM struct { type SCRAM struct {
Salt []byte Salt []byte
@ -714,36 +722,79 @@ func initAccount(db *bstore.DB) error {
return db.Write(context.TODO(), func(tx *bstore.Tx) error { return db.Write(context.TODO(), func(tx *bstore.Tx) error {
uidvalidity := InitialUIDValidity() uidvalidity := InitialUIDValidity()
mailboxes := InitialMailboxes if len(mox.Conf.Static.DefaultMailboxes) > 0 {
defaultMailboxes := mox.Conf.Static.DefaultMailboxes // Deprecated in favor of InitialMailboxes.
if len(defaultMailboxes) > 0 { defaultMailboxes := mox.Conf.Static.DefaultMailboxes
mailboxes = []string{"Inbox"} mailboxes := []string{"Inbox"}
for _, name := range defaultMailboxes { for _, name := range defaultMailboxes {
if strings.EqualFold(name, "Inbox") { if strings.EqualFold(name, "Inbox") {
continue continue
} }
mailboxes = append(mailboxes, name) mailboxes = append(mailboxes, name)
} }
} for _, name := range mailboxes {
for _, name := range mailboxes { mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true}
mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true} if strings.HasPrefix(name, "Archive") {
if strings.HasPrefix(name, "Archive") { mb.Archive = true
mb.Archive = true } else if strings.HasPrefix(name, "Drafts") {
} else if strings.HasPrefix(name, "Drafts") { mb.Draft = true
mb.Draft = true } else if strings.HasPrefix(name, "Junk") {
} else if strings.HasPrefix(name, "Junk") { mb.Junk = true
mb.Junk = true } else if strings.HasPrefix(name, "Sent") {
} else if strings.HasPrefix(name, "Sent") { mb.Sent = true
mb.Sent = true } else if strings.HasPrefix(name, "Trash") {
} else if strings.HasPrefix(name, "Trash") { mb.Trash = true
mb.Trash = true }
if err := tx.Insert(&mb); err != nil {
return fmt.Errorf("creating mailbox: %w", err)
}
if err := tx.Insert(&Subscription{name}); err != nil {
return fmt.Errorf("adding subscription: %w", err)
}
} }
if err := tx.Insert(&mb); err != nil { } else {
return fmt.Errorf("creating mailbox: %w", err) mailboxes := mox.Conf.Static.InitialMailboxes
var zerouse config.SpecialUseMailboxes
if mailboxes.SpecialUse == zerouse && len(mailboxes.Regular) == 0 {
mailboxes = DefaultInitialMailboxes
} }
if err := tx.Insert(&Subscription{name}); err != nil { add := func(name string, use SpecialUse) error {
return fmt.Errorf("adding subscription: %w", err) mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, SpecialUse: use, HaveCounts: true}
if err := tx.Insert(&mb); err != nil {
return fmt.Errorf("creating mailbox: %w", err)
}
if err := tx.Insert(&Subscription{name}); err != nil {
return fmt.Errorf("adding subscription: %w", err)
}
return nil
}
addSpecialOpt := func(nameOpt string, use SpecialUse) error {
if nameOpt == "" {
return nil
}
return add(nameOpt, use)
}
l := []struct {
nameOpt string
use SpecialUse
}{
{"Inbox", SpecialUse{}},
{mailboxes.SpecialUse.Archive, SpecialUse{Archive: true}},
{mailboxes.SpecialUse.Draft, SpecialUse{Draft: true}},
{mailboxes.SpecialUse.Junk, SpecialUse{Junk: true}},
{mailboxes.SpecialUse.Sent, SpecialUse{Sent: true}},
{mailboxes.SpecialUse.Trash, SpecialUse{Trash: true}},
}
for _, e := range l {
if err := addSpecialOpt(e.nameOpt, e.use); err != nil {
return err
}
}
for _, name := range mailboxes.Regular {
if err := add(name, SpecialUse{}); err != nil {
return err
}
} }
} }
@ -934,8 +985,9 @@ func (a *Account) WithRLock(fn func()) {
// section. The caller is responsible for adding a header separator to // section. The caller is responsible for adding a header separator to
// msg.MsgPrefix if missing from an incoming message. // msg.MsgPrefix if missing from an incoming message.
// //
// If isSent is true, the message is parsed for its recipients (to/cc/bcc). Their // If the destination mailbox has the Sent special-use flag, the message is parsed
// domains are added to Recipients for use in dmarc reputation. // for its recipients (to/cc/bcc). Their domains are added to Recipients for use in
// dmarc reputation.
// //
// If sync is true, the message file and its directory are synced. Should be true // If sync is true, the message file and its directory are synced. Should be true
// for regular mail delivery, but can be false when importing many messages. // for regular mail delivery, but can be false when importing many messages.
@ -947,7 +999,7 @@ func (a *Account) WithRLock(fn func()) {
// Caller must broadcast new message. // Caller must broadcast new message.
// //
// Caller must update mailbox counts. // Caller must update mailbox counts.
func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, consumeFile, isSent, sync, notrain bool) error { func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, consumeFile, sync, notrain bool) error {
if m.Expunged { if m.Expunged {
return fmt.Errorf("cannot deliver expunged message") return fmt.Errorf("cannot deliver expunged message")
} }
@ -999,7 +1051,7 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
} }
// todo: perhaps we should match the recipients based on smtp submission and a matching message-id? we now miss the addresses in bcc's. for webmail, we could insert the recipients directly. // todo: perhaps we should match the recipients based on smtp submission and a matching message-id? we now miss the addresses in bcc's. for webmail, we could insert the recipients directly.
if isSent { if mb.Sent {
// Attempt to parse the message for its To/Cc/Bcc headers, which we insert into Recipient. // Attempt to parse the message for its To/Cc/Bcc headers, which we insert into Recipient.
if part == nil { if part == nil {
var p message.Part var p message.Part
@ -1405,7 +1457,7 @@ func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgF
return fmt.Errorf("updating mailbox for delivery: %w", err) return fmt.Errorf("updating mailbox for delivery: %w", err)
} }
if err := a.DeliverMessage(log, tx, m, msgFile, consumeFile, mb.Sent, true, false); err != nil { if err := a.DeliverMessage(log, tx, m, msgFile, consumeFile, true, false); err != nil {
return err return err
} }

View file

@ -77,7 +77,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "sent mailbox") tcheck(t, err, "sent mailbox")
msent.MailboxID = mbsent.ID msent.MailboxID = mbsent.ID
msent.MailboxOrigID = mbsent.ID msent.MailboxOrigID = mbsent.ID
err = acc.DeliverMessage(xlog, tx, &msent, msgFile, false, true, true, false) err = acc.DeliverMessage(xlog, tx, &msent, msgFile, false, true, false)
tcheck(t, err, "deliver message") tcheck(t, err, "deliver message")
err = tx.Get(&mbsent) err = tx.Get(&mbsent)
@ -90,7 +90,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "insert rejects mailbox") tcheck(t, err, "insert rejects mailbox")
mreject.MailboxID = mbrejects.ID mreject.MailboxID = mbrejects.ID
mreject.MailboxOrigID = mbrejects.ID mreject.MailboxOrigID = mbrejects.ID
err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, false, false, true, false) err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, false, true, false)
tcheck(t, err, "deliver message") tcheck(t, err, "deliver message")
err = tx.Get(&mbrejects) err = tx.Get(&mbrejects)

View file

@ -531,7 +531,7 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
const consumeFile = true const consumeFile = true
const sync = false const sync = false
const notrain = true const notrain = true
if err := acc.DeliverMessage(log, tx, m, f, consumeFile, mb.Sent, sync, notrain); err != nil { if err := acc.DeliverMessage(log, tx, m, f, consumeFile, sync, notrain); err != nil {
problemf("delivering message %s: %s (continuing)", pos, err) problemf("delivering message %s: %s (continuing)", pos, err)
return return
} }

View file

@ -748,7 +748,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
err = tx.Update(&sentmb) err = tx.Update(&sentmb)
xcheckf(ctx, err, "updating sent mailbox for counts") xcheckf(ctx, err, "updating sent mailbox for counts")
err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, true, true, false) err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, true, false)
if err != nil { if err != nil {
metricSubmission.WithLabelValues("storesenterror").Inc() metricSubmission.WithLabelValues("storesenterror").Inc()
metricked = true metricked = true