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:
Mechiel Lukkien 2023-11-13 13:48:52 +01:00
parent 6ce69d5425
commit e24e1bee19
No known key found for this signature in database
12 changed files with 697 additions and 17 deletions

View file

@ -60,7 +60,7 @@ var (
) )
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 // Exported for backups. For incoming deliveries the SMTP server adds evaluations
// to the database. Every hour, a goroutine wakes up that gathers evaluations from // 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. // the last hour(s), sends a report, and removes the evaluations from the database.
@ -119,6 +119,16 @@ type Evaluation struct {
SPFResults []dmarcrpt.SPFAuthResult 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{ var dmarcResults = map[bool]dmarcrpt.DMARCResult{
false: dmarcrpt.DMARCFail, false: dmarcrpt.DMARCFail,
true: dmarcrpt.DMARCPass, true: dmarcrpt.DMARCPass,
@ -803,6 +813,19 @@ Period: %s - %s UTC
msgSize := int64(len(msgPrefix)) + msgInfo.Size() msgSize := int64(len(msgPrefix)) + msgInfo.Size()
var queued bool var queued bool
for _, rcpt := range recipients { 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 // 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 // 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 // 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.MaxAttempts = 5
qm.IsDMARCReport = true qm.IsDMARCReport = true
err := queueAdd(ctx, log, &qm, msgf) err = queueAdd(ctx, log, &qm, msgf)
if err != nil { if err != nil {
tempError = true tempError = true
log.Errorx("queueing message with dmarc aggregate report", err) log.Errorx("queueing message with dmarc aggregate report", err)
@ -831,7 +854,7 @@ Period: %s - %s UTC
} }
if !queued { 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) log.Errorx("sending dmarc error reports", err)
metricReportError.Inc() 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 // 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. // an error report in case our report is too large for all recipients.
// ../rfc/7489:1918 // ../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") log.Debug("no reporting addresses willing to accept report given size, queuing short error message")
msgf, err := store.CreateMessageTemp("dmarcreportmsg-out") msgf, err := store.CreateMessageTemp("dmarcreportmsg-out")
@ -954,6 +977,19 @@ Submitting-URI: %s
msgSize := int64(len(msgPrefix)) + msgInfo.Size() msgSize := int64(len(msgPrefix)) + msgInfo.Size()
for _, rcpt := range recipients { 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) 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 // 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. // 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 "" 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)
}

View file

@ -312,7 +312,7 @@ func TestSendReports(t *testing.T) {
if optExpReport != nil { if optExpReport != nil {
// Parse report in message and compare with expected. // Parse report in message and compare with expected.
expFeedback.ReportMetadata.ReportID = feedback.ReportMetadata.ReportID optExpReport.ReportMetadata.ReportID = feedback.ReportMetadata.ReportID
tcompare(t, feedback, expFeedback) tcompare(t, feedback, expFeedback)
} }
@ -348,6 +348,18 @@ func TestSendReports(t *testing.T) {
evalOpt.Optional = true evalOpt.Optional = true
test([]Evaluation{evalOpt}, map[string]struct{}{}, map[string]struct{}{}, nil) test([]Evaluation{evalOpt}, map[string]struct{}{}, map[string]struct{}{}, nil)
// Address is suppressed.
sa := SuppressAddress{ReportingAddress: "dmarcrpt@sender.example", Until: time.Now().Add(time.Minute)}
err = db.Insert(ctxbg, &sa)
tcheckf(t, err, "insert suppress address")
test([]Evaluation{eval}, map[string]struct{}{}, map[string]struct{}{}, nil)
// Suppression has expired.
sa.Until = time.Now().Add(-time.Minute)
err = db.Update(ctxbg, &sa)
tcheckf(t, err, "update suppress address")
test([]Evaluation{eval}, map[string]struct{}{"dmarcrpt@sender.example": {}}, map[string]struct{}{}, expFeedback)
// Two RUA's, one with a size limit that doesn't pass, and one that does pass. // Two RUA's, one with a size limit that doesn't pass, and one that does pass.
resolver.TXT["_dmarc.sender.example."] = []string{"v=DMARC1; rua=mailto:dmarcrpt1@sender.example!1,mailto:dmarcrpt2@sender.example!10t; ri=3600"} resolver.TXT["_dmarc.sender.example."] = []string{"v=DMARC1; rua=mailto:dmarcrpt1@sender.example!1,mailto:dmarcrpt2@sender.example!10t; ri=3600"}
test([]Evaluation{eval}, map[string]struct{}{"dmarcrpt2@sender.example": {}}, map[string]struct{}{}, nil) test([]Evaluation{eval}, map[string]struct{}{"dmarcrpt2@sender.example": {}}, map[string]struct{}{}, nil)

View file

@ -605,6 +605,20 @@ func deliver(resolver dns.Resolver, m Msg) {
now := time.Now() now := time.Now()
dayUTC := now.UTC().Format("20060102") dayUTC := now.UTC().Format("20060102")
// See if this contains a failure. If not, we'll mark TLS results for delivering
// DMARC reports SendReport false, so we won't as easily get into a report sending
// loop.
var failure bool
for _, result := range hostResults {
if result.Summary.TotalFailureSessionCount > 0 {
failure = true
break
}
}
if recipientDomainResult.Summary.TotalFailureSessionCount > 0 {
failure = true
}
results := make([]tlsrptdb.TLSResult, 0, 1+len(hostResults)) results := make([]tlsrptdb.TLSResult, 0, 1+len(hostResults))
tlsaPolicyDomains := map[string]bool{} tlsaPolicyDomains := map[string]bool{}
addResult := func(r tlsrpt.Result, isHost bool) { addResult := func(r tlsrpt.Result, isHost bool) {
@ -629,7 +643,7 @@ func deliver(resolver dns.Resolver, m Msg) {
DayUTC: dayUTC, DayUTC: dayUTC,
RecipientDomain: m.RecipientDomain.Domain.Name(), RecipientDomain: m.RecipientDomain.Domain.Name(),
IsHost: isHost, IsHost: isHost,
SendReport: !m.IsTLSReport, SendReport: !m.IsTLSReport && (!m.IsDMARCReport || failure),
Results: []tlsrpt.Result{r}, Results: []tlsrpt.Result{r},
} }
results = append(results, tlsResult) results = append(results, tlsResult)

View file

@ -985,13 +985,12 @@ func TestTLSReport(t *testing.T) {
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver) ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
defer ts.close() defer ts.close()
run := func(tlsrpt string, n int) { run := func(rcptTo, tlsrpt string, n int) {
t.Helper() t.Helper()
ts.run(func(err error, client *smtpclient.Client) { ts.run(func(err error, client *smtpclient.Client) {
t.Helper() t.Helper()
mailFrom := "remote@example.org" mailFrom := "remote@example.org"
rcptTo := "mjl@mox.example"
msgb := &bytes.Buffer{} msgb := &bytes.Buffer{}
_, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: tlsrpt report\r\nMIME-Version: 1.0\r\nContent-Type: application/tlsrpt+json\r\n\r\n%s\r\n", mailFrom, rcptTo, tlsrpt) _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: tlsrpt report\r\nMIME-Version: 1.0\r\nContent-Type: application/tlsrpt+json\r\n\r\n%s\r\n", mailFrom, rcptTo, tlsrpt)
@ -1017,13 +1016,15 @@ func TestTLSReport(t *testing.T) {
const tlsrpt = `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}` const tlsrpt = `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}`
run(tlsrpt, 0) run("mjl@mox.example", tlsrpt, 0)
run(strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1) run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
// We always store as an evaluation, but as optional for reports. // We always store as an evaluation, but as optional for reports.
evals := checkEvaluationCount(t, 2) evals := checkEvaluationCount(t, 3)
tcompare(t, evals[0].Optional, true) tcompare(t, evals[0].Optional, true)
tcompare(t, evals[1].Optional, true) tcompare(t, evals[1].Optional, true)
tcompare(t, evals[2].Optional, true)
} }
func TestRatelimitConnectionrate(t *testing.T) { func TestRatelimitConnectionrate(t *testing.T) {

View file

@ -1,9 +1,13 @@
DataDir: ../data DataDir: ../data
User: 1000 User: 1000
LogLevel: trace LogLevel: trace
Hostname: mox.example Hostname: mailhost.mox.example
Postmaster: Postmaster:
Account: mjl Account: mjl
Mailbox: postmaster Mailbox: postmaster
Listeners: Listeners:
local: nil local: nil
HostTLSRPT:
Account: mjl
Mailbox: TLSRPT
Localpart: mjl

View file

@ -17,7 +17,7 @@ var (
mutex sync.Mutex mutex sync.Mutex
// Accessed directly by tlsrptsend. // Accessed directly by tlsrptsend.
ResultDBTypes = []any{TLSResult{}} ResultDBTypes = []any{TLSResult{}, TLSRPTSuppressAddress{}}
ResultDB *bstore.DB ResultDB *bstore.DB
) )

View file

@ -51,6 +51,18 @@ type TLSResult struct {
Results []tlsrpt.Result Results []tlsrpt.Result
} }
// todo: TLSRPTSuppressAddress should be named just SuppressAddress, but would clash with dmarcdb.SuppressAddress in sherpa api.
// TLSRPTSuppressAddress is a reporting address for which outgoing TLS reports
// will be suppressed for a period.
type TLSRPTSuppressAddress struct {
ID int64
Inserted time.Time `bstore:"default now"`
ReportingAddress string `bstore:"unique"`
Until time.Time `bstore:"nonzero"`
Comment string
}
func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) { func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
@ -159,3 +171,49 @@ func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, day
_, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete() _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
return err return err
} }
// SuppressAdd adds an address to the suppress list.
func SuppressAdd(ctx context.Context, ba *TLSRPTSuppressAddress) error {
db, err := resultDB(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) ([]TLSRPTSuppressAddress, error) {
db, err := resultDB(ctx)
if err != nil {
return nil, err
}
return bstore.QueryDB[TLSRPTSuppressAddress](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 := resultDB(ctx)
if err != nil {
return err
}
return db.Delete(ctx, &TLSRPTSuppressAddress{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 := resultDB(ctx)
if err != nil {
return err
}
ba := TLSRPTSuppressAddress{ID: id}
err = db.Get(ctx, &ba)
if err != nil {
return err
}
ba.Until = until
return db.Update(ctx, &ba)
}

View file

@ -441,6 +441,19 @@ Period: %s - %s UTC
msgSize := int64(len(msgPrefix)) + msgInfo.Size() msgSize := int64(len(msgPrefix)) + msgInfo.Size()
for _, rcpt := range recipients { for _, rcpt := range recipients {
// If recipient is on suppression list, we won't queue the reporting message.
q := bstore.QueryDB[tlsrptdb.TLSRPTSuppressAddress](ctx, db)
q.FilterNonzero(tlsrptdb.TLSRPTSuppressAddress{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 tls report", mlog.Field("reportingaddress", rcpt.Address))
continue
}
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, from.Path(), rcpt.Address.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil) qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, from.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 // 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 IsTLSReport. // delayed DSN. Though we also won't send that due to IsTLSReport.
@ -451,7 +464,7 @@ Period: %s - %s UTC
no := false no := false
qm.RequireTLS = &no qm.RequireTLS = &no
err := queueAdd(ctx, log, &qm, msgf) err = queueAdd(ctx, log, &qm, msgf)
if err != nil { if err != nil {
tempError = true tempError = true
log.Errorx("queueing message with tls report", err) log.Errorx("queueing message with tls report", err)

View file

@ -381,4 +381,14 @@ func TestSendReports(t *testing.T) {
"tls-reports3@mailhost.sender.example": report2, "tls-reports3@mailhost.sender.example": report2,
} }
test(tlsResults, expReports) test(tlsResults, expReports)
db.Insert(ctxbg,
&tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: "tls-reports@sender.example", Until: time.Now().Add(-time.Minute)}, // Expired, so ignored.
&tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: "tls-reports1@mailhost.sender.example", Until: time.Now().Add(time.Minute)}, // Still valid.
&tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: "tls-reports3@mailhost.sender.example", Until: time.Now().Add(31 * 24 * time.Hour)}, // Still valid.
)
test(tlsResults, map[string]tlsrpt.Report{
"tls-reports@sender.example": report1,
"tls-reports2@mailhost.sender.example": report2,
})
} }

View file

@ -2031,6 +2031,36 @@ func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
xcheckf(ctx, err, "removing evaluations for domain") xcheckf(ctx, err, "removing evaluations for domain")
} }
// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
// reports will be suppressed for a period.
func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
addr, err := smtp.ParseAddress(reportingAddress)
xcheckuserf(ctx, err, "parsing reporting address")
ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
err = dmarcdb.SuppressAdd(ctx, &ba)
xcheckf(ctx, err, "adding address to suppresslist")
}
// DMARCSuppressList returns all reporting addresses on the suppress list.
func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
l, err := dmarcdb.SuppressList(ctx)
xcheckf(ctx, err, "listing reporting addresses in suppresslist")
return l
}
// DMARCSuppressRemove removes a reporting address record from the suppress list.
func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
err := dmarcdb.SuppressRemove(ctx, id)
xcheckf(ctx, err, "removing reporting address from suppresslist")
}
// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
err := dmarcdb.SuppressUpdate(ctx, id, until)
xcheckf(ctx, err, "updating reporting address in suppresslist")
}
// TLSRPTResults returns all TLSRPT results in the database. // TLSRPTResults returns all TLSRPT results in the database.
func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult { func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
results, err := tlsrptdb.Results(ctx) results, err := tlsrptdb.Results(ctx)
@ -2078,3 +2108,33 @@ func (Admin) TLSRPTRemoveResults(ctx context.Context, domain string, day string)
err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day) err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
xcheckf(ctx, err, "removing tls results") xcheckf(ctx, err, "removing tls results")
} }
// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
// reports will be suppressed for a period.
func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
addr, err := smtp.ParseAddress(reportingAddress)
xcheckuserf(ctx, err, "parsing reporting address")
ba := tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
err = tlsrptdb.SuppressAdd(ctx, &ba)
xcheckf(ctx, err, "adding address to suppresslist")
}
// TLSRPTSuppressList returns all reporting addresses on the suppress list.
func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.TLSRPTSuppressAddress {
l, err := tlsrptdb.SuppressList(ctx)
xcheckf(ctx, err, "listing reporting addresses in suppresslist")
return l
}
// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
err := tlsrptdb.SuppressRemove(ctx, id)
xcheckf(ctx, err, "removing reporting address from suppresslist")
}
// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
err := tlsrptdb.SuppressUpdate(ctx, id, until)
xcheckf(ctx, err, "updating reporting address in suppresslist")
}

View file

@ -1023,7 +1023,7 @@ const dmarcIndex = async () => {
dom._kids(page, dom._kids(page,
crumbs( crumbs(
crumblink('Mox Admin', '#'), crumblink('Mox Admin', '#'),
'DMARC reports and evaluations', 'DMARC',
), ),
dom.ul( dom.ul(
dom.li( dom.li(
@ -1085,7 +1085,10 @@ const renderDMARCSummaries = (summaries) => {
} }
const dmarcEvaluations = async () => { const dmarcEvaluations = async () => {
const evalStats = await api.DMARCEvaluationStats() const [evalStats, suppressAddresses] = await Promise.all([
api.DMARCEvaluationStats(),
api.DMARCSuppressList(),
])
const isEmpty = (o) => { const isEmpty = (o) => {
for (const e in o) { for (const e in o) {
@ -1094,6 +1097,9 @@ const dmarcEvaluations = async () => {
return true return true
} }
let fieldset, reportingAddress, until, comment
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
const page = document.getElementById('page') const page = document.getElementById('page')
dom._kids(page, dom._kids(page,
crumbs( crumbs(
@ -1121,6 +1127,101 @@ const dmarcEvaluations = async () => {
isEmpty(evalStats) ? dom.tr(dom.td(attr({colspan: '3'}), 'No evaluations.')) : [], isEmpty(evalStats) ? dom.tr(dom.td(attr({colspan: '3'}), 'No evaluations.')) : [],
), ),
), ),
dom.br(),
dom.br(),
dom.h2('Suppressed reporting addresses'),
dom.p('In practice, sending a DMARC report to a reporting address can cause DSN to be sent back. Such addresses can be added to a supression list for a period, to reduce noise in the postmaster mailbox.'),
dom.form(
async function submit(e) {
e.stopPropagation()
e.preventDefault()
try {
fieldset.disabled = true
await api.DMARCSuppressAdd(reportingAddress.value, new Date(until.value), comment.value)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
fieldset.disabled = false
}
window.location.reload() // todo: add the address to the list, or only reload the list
},
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Reporting address',
dom.br(),
reportingAddress=dom.input(attr({required: ''})),
),
' ',
dom.label(
style({display: 'inline-block'}),
'Until',
dom.br(),
until=dom.input(attr({type: 'date', required: '', value: nextmonth.getFullYear()+'-'+(1+nextmonth.getMonth())+'-'+nextmonth.getDate()})),
),
' ',
dom.label(
style({display: 'inline-block'}),
dom.span('Comment (optional)'),
dom.br(),
comment=dom.input(),
),
' ',
dom.button('Add', attr({title: 'Outgoing reports to this reporting address will be suppressed until the end time.'})),
),
),
dom.br(),
dom('table.hover',
dom.thead(
dom.tr(
dom.th('Reporting address'),
dom.th('Until'),
dom.th('Comment'),
dom.th('Action'),
),
),
dom.tbody(
(suppressAddresses || []).length === 0 ? dom.tr(dom.td(attr({colspan: '4'}), 'No suppressed reporting addresses.')) : [],
(suppressAddresses || []).map(ba =>
dom.tr(
dom.td(ba.ReportingAddress),
dom.td(ba.Until),
dom.td(ba.Comment),
dom.td(
dom.button('Remove', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.DMARCSuppressRemove(ba.ID)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
' ',
dom.button('Extend for 1 month', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.DMARCSuppressExtend(ba.ID, new Date(new Date().getTime() + 31*24*3600*1000))
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
),
)
),
),
),
) )
} }
@ -1479,10 +1580,16 @@ const tlsrptIndex = async () => {
} }
const tlsrptResults = async () => { const tlsrptResults = async () => {
const results = await api.TLSRPTResults() const [results, suppressAddresses] = await Promise.all([
api.TLSRPTResults(),
api.TLSRPTSuppressList(),
])
// todo: add a view where results are grouped by policy domain+dayutc. now each recipient domain gets a row. // todo: add a view where results are grouped by policy domain+dayutc. now each recipient domain gets a row.
let fieldset, reportingAddress, until, comment
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
const page = document.getElementById('page') const page = document.getElementById('page')
dom._kids(page, dom._kids(page,
crumbs( crumbs(
@ -1545,6 +1652,101 @@ const tlsrptResults = async () => {
results.length === 0 ? dom.tr(dom.td(attr({colspan: '9'}), 'No results.')) : [], results.length === 0 ? dom.tr(dom.td(attr({colspan: '9'}), 'No results.')) : [],
), ),
), ),
dom.br(),
dom.br(),
dom.h2('Suppressed reporting addresses'),
dom.p('In practice, sending a TLS report to a reporting address can cause DSN to be sent back. Such addresses can be added to a suppress list for a period, to reduce noise in the postmaster mailbox.'),
dom.form(
async function submit(e) {
e.stopPropagation()
e.preventDefault()
try {
fieldset.disabled = true
await api.TLSRPTSuppressAdd(reportingAddress.value, new Date(until.value), comment.value)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
fieldset.disabled = false
}
window.location.reload() // todo: add the address to the list, or only reload the list
},
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Reporting address',
dom.br(),
reportingAddress=dom.input(attr({required: ''})),
),
' ',
dom.label(
style({display: 'inline-block'}),
'Until',
dom.br(),
until=dom.input(attr({type: 'date', required: '', value: nextmonth.getFullYear()+'-'+(1+nextmonth.getMonth())+'-'+nextmonth.getDate()})),
),
' ',
dom.label(
style({display: 'inline-block'}),
dom.span('Comment (optional)'),
dom.br(),
comment=dom.input(),
),
' ',
dom.button('Add', attr({title: 'Outgoing reports to this reporting address will be suppressed until the end time.'})),
),
),
dom.br(),
dom('table.hover',
dom.thead(
dom.tr(
dom.th('Reporting address'),
dom.th('Until'),
dom.th('Comment'),
dom.th('Action'),
),
),
dom.tbody(
(suppressAddresses || []).length === 0 ? dom.tr(dom.td(attr({colspan: '4'}), 'No suppressed reporting addresses.')) : [],
(suppressAddresses || []).map(ba =>
dom.tr(
dom.td(ba.ReportingAddress),
dom.td(ba.Until),
dom.td(ba.Comment),
dom.td(
dom.button('Remove', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.TLSRPTSuppressRemove(ba.ID)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
' ',
dom.button('Extend for 1 month', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.TLSRPTSuppressExtend(ba.ID, new Date(new Date().getTime() + 31*24*3600*1000))
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
),
)
),
),
),
) )
} }

View file

@ -828,6 +828,77 @@
], ],
"Returns": [] "Returns": []
}, },
{
"Name": "DMARCSuppressAdd",
"Docs": "DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing\nreports will be suppressed for a period.",
"Params": [
{
"Name": "reportingAddress",
"Typewords": [
"string"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
},
{
"Name": "comment",
"Typewords": [
"string"
]
}
],
"Returns": []
},
{
"Name": "DMARCSuppressList",
"Docs": "DMARCSuppressList returns all reporting addresses on the suppress list.",
"Params": [],
"Returns": [
{
"Name": "r0",
"Typewords": [
"[]",
"SuppressAddress"
]
}
]
},
{
"Name": "DMARCSuppressRemove",
"Docs": "DMARCSuppressRemove removes a reporting address record from the suppress list.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
}
],
"Returns": []
},
{
"Name": "DMARCSuppressExtend",
"Docs": "DMARCSuppressExtend updates the until field of a suppressed reporting address record.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
}
],
"Returns": []
},
{ {
"Name": "TLSRPTResults", "Name": "TLSRPTResults",
"Docs": "TLSRPTResults returns all TLSRPT results in the database.", "Docs": "TLSRPTResults returns all TLSRPT results in the database.",
@ -920,6 +991,77 @@
} }
], ],
"Returns": [] "Returns": []
},
{
"Name": "TLSRPTSuppressAdd",
"Docs": "TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing\nreports will be suppressed for a period.",
"Params": [
{
"Name": "reportingAddress",
"Typewords": [
"string"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
},
{
"Name": "comment",
"Typewords": [
"string"
]
}
],
"Returns": []
},
{
"Name": "TLSRPTSuppressList",
"Docs": "TLSRPTSuppressList returns all reporting addresses on the suppress list.",
"Params": [],
"Returns": [
{
"Name": "r0",
"Typewords": [
"[]",
"TLSRPTSuppressAddress"
]
}
]
},
{
"Name": "TLSRPTSuppressRemove",
"Docs": "TLSRPTSuppressRemove removes a reporting address record from the suppress list.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
}
],
"Returns": []
},
{
"Name": "TLSRPTSuppressExtend",
"Docs": "TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
}
],
"Returns": []
} }
], ],
"Sections": [], "Sections": [],
@ -3808,6 +3950,47 @@
} }
] ]
}, },
{
"Name": "SuppressAddress",
"Docs": "SuppressAddress is a reporting address for which outgoing DMARC reports\nwill be suppressed for a period.",
"Fields": [
{
"Name": "ID",
"Docs": "",
"Typewords": [
"int64"
]
},
{
"Name": "Inserted",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "ReportingAddress",
"Docs": "",
"Typewords": [
"string"
]
},
{
"Name": "Until",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "Comment",
"Docs": "",
"Typewords": [
"string"
]
}
]
},
{ {
"Name": "TLSResult", "Name": "TLSResult",
"Docs": "TLSResult is stored in the database to track TLS results per policy domain, day\nand recipient domain. These records will be included in TLS reports.", "Docs": "TLSResult is stored in the database to track TLS results per policy domain, day\nand recipient domain. These records will be included in TLS reports.",
@ -3877,6 +4060,47 @@
] ]
} }
] ]
},
{
"Name": "TLSRPTSuppressAddress",
"Docs": "TLSRPTSuppressAddress is a reporting address for which outgoing TLS reports\nwill be suppressed for a period.",
"Fields": [
{
"Name": "ID",
"Docs": "",
"Typewords": [
"int64"
]
},
{
"Name": "Inserted",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "ReportingAddress",
"Docs": "",
"Typewords": [
"string"
]
},
{
"Name": "Until",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "Comment",
"Docs": "",
"Typewords": [
"string"
]
}
]
} }
], ],
"Ints": [], "Ints": [],