diff --git a/dmarcdb/eval.go b/dmarcdb/eval.go index 4c26e96..a11dd6d 100644 --- a/dmarcdb/eval.go +++ b/dmarcdb/eval.go @@ -22,6 +22,7 @@ import ( "time" "golang.org/x/exp/maps" + "golang.org/x/exp/slices" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -219,9 +220,10 @@ func Evaluations(ctx context.Context) ([]Evaluation, error) { // EvaluationStat summarizes stored evaluations, for inclusion in an upcoming // aggregate report, for a domain. type EvaluationStat struct { - Count int - SendReport bool - Domain dns.Domain + Domain dns.Domain + Dispositions []string + Count int + SendReport bool } // EvaluationStats returns evaluation counts and report-sending status per domain. @@ -235,6 +237,9 @@ func EvaluationStats(ctx context.Context) (map[string]EvaluationStat, error) { err = bstore.QueryDB[Evaluation](ctx, db).ForEach(func(e Evaluation) error { if stat, ok := r[e.PolicyDomain]; ok { + if !slices.Contains(stat.Dispositions, string(e.Disposition)) { + stat.Dispositions = append(stat.Dispositions, string(e.Disposition)) + } stat.Count++ stat.SendReport = stat.SendReport || !e.Optional r[e.PolicyDomain] = stat @@ -244,9 +249,10 @@ func EvaluationStats(ctx context.Context) (map[string]EvaluationStat, error) { return fmt.Errorf("parsing domain %q: %v", e.PolicyDomain, err) } r[e.PolicyDomain] = EvaluationStat{ - Count: 1, - SendReport: !e.Optional, - Domain: dom, + Domain: dom, + Dispositions: []string{string(e.Disposition)}, + Count: 1, + SendReport: !e.Optional, } } return nil diff --git a/dmarcdb/eval_test.go b/dmarcdb/eval_test.go index 20f3ad9..35baaf9 100644 --- a/dmarcdb/eval_test.go +++ b/dmarcdb/eval_test.go @@ -109,14 +109,16 @@ func TestEvaluations(t *testing.T) { expStats := map[string]EvaluationStat{ "sender1.example": { - Count: 3, - SendReport: true, - Domain: dns.Domain{ASCII: "sender1.example"}, + Domain: dns.Domain{ASCII: "sender1.example"}, + Dispositions: []string{"none"}, + Count: 3, + SendReport: true, }, "sender2.example": { - Count: 1, - SendReport: true, - Domain: dns.Domain{ASCII: "sender2.example"}, + Domain: dns.Domain{ASCII: "sender2.example"}, + Dispositions: []string{"none"}, + Count: 1, + SendReport: true, }, } stats, err := EvaluationStats(ctxbg) @@ -142,9 +144,10 @@ func TestEvaluations(t *testing.T) { expStats = map[string]EvaluationStat{ "sender2.example": { - Count: 1, - SendReport: true, - Domain: dns.Domain{ASCII: "sender2.example"}, + Domain: dns.Domain{ASCII: "sender2.example"}, + Dispositions: []string{"none"}, + Count: 1, + SendReport: true, }, } stats, err = EvaluationStats(ctxbg) diff --git a/webadmin/admin.html b/webadmin/admin.html index 69aebc7..ecbcd9a 100644 --- a/webadmin/admin.html +++ b/webadmin/admin.html @@ -1112,6 +1112,7 @@ const dmarcEvaluations = async () => { dom.thead( dom.tr( dom.th('Domain', attr({title: 'Domain in the message From header. Keep in mind these can be forged, so this does not necessarily mean someone from this domain authentically tried delivering email.'})), + dom.th('Dispositions', attr({title: 'Unique dispositions occurring in report.'})), dom.th('Evaluations', attr({title: 'Total number of message delivery attempts, including retries.'})), dom.th('Send report', attr({title: 'Whether the current evaluations will cause a report to be sent.'})), ), @@ -1120,6 +1121,7 @@ const dmarcEvaluations = async () => { Object.entries(evalStats).sort((a, b) => a[0] < b[0] ? -1 : 1).map(t => dom.tr( dom.td(dom.a(attr({href: '#dmarc/evaluations/'+domainName(t[1].Domain)}), domainString(t[1].Domain))), + dom.td((t[1].Dispositions || []).join(' ')), dom.td(style({textAlign: 'right'}), ''+t[1].Count), dom.td(style({textAlign: 'right'}), t[1].SendReport ? '✓' : ''), ), diff --git a/webadmin/adminapi.json b/webadmin/adminapi.json index f1d2a74..3b9a20c 100644 --- a/webadmin/adminapi.json +++ b/webadmin/adminapi.json @@ -3798,6 +3798,21 @@ "Name": "EvaluationStat", "Docs": "EvaluationStat summarizes stored evaluations, for inclusion in an upcoming\naggregate report, for a domain.", "Fields": [ + { + "Name": "Domain", + "Docs": "", + "Typewords": [ + "Domain" + ] + }, + { + "Name": "Dispositions", + "Docs": "", + "Typewords": [ + "[]", + "string" + ] + }, { "Name": "Count", "Docs": "", @@ -3811,13 +3826,6 @@ "Typewords": [ "bool" ] - }, - { - "Name": "Domain", - "Docs": "", - "Typewords": [ - "Domain" - ] } ] },