mirror of
https://github.com/mjl-/mox.git
synced 2025-07-10 09:54:40 +03:00
add suppression list for outgoing dmarc and tls reports
for reporting addresses that cause DSNs to be returned. that just adds noise. the admin can add/remove/extend addresses through the webadmin. in the future, we could send reports with a smtp mail from of "postmaster+<signed-encoded-recipient>@...", and add the reporting recipient on the suppression list automatically when a DSN comes in on that address, but for now this will probably do.
This commit is contained in:
@ -60,7 +60,7 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
EvalDBTypes = []any{Evaluation{}} // Types stored in DB.
|
||||
EvalDBTypes = []any{Evaluation{}, SuppressAddress{}} // Types stored in DB.
|
||||
// Exported for backups. For incoming deliveries the SMTP server adds evaluations
|
||||
// to the database. Every hour, a goroutine wakes up that gathers evaluations from
|
||||
// the last hour(s), sends a report, and removes the evaluations from the database.
|
||||
@ -119,6 +119,16 @@ type Evaluation struct {
|
||||
SPFResults []dmarcrpt.SPFAuthResult
|
||||
}
|
||||
|
||||
// SuppressAddress is a reporting address for which outgoing DMARC reports
|
||||
// will be suppressed for a period.
|
||||
type SuppressAddress struct {
|
||||
ID int64
|
||||
Inserted time.Time `bstore:"default now"`
|
||||
ReportingAddress string `bstore:"unique"`
|
||||
Until time.Time `bstore:"nonzero"`
|
||||
Comment string
|
||||
}
|
||||
|
||||
var dmarcResults = map[bool]dmarcrpt.DMARCResult{
|
||||
false: dmarcrpt.DMARCFail,
|
||||
true: dmarcrpt.DMARCPass,
|
||||
@ -803,6 +813,19 @@ Period: %s - %s UTC
|
||||
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
||||
var queued bool
|
||||
for _, rcpt := range recipients {
|
||||
// If recipient is on suppression list, we won't queue the reporting message.
|
||||
q := bstore.QueryDB[SuppressAddress](ctx, db)
|
||||
q.FilterNonzero(SuppressAddress{ReportingAddress: rcpt.address.Path().String()})
|
||||
q.FilterGreater("Until", time.Now())
|
||||
exists, err := q.Exists()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("querying suppress list: %v", err)
|
||||
}
|
||||
if exists {
|
||||
log.Info("suppressing outgoing dmarc aggregate report", mlog.Field("reportingaddress", rcpt.address))
|
||||
continue
|
||||
}
|
||||
|
||||
// Only send to addresses where we don't exceed their size limit. The RFC mentions
|
||||
// the size of the report, but then continues about the size after compression and
|
||||
// transport encodings (i.e. gzip and the mime base64 attachment, so the intention
|
||||
@ -818,7 +841,7 @@ Period: %s - %s UTC
|
||||
qm.MaxAttempts = 5
|
||||
qm.IsDMARCReport = true
|
||||
|
||||
err := queueAdd(ctx, log, &qm, msgf)
|
||||
err = queueAdd(ctx, log, &qm, msgf)
|
||||
if err != nil {
|
||||
tempError = true
|
||||
log.Errorx("queueing message with dmarc aggregate report", err)
|
||||
@ -831,7 +854,7 @@ Period: %s - %s UTC
|
||||
}
|
||||
|
||||
if !queued {
|
||||
if err := sendErrorReport(ctx, log, from, addrs, dom, report.ReportMetadata.ReportID, msgSize); err != nil {
|
||||
if err := sendErrorReport(ctx, log, db, from, addrs, dom, report.ReportMetadata.ReportID, msgSize); err != nil {
|
||||
log.Errorx("sending dmarc error reports", err)
|
||||
metricReportError.Inc()
|
||||
}
|
||||
@ -917,7 +940,7 @@ func composeAggregateReport(ctx context.Context, log *mlog.Log, mf *os.File, fro
|
||||
// Though this functionality is quite underspecified, we'll do our best to send our
|
||||
// an error report in case our report is too large for all recipients.
|
||||
// ../rfc/7489:1918
|
||||
func sendErrorReport(ctx context.Context, log *mlog.Log, fromAddr smtp.Address, recipients []message.NameAddress, reportDomain dns.Domain, reportID string, reportMsgSize int64) error {
|
||||
func sendErrorReport(ctx context.Context, log *mlog.Log, db *bstore.DB, fromAddr smtp.Address, recipients []message.NameAddress, reportDomain dns.Domain, reportID string, reportMsgSize int64) error {
|
||||
log.Debug("no reporting addresses willing to accept report given size, queuing short error message")
|
||||
|
||||
msgf, err := store.CreateMessageTemp("dmarcreportmsg-out")
|
||||
@ -954,6 +977,19 @@ Submitting-URI: %s
|
||||
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
||||
|
||||
for _, rcpt := range recipients {
|
||||
// If recipient is on suppression list, we won't queue the reporting message.
|
||||
q := bstore.QueryDB[SuppressAddress](ctx, db)
|
||||
q.FilterNonzero(SuppressAddress{ReportingAddress: rcpt.Address.Path().String()})
|
||||
q.FilterGreater("Until", time.Now())
|
||||
exists, err := q.Exists()
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying suppress list: %v", err)
|
||||
}
|
||||
if exists {
|
||||
log.Info("suppressing outgoing dmarc error report", mlog.Field("reportingaddress", rcpt.Address))
|
||||
continue
|
||||
}
|
||||
|
||||
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, fromAddr.Path(), rcpt.Address.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil)
|
||||
// Don't try as long as regular deliveries, and stop before we would send the
|
||||
// delayed DSN. Though we also won't send that due to IsDMARCReport.
|
||||
@ -1044,3 +1080,49 @@ func dkimSign(ctx context.Context, log *mlog.Log, fromAddr smtp.Address, smtputf
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SuppressAdd adds an address to the suppress list.
|
||||
func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
|
||||
db, err := evalDB(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Insert(ctx, ba)
|
||||
}
|
||||
|
||||
// SuppressList returns all reporting addresses on the suppress list.
|
||||
func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
|
||||
db, err := evalDB(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bstore.QueryDB[SuppressAddress](ctx, db).SortDesc("ID").List()
|
||||
}
|
||||
|
||||
// SuppressRemove removes a reporting address record from the suppress list.
|
||||
func SuppressRemove(ctx context.Context, id int64) error {
|
||||
db, err := evalDB(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Delete(ctx, &SuppressAddress{ID: id})
|
||||
}
|
||||
|
||||
// SuppressUpdate updates the until field of a reporting address record.
|
||||
func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
|
||||
db, err := evalDB(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ba := SuppressAddress{ID: id}
|
||||
err = db.Get(ctx, &ba)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ba.Until = until
|
||||
return db.Update(ctx, &ba)
|
||||
}
|
||||
|
Reference in New Issue
Block a user