mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 08:23:48 +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:
parent
6ce69d5425
commit
e24e1bee19
12 changed files with 697 additions and 17 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -312,7 +312,7 @@ func TestSendReports(t *testing.T) {
|
|||
|
||||
if optExpReport != nil {
|
||||
// Parse report in message and compare with expected.
|
||||
expFeedback.ReportMetadata.ReportID = feedback.ReportMetadata.ReportID
|
||||
optExpReport.ReportMetadata.ReportID = feedback.ReportMetadata.ReportID
|
||||
tcompare(t, feedback, expFeedback)
|
||||
}
|
||||
|
||||
|
@ -348,6 +348,18 @@ func TestSendReports(t *testing.T) {
|
|||
evalOpt.Optional = true
|
||||
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.
|
||||
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)
|
||||
|
|
|
@ -605,6 +605,20 @@ func deliver(resolver dns.Resolver, m Msg) {
|
|||
now := time.Now()
|
||||
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))
|
||||
tlsaPolicyDomains := map[string]bool{}
|
||||
addResult := func(r tlsrpt.Result, isHost bool) {
|
||||
|
@ -629,7 +643,7 @@ func deliver(resolver dns.Resolver, m Msg) {
|
|||
DayUTC: dayUTC,
|
||||
RecipientDomain: m.RecipientDomain.Domain.Name(),
|
||||
IsHost: isHost,
|
||||
SendReport: !m.IsTLSReport,
|
||||
SendReport: !m.IsTLSReport && (!m.IsDMARCReport || failure),
|
||||
Results: []tlsrpt.Result{r},
|
||||
}
|
||||
results = append(results, tlsResult)
|
||||
|
|
|
@ -985,13 +985,12 @@ func TestTLSReport(t *testing.T) {
|
|||
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
|
||||
defer ts.close()
|
||||
|
||||
run := func(tlsrpt string, n int) {
|
||||
run := func(rcptTo, tlsrpt string, n int) {
|
||||
t.Helper()
|
||||
ts.run(func(err error, client *smtpclient.Client) {
|
||||
t.Helper()
|
||||
|
||||
mailFrom := "remote@example.org"
|
||||
rcptTo := "mjl@mox.example"
|
||||
|
||||
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)
|
||||
|
@ -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}}]}`
|
||||
|
||||
run(tlsrpt, 0)
|
||||
run(strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
|
||||
run("mjl@mox.example", tlsrpt, 0)
|
||||
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.
|
||||
evals := checkEvaluationCount(t, 2)
|
||||
evals := checkEvaluationCount(t, 3)
|
||||
tcompare(t, evals[0].Optional, true)
|
||||
tcompare(t, evals[1].Optional, true)
|
||||
tcompare(t, evals[2].Optional, true)
|
||||
}
|
||||
|
||||
func TestRatelimitConnectionrate(t *testing.T) {
|
||||
|
|
6
testdata/smtp/tlsrpt/mox.conf
vendored
6
testdata/smtp/tlsrpt/mox.conf
vendored
|
@ -1,9 +1,13 @@
|
|||
DataDir: ../data
|
||||
User: 1000
|
||||
LogLevel: trace
|
||||
Hostname: mox.example
|
||||
Hostname: mailhost.mox.example
|
||||
Postmaster:
|
||||
Account: mjl
|
||||
Mailbox: postmaster
|
||||
Listeners:
|
||||
local: nil
|
||||
HostTLSRPT:
|
||||
Account: mjl
|
||||
Mailbox: TLSRPT
|
||||
Localpart: mjl
|
||||
|
|
|
@ -17,7 +17,7 @@ var (
|
|||
mutex sync.Mutex
|
||||
|
||||
// Accessed directly by tlsrptsend.
|
||||
ResultDBTypes = []any{TLSResult{}}
|
||||
ResultDBTypes = []any{TLSResult{}, TLSRPTSuppressAddress{}}
|
||||
ResultDB *bstore.DB
|
||||
)
|
||||
|
||||
|
|
|
@ -51,6 +51,18 @@ type TLSResult struct {
|
|||
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) {
|
||||
mutex.Lock()
|
||||
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()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -441,6 +441,19 @@ Period: %s - %s UTC
|
|||
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[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)
|
||||
// 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.
|
||||
|
@ -451,7 +464,7 @@ Period: %s - %s UTC
|
|||
no := false
|
||||
qm.RequireTLS = &no
|
||||
|
||||
err := queueAdd(ctx, log, &qm, msgf)
|
||||
err = queueAdd(ctx, log, &qm, msgf)
|
||||
if err != nil {
|
||||
tempError = true
|
||||
log.Errorx("queueing message with tls report", err)
|
||||
|
|
|
@ -381,4 +381,14 @@ func TestSendReports(t *testing.T) {
|
|||
"tls-reports3@mailhost.sender.example": report2,
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2031,6 +2031,36 @@ func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
|
|||
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.
|
||||
func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -1023,7 +1023,7 @@ const dmarcIndex = async () => {
|
|||
dom._kids(page,
|
||||
crumbs(
|
||||
crumblink('Mox Admin', '#'),
|
||||
'DMARC reports and evaluations',
|
||||
'DMARC',
|
||||
),
|
||||
dom.ul(
|
||||
dom.li(
|
||||
|
@ -1085,7 +1085,10 @@ const renderDMARCSummaries = (summaries) => {
|
|||
}
|
||||
|
||||
const dmarcEvaluations = async () => {
|
||||
const evalStats = await api.DMARCEvaluationStats()
|
||||
const [evalStats, suppressAddresses] = await Promise.all([
|
||||
api.DMARCEvaluationStats(),
|
||||
api.DMARCSuppressList(),
|
||||
])
|
||||
|
||||
const isEmpty = (o) => {
|
||||
for (const e in o) {
|
||||
|
@ -1094,6 +1097,9 @@ const dmarcEvaluations = async () => {
|
|||
return true
|
||||
}
|
||||
|
||||
let fieldset, reportingAddress, until, comment
|
||||
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
|
||||
|
||||
const page = document.getElementById('page')
|
||||
dom._kids(page,
|
||||
crumbs(
|
||||
|
@ -1121,6 +1127,101 @@ const dmarcEvaluations = async () => {
|
|||
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 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.
|
||||
|
||||
let fieldset, reportingAddress, until, comment
|
||||
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
|
||||
|
||||
const page = document.getElementById('page')
|
||||
dom._kids(page,
|
||||
crumbs(
|
||||
|
@ -1545,6 +1652,101 @@ const tlsrptResults = async () => {
|
|||
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
|
||||
}),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -828,6 +828,77 @@
|
|||
],
|
||||
"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",
|
||||
"Docs": "TLSRPTResults returns all TLSRPT results in the database.",
|
||||
|
@ -920,6 +991,77 @@
|
|||
}
|
||||
],
|
||||
"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": [],
|
||||
|
@ -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",
|
||||
"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": [],
|
||||
|
|
Loading…
Reference in a new issue