mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).
Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.
Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.
The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.
Co-authored-by: Hanzo Dev <[email protected]>
171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
package queue
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/hanzoai/mail/mlog"
|
|
"github.com/hanzoai/mail/smtp"
|
|
"github.com/hanzoai/mail/webapi"
|
|
)
|
|
|
|
// todo: we should be processing spam complaints and add addresses to the list.
|
|
|
|
var errSuppressed = errors.New("address is on suppression list")
|
|
|
|
func baseAddress(a smtp.Path) smtp.Path {
|
|
s := string(a.Localpart)
|
|
s, _, _ = strings.Cut(s, "+")
|
|
s, _, _ = strings.Cut(s, "-")
|
|
s = strings.ReplaceAll(s, ".", "")
|
|
s = strings.ToLower(s)
|
|
return smtp.Path{Localpart: smtp.Localpart(s), IPDomain: a.IPDomain}
|
|
}
|
|
|
|
// SuppressionList returns suppression. If account is not empty, only suppression
|
|
// for that account are returned.
|
|
//
|
|
// SuppressionList does not check if an account exists.
|
|
func SuppressionList(ctx context.Context, account string) ([]webapi.Suppression, error) {
|
|
q := bstore.QueryDB[webapi.Suppression](ctx, DB)
|
|
if account != "" {
|
|
q.FilterNonzero(webapi.Suppression{Account: account})
|
|
}
|
|
return q.List()
|
|
}
|
|
|
|
// SuppressionLookup looks up a suppression for an address for an account. Returns
|
|
// a nil suppression if not found.
|
|
//
|
|
// SuppressionLookup does not check if an account exists.
|
|
func SuppressionLookup(ctx context.Context, account string, address smtp.Path) (*webapi.Suppression, error) {
|
|
baseAddr := baseAddress(address).XString(true)
|
|
q := bstore.QueryDB[webapi.Suppression](ctx, DB)
|
|
q.FilterNonzero(webapi.Suppression{Account: account, BaseAddress: baseAddr})
|
|
sup, err := q.Get()
|
|
if err == bstore.ErrAbsent {
|
|
return nil, nil
|
|
}
|
|
return &sup, err
|
|
}
|
|
|
|
// SuppressionAdd adds a suppression for an address for an account, setting
|
|
// BaseAddress based on OriginalAddress.
|
|
//
|
|
// If the base address of original address is already present, an error is
|
|
// returned (such as from bstore).
|
|
//
|
|
// SuppressionAdd does not check if an account exists.
|
|
func SuppressionAdd(ctx context.Context, originalAddress smtp.Path, sup *webapi.Suppression) error {
|
|
sup.BaseAddress = baseAddress(originalAddress).XString(true)
|
|
sup.OriginalAddress = originalAddress.XString(true)
|
|
return DB.Insert(ctx, sup)
|
|
}
|
|
|
|
// SuppressionRemove removes a suppression. The base address for the the given
|
|
// address is removed.
|
|
//
|
|
// SuppressionRemove does not check if an account exists.
|
|
func SuppressionRemove(ctx context.Context, account string, address smtp.Path) error {
|
|
baseAddr := baseAddress(address).XString(true)
|
|
q := bstore.QueryDB[webapi.Suppression](ctx, DB)
|
|
q.FilterNonzero(webapi.Suppression{Account: account, BaseAddress: baseAddr})
|
|
n, err := q.Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return bstore.ErrAbsent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type suppressionCheck struct {
|
|
MsgID int64
|
|
Account string
|
|
Recipient smtp.Path
|
|
Code int
|
|
Secode string
|
|
Source string
|
|
}
|
|
|
|
// process failures, possibly creating suppressions.
|
|
func suppressionProcess(log mlog.Log, tx *bstore.Tx, scl ...suppressionCheck) (suppressedMsgIDs []int64, err error) {
|
|
for _, sc := range scl {
|
|
xlog := log.With(slog.Any("suppressioncheck", sc))
|
|
baseAddr := baseAddress(sc.Recipient).XString(true)
|
|
exists, err := bstore.QueryTx[webapi.Suppression](tx).FilterNonzero(webapi.Suppression{Account: sc.Account, BaseAddress: baseAddr}).Exists()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checking if address is in suppression list: %v", err)
|
|
} else if exists {
|
|
xlog.Debug("address already in suppression list")
|
|
continue
|
|
}
|
|
|
|
origAddr := sc.Recipient.XString(true)
|
|
sup := webapi.Suppression{
|
|
Account: sc.Account,
|
|
BaseAddress: baseAddr,
|
|
OriginalAddress: origAddr,
|
|
}
|
|
|
|
if isImmedateBlock(sc.Code, sc.Secode) {
|
|
sup.Reason = fmt.Sprintf("delivery failure from %s with smtp code %d, enhanced code %q", sc.Source, sc.Code, sc.Secode)
|
|
} else {
|
|
// If two most recent deliveries failed (excluding this one, so three most recent
|
|
// messages including this one), we'll add the address to the list.
|
|
q := bstore.QueryTx[MsgRetired](tx)
|
|
q.FilterNonzero(MsgRetired{RecipientAddress: origAddr})
|
|
q.FilterNotEqual("ID", sc.MsgID)
|
|
q.SortDesc("LastActivity")
|
|
q.Limit(2)
|
|
l, err := q.List()
|
|
if err != nil {
|
|
xlog.Errorx("checking for previous delivery failures", err)
|
|
continue
|
|
}
|
|
if len(l) < 2 || l[0].Success || l[1].Success {
|
|
continue
|
|
}
|
|
sup.Reason = fmt.Sprintf("delivery failure from %s and three consecutive failures", sc.Source)
|
|
}
|
|
if err := tx.Insert(&sup); err != nil {
|
|
return nil, fmt.Errorf("inserting suppression: %v", err)
|
|
}
|
|
suppressedMsgIDs = append(suppressedMsgIDs, sc.MsgID)
|
|
}
|
|
return suppressedMsgIDs, nil
|
|
}
|
|
|
|
// Decide whether an SMTP code and short enhanced code is a reason for an
|
|
// immediate suppression listing. For some errors, we don't want to bother the
|
|
// remote mail server again, or they may decide our behaviour looks spammy.
|
|
func isImmedateBlock(code int, secode string) bool {
|
|
switch code {
|
|
case smtp.C521HostNoMail, // Host is not interested in accepting email at all.
|
|
smtp.C550MailboxUnavail, // Likely mailbox does not exist.
|
|
smtp.C551UserNotLocal, // Also not interested in accepting email for this address.
|
|
smtp.C553BadMailbox, // We are sending a mailbox name that server doesn't understand and won't accept email for.
|
|
smtp.C556DomainNoMail: // Remote is not going to accept email for this address/domain.
|
|
return true
|
|
}
|
|
if code/100 != 5 {
|
|
return false
|
|
}
|
|
switch secode {
|
|
case smtp.SeAddr1UnknownDestMailbox1, // Recipient localpart doesn't exist.
|
|
smtp.SeAddr1UnknownSystem2, // Bad recipient domain.
|
|
smtp.SeAddr1MailboxSyntax3, // Remote doesn't understand syntax.
|
|
smtp.SeAddr1DestMailboxMoved6, // Address no longer exists.
|
|
smtp.SeMailbox2Disabled1, // Account exists at remote, but is disabled.
|
|
smtp.SePol7DeliveryUnauth1: // Seems popular for saying we are on a blocklist.
|
|
return true
|
|
}
|
|
return false
|
|
}
|