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
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."`
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."`
// 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:"-"`
}
// 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.
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."`

View file

@ -359,9 +359,37 @@ describe-static" and "mox config describe-domains":
# E.g. Postmaster or Inbox.
Mailbox:
# 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)
# 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. (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:
-

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
"automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added
to the message metadata, causing later incoming messages from these recipients
to be accepted, unless other reputation signals prevent that.
If the destination mailbox is the Sent mailbox, the recipients of the messages
are added to the message metadata, causing later incoming messages from these
recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by
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
"automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added
to the message metadata, causing later incoming messages from these recipients
to be accepted, unless other reputation signals prevent that.
If the destination mailbox is the Sent mailbox, the recipients of the messages
are added to the message metadata, causing later incoming messages from these
recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by
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")
_, err = fmt.Fprint(mf, msg)
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")
err = mf.Close()
xcheckf(err, "closing file")
@ -335,7 +335,7 @@ Accounts:
xcheckf(err, "creating temp file for delivery")
_, err = fmt.Fprint(mf0, msg0)
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")
err = mf0.Close()
xcheckf(err, "closing file")
@ -362,7 +362,7 @@ Accounts:
xcheckf(err, "creating temp file for delivery")
_, err = fmt.Fprint(mf1, msg1)
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")
err = mf1.Close()
xcheckf(err, "closing file")

View file

@ -71,7 +71,11 @@ func TestListExtended(t *testing.T) {
}
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
}
var uidvalnext uint32 = 2

View file

@ -2741,8 +2741,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
err = tx.Update(&mb)
xcheckf(err, "updating mailbox counts")
isSent := name == "Sent"
err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, isSent, true, false)
err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, true, false)
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
"automatic junk flags" configuration is set, based on mailbox naming.
If the destination mailbox is "Sent", the recipients of the messages are added
to the message metadata, causing later incoming messages from these recipients
to be accepted, unless other reputation signals prevent that.
If the destination mailbox is the Sent mailbox, the recipients of the messages
are added to the message metadata, causing later incoming messages from these
recipients to be accepted, unless other reputation signals prevent that.
Users can also import mailboxes/messages through the account web page by
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.
const consumeFile = true
isSent := mailbox == "Sent"
const sync = false
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")
deliveredIDs = append(deliveredIDs, 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
}
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 {
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) {
var err error

View file

@ -73,7 +73,15 @@ var (
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 {
Salt []byte
@ -714,36 +722,79 @@ func initAccount(db *bstore.DB) error {
return db.Write(context.TODO(), func(tx *bstore.Tx) error {
uidvalidity := InitialUIDValidity()
mailboxes := InitialMailboxes
defaultMailboxes := mox.Conf.Static.DefaultMailboxes
if len(defaultMailboxes) > 0 {
mailboxes = []string{"Inbox"}
if len(mox.Conf.Static.DefaultMailboxes) > 0 {
// Deprecated in favor of InitialMailboxes.
defaultMailboxes := mox.Conf.Static.DefaultMailboxes
mailboxes := []string{"Inbox"}
for _, name := range defaultMailboxes {
if strings.EqualFold(name, "Inbox") {
continue
}
mailboxes = append(mailboxes, name)
}
}
for _, name := range mailboxes {
mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true}
if strings.HasPrefix(name, "Archive") {
mb.Archive = true
} else if strings.HasPrefix(name, "Drafts") {
mb.Draft = true
} else if strings.HasPrefix(name, "Junk") {
mb.Junk = true
} else if strings.HasPrefix(name, "Sent") {
mb.Sent = true
} else if strings.HasPrefix(name, "Trash") {
mb.Trash = true
for _, name := range mailboxes {
mb := Mailbox{Name: name, UIDValidity: uidvalidity, UIDNext: 1, HaveCounts: true}
if strings.HasPrefix(name, "Archive") {
mb.Archive = true
} else if strings.HasPrefix(name, "Drafts") {
mb.Draft = true
} else if strings.HasPrefix(name, "Junk") {
mb.Junk = true
} else if strings.HasPrefix(name, "Sent") {
mb.Sent = true
} else if strings.HasPrefix(name, "Trash") {
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 {
return fmt.Errorf("creating mailbox: %w", err)
} else {
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 {
return fmt.Errorf("adding subscription: %w", err)
add := func(name string, use SpecialUse) error {
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
// msg.MsgPrefix if missing from an incoming message.
//
// If isSent is true, the message is parsed for its recipients (to/cc/bcc). Their
// domains are added to Recipients for use in dmarc reputation.
// If the destination mailbox has the Sent special-use flag, the message is parsed
// 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
// 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 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 {
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.
if isSent {
if mb.Sent {
// Attempt to parse the message for its To/Cc/Bcc headers, which we insert into Recipient.
if part == nil {
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)
}
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
}

View file

@ -77,7 +77,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "sent mailbox")
msent.MailboxID = 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")
err = tx.Get(&mbsent)
@ -90,7 +90,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "insert rejects mailbox")
mreject.MailboxID = 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")
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 sync = false
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)
return
}

View file

@ -748,7 +748,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
err = tx.Update(&sentmb)
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 {
metricSubmission.WithLabelValues("storesenterror").Inc()
metricked = true