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