1package dmarc
2
3import (
4 "fmt"
5 "strings"
6)
7
8// todo: DMARCPolicy should be named just Policy, but this is causing conflicting types in sherpadoc output. should somehow get the dmarc-prefix only in the sherpadoc.
9
10// Policy as used in DMARC DNS record for "p=" or "sp=".
11type DMARCPolicy string
12
13// ../rfc/7489:1157
14
15const (
16 PolicyEmpty DMARCPolicy = "" // Only for the optional Record.SubdomainPolicy.
17 PolicyNone DMARCPolicy = "none"
18 PolicyQuarantine DMARCPolicy = "quarantine"
19 PolicyReject DMARCPolicy = "reject"
20)
21
22// URI is a destination address for reporting.
23type URI struct {
24 Address string // Should start with "mailto:".
25 MaxSize uint64 // Optional maximum message size, subject to Unit.
26 Unit string // "" (b), "k", "m", "g", "t" (case insensitive), unit size, where k is 2^10 etc.
27}
28
29// String returns a string representation of the URI for inclusion in a DMARC
30// record.
31func (u URI) String() string {
32 s := u.Address
33 s = strings.ReplaceAll(s, ",", "%2C")
34 s = strings.ReplaceAll(s, "!", "%21")
35 if u.MaxSize > 0 {
36 s += fmt.Sprintf("!%d", u.MaxSize)
37 }
38 s += u.Unit
39 return s
40}
41
42// ../rfc/7489:1127
43
44// Align specifies the required alignment of a domain name.
45type Align string
46
47const (
48 AlignStrict Align = "s" // Strict requires an exact domain name match.
49 AlignRelaxed Align = "r" // Relaxed requires either an exact or subdomain name match.
50)
51
52// Record is a DNS policy or reporting record.
53//
54// Example:
55//
56// v=DMARC1; p=reject; rua=mailto:postmaster@mox.example
57type Record struct {
58 Version string // "v=DMARC1", fixed.
59 Policy DMARCPolicy // Required, for "p=".
60 SubdomainPolicy DMARCPolicy // Like policy but for subdomains. Optional, for "sp=".
61 AggregateReportAddresses []URI // Optional, for "rua=". Destination addresses for aggregate reports.
62 FailureReportAddresses []URI // Optional, for "ruf=". Destination addresses for failure reports.
63 ADKIM Align // Alignment: "r" (default) for relaxed or "s" for simple. For "adkim=".
64 ASPF Align // Alignment: "r" (default) for relaxed or "s" for simple. For "aspf=".
65 AggregateReportingInterval int // In seconds, default 86400. For "ri="
66 FailureReportingOptions []string // "0" (default), "1", "d", "s". For "fo=".
67 ReportingFormat []string // "afrf" (default). For "rf=".
68 Percentage int // Between 0 and 100, default 100. For "pct=". Policy applies randomly to this percentage of messages.
69}
70
71// DefaultRecord holds the defaults for a DMARC record.
72var DefaultRecord = Record{
73 Version: "DMARC1",
74 ADKIM: "r",
75 ASPF: "r",
76 AggregateReportingInterval: 86400,
77 FailureReportingOptions: []string{"0"},
78 ReportingFormat: []string{"afrf"},
79 Percentage: 100,
80}
81
82// String returns the DMARC record for use as DNS TXT record.
83func (r Record) String() string {
84 b := &strings.Builder{}
85 b.WriteString("v=" + r.Version)
86
87 wrote := false
88 write := func(do bool, tag, value string) {
89 if do {
90 fmt.Fprintf(b, ";%s=%s", tag, value)
91 wrote = true
92 }
93 }
94 write(r.Policy != "", "p", string(r.Policy))
95 write(r.SubdomainPolicy != "", "sp", string(r.SubdomainPolicy))
96 if len(r.AggregateReportAddresses) > 0 {
97 l := make([]string, len(r.AggregateReportAddresses))
98 for i, a := range r.AggregateReportAddresses {
99 l[i] = a.String()
100 }
101 s := strings.Join(l, ",")
102 write(true, "rua", s)
103 }
104 if len(r.FailureReportAddresses) > 0 {
105 l := make([]string, len(r.FailureReportAddresses))
106 for i, a := range r.FailureReportAddresses {
107 l[i] = a.String()
108 }
109 s := strings.Join(l, ",")
110 write(true, "ruf", s)
111 }
112 write(r.ADKIM != "" && r.ADKIM != "r", "adkim", string(r.ADKIM))
113 write(r.ASPF != "" && r.ASPF != "r", "aspf", string(r.ASPF))
114 write(r.AggregateReportingInterval != DefaultRecord.AggregateReportingInterval, "ri", fmt.Sprintf("%d", r.AggregateReportingInterval))
115 if len(r.FailureReportingOptions) > 1 || len(r.FailureReportingOptions) == 1 && r.FailureReportingOptions[0] != "0" {
116 write(true, "fo", strings.Join(r.FailureReportingOptions, ":"))
117 }
118 if len(r.ReportingFormat) > 1 || len(r.ReportingFormat) == 1 && !strings.EqualFold(r.ReportingFormat[0], "afrf") {
119 write(true, "rf", strings.Join(r.FailureReportingOptions, ":"))
120 }
121 write(r.Percentage != 100, "pct", fmt.Sprintf("%d", r.Percentage))
122
123 if !wrote {
124 b.WriteString(";")
125 }
126 return b.String()
127}
128