when moving a message out of a Rejects mailbox, mark the message as "not seen" so stands out in the destination mailbox (e.g. inbox)

we set the flag both for move in imap and in webmail.

this also ensures the "MailboxDestinedID", used for per-mailbox reputation
analysis, is set in more reject-situations. before this change, some rejects
(such as based on DMARC reject) wouldn't result in reputation being used after
having been moved the message out of the rejects mailbox.

in the future, we need more tests for scenario's like this...

for issue #63 reported by x8x
may also help with issue #64
This commit is contained in:
Mechiel Lukkien 2023-09-22 15:43:25 +02:00
parent 2ec8c79e10
commit a0f3856e40
No known key found for this signature in database
3 changed files with 48 additions and 23 deletions

View file

@ -3378,9 +3378,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i) xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
} }
mc := m.MailboxCounts() mbSrc.Sub(m.MailboxCounts())
mbSrc.Sub(mc)
mbDst.Add(mc)
// Copy of message record that we'll insert when UID is freed up. // Copy of message record that we'll insert when UID is freed up.
om := *m om := *m
@ -3394,7 +3392,9 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
// is used for reputation calculation during future deliveries. // is used for reputation calculation during future deliveries.
m.MailboxOrigID = m.MailboxDestinedID m.MailboxOrigID = m.MailboxDestinedID
m.IsReject = false m.IsReject = false
m.Seen = false
} }
mbDst.Add(m.MailboxCounts())
m.UID = uidnext m.UID = uidnext
m.ModSeq = modseq m.ModSeq = modseq
m.JunkFlagsForMailbox(mbDst, conf) m.JunkFlagsForMailbox(mbDst, conf)

View file

@ -116,7 +116,49 @@ func analyze(ctx context.Context, log *mlog.Log, resolver dns.Resolver, d delive
log.Info("forwarded message, clearing identifying signals of forwarding mail server") log.Info("forwarded message, clearing identifying signals of forwarding mail server")
} }
assignMailbox := func(tx *bstore.Tx) error {
// Set message MailboxID to which mail will be delivered. Reputation is
// per-mailbox. If referenced mailbox is not found (e.g. does not yet exist), we
// can still determine a reputation because we also base it on outgoing
// messages and those are account-global.
mb, err := d.acc.MailboxFind(tx, mailbox)
if err != nil {
return fmt.Errorf("finding destination mailbox: %w", err)
}
if mb != nil {
// We want to deliver to mb.ID, but this message may be rejected and sent to the
// Rejects mailbox instead, with MailboxID overwritten. Record the ID in
// MailboxDestinedID too. If the message is later moved out of the Rejects mailbox,
// we'll adjust the MailboxOrigID so it gets taken into account during reputation
// calculating in future deliveries. If we end up delivering to the intended
// mailbox (i.e. not rejecting), MailboxDestinedID is cleared during delivery so we
// don't store it unnecessarily.
d.m.MailboxID = mb.ID
d.m.MailboxDestinedID = mb.ID
} else {
log.Debug("mailbox not found in database", mlog.Field("mailbox", mailbox))
}
return nil
}
reject := func(code int, secode string, errmsg string, err error, reason string) analysis { reject := func(code int, secode string, errmsg string, err error, reason string) analysis {
// We may have set MailboxDestinedID below already while we had a transaction. If
// not, do it now. This makes it possible to use the per-mailbox reputation when a
// user moves the message out of the Rejects mailbox to the intended mailbox
// (typically Inbox).
if d.m.MailboxDestinedID == 0 {
var mberr error
d.acc.WithRLock(func() {
mberr = d.acc.DB.Read(ctx, func(tx *bstore.Tx) error {
return assignMailbox(tx)
})
})
if mberr != nil {
return analysis{false, mailbox, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing", err, nil, nil, reasonReputationError}
}
d.m.MailboxID = 0 // We plan to reject, no need to set intended MailboxID.
}
accept := false accept := false
if rs != nil && rs.AcceptRejectsToMailbox != "" { if rs != nil && rs.AcceptRejectsToMailbox != "" {
accept = true accept = true
@ -203,26 +245,8 @@ func analyze(ctx context.Context, log *mlog.Log, resolver dns.Resolver, d delive
var err error var err error
d.acc.WithRLock(func() { d.acc.WithRLock(func() {
err = d.acc.DB.Read(ctx, func(tx *bstore.Tx) error { err = d.acc.DB.Read(ctx, func(tx *bstore.Tx) error {
// Set message MailboxID to which mail will be delivered. Reputation is if err := assignMailbox(tx); err != nil {
// per-mailbox. If referenced mailbox is not found (e.g. does not yet exist), we return err
// can still determine a reputation because we also base it on outgoing
// messages and those are account-global.
mb, err := d.acc.MailboxFind(tx, mailbox)
if err != nil {
return fmt.Errorf("finding destination mailbox: %w", err)
}
if mb != nil {
// We want to deliver to mb.ID, but this message may be rejected and sent to the
// Rejects mailbox instead, with MailboxID overwritten. Record the ID in
// MailboxDestinedID too. If the message is later moved out of the Rejects mailbox,
// we'll adjust the MailboxOrigID so it gets taken into account during reputation
// calculating in future deliveries. If we end up delivering to the intended
// mailbox (i.e. not rejecting), MailboxDestinedID is cleared during delivery so we
// don't store it unnecessarily.
d.m.MailboxID = mb.ID
d.m.MailboxDestinedID = mb.ID
} else {
log.Debug("mailbox not found in database", mlog.Field("mailbox", mailbox))
} }
isjunk, conclusive, method, err = reputation(tx, log, d.m) isjunk, conclusive, method, err = reputation(tx, log, d.m)

View file

@ -844,6 +844,7 @@ func (Webmail) MessageMove(ctx context.Context, messageIDs []int64, mailboxID in
// is used for reputation calculation during future deliveries. // is used for reputation calculation during future deliveries.
m.MailboxOrigID = m.MailboxDestinedID m.MailboxOrigID = m.MailboxDestinedID
m.IsReject = false m.IsReject = false
m.Seen = false
} }
m.UID = mbDst.UIDNext m.UID = mbDst.UIDNext
m.ModSeq = modseq m.ModSeq = modseq