2023-01-30 16:27:06 +03:00
|
|
|
package dmarc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Policy as used in DMARC DNS record for "p=" or "sp=".
|
2024-04-19 11:51:24 +03:00
|
|
|
type Policy string
|
2023-01-30 16:27:06 +03:00
|
|
|
|
|
|
|
// ../rfc/7489:1157
|
|
|
|
|
|
|
|
const (
|
2024-04-19 11:51:24 +03:00
|
|
|
PolicyEmpty Policy = "" // Only for the optional Record.SubdomainPolicy.
|
|
|
|
PolicyNone Policy = "none"
|
|
|
|
PolicyQuarantine Policy = "quarantine"
|
|
|
|
PolicyReject Policy = "reject"
|
2023-01-30 16:27:06 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// URI is a destination address for reporting.
|
|
|
|
type URI struct {
|
|
|
|
Address string // Should start with "mailto:".
|
|
|
|
MaxSize uint64 // Optional maximum message size, subject to Unit.
|
2023-08-23 15:27:21 +03:00
|
|
|
Unit string // "" (b), "k", "m", "g", "t" (case insensitive), unit size, where k is 2^10 etc.
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a string representation of the URI for inclusion in a DMARC
|
|
|
|
// record.
|
|
|
|
func (u URI) String() string {
|
|
|
|
s := u.Address
|
|
|
|
s = strings.ReplaceAll(s, ",", "%2C")
|
|
|
|
s = strings.ReplaceAll(s, "!", "%21")
|
|
|
|
if u.MaxSize > 0 {
|
2023-08-23 15:27:21 +03:00
|
|
|
s += fmt.Sprintf("!%d", u.MaxSize)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
s += u.Unit
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// ../rfc/7489:1127
|
|
|
|
|
|
|
|
// Align specifies the required alignment of a domain name.
|
|
|
|
type Align string
|
|
|
|
|
|
|
|
const (
|
|
|
|
AlignStrict Align = "s" // Strict requires an exact domain name match.
|
|
|
|
AlignRelaxed Align = "r" // Relaxed requires either an exact or subdomain name match.
|
|
|
|
)
|
|
|
|
|
|
|
|
// Record is a DNS policy or reporting record.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// v=DMARC1; p=reject; rua=mailto:postmaster@mox.example
|
|
|
|
type Record struct {
|
2024-04-19 11:51:24 +03:00
|
|
|
Version string // "v=DMARC1", fixed.
|
|
|
|
Policy Policy // Required, for "p=".
|
|
|
|
SubdomainPolicy Policy // Like policy but for subdomains. Optional, for "sp=".
|
|
|
|
AggregateReportAddresses []URI // Optional, for "rua=". Destination addresses for aggregate reports.
|
|
|
|
FailureReportAddresses []URI // Optional, for "ruf=". Destination addresses for failure reports.
|
|
|
|
ADKIM Align // Alignment: "r" (default) for relaxed or "s" for simple. For "adkim=".
|
|
|
|
ASPF Align // Alignment: "r" (default) for relaxed or "s" for simple. For "aspf=".
|
|
|
|
AggregateReportingInterval int // In seconds, default 86400. For "ri="
|
|
|
|
FailureReportingOptions []string // "0" (default), "1", "d", "s". For "fo=".
|
|
|
|
ReportingFormat []string // "afrf" (default). For "rf=".
|
|
|
|
Percentage int // Between 0 and 100, default 100. For "pct=". Policy applies randomly to this percentage of messages.
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultRecord holds the defaults for a DMARC record.
|
|
|
|
var DefaultRecord = Record{
|
|
|
|
Version: "DMARC1",
|
|
|
|
ADKIM: "r",
|
|
|
|
ASPF: "r",
|
|
|
|
AggregateReportingInterval: 86400,
|
|
|
|
FailureReportingOptions: []string{"0"},
|
|
|
|
ReportingFormat: []string{"afrf"},
|
|
|
|
Percentage: 100,
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns the DMARC record for use as DNS TXT record.
|
|
|
|
func (r Record) String() string {
|
|
|
|
b := &strings.Builder{}
|
|
|
|
b.WriteString("v=" + r.Version)
|
|
|
|
|
|
|
|
wrote := false
|
|
|
|
write := func(do bool, tag, value string) {
|
|
|
|
if do {
|
|
|
|
fmt.Fprintf(b, ";%s=%s", tag, value)
|
|
|
|
wrote = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
write(r.Policy != "", "p", string(r.Policy))
|
|
|
|
write(r.SubdomainPolicy != "", "sp", string(r.SubdomainPolicy))
|
|
|
|
if len(r.AggregateReportAddresses) > 0 {
|
|
|
|
l := make([]string, len(r.AggregateReportAddresses))
|
|
|
|
for i, a := range r.AggregateReportAddresses {
|
|
|
|
l[i] = a.String()
|
|
|
|
}
|
|
|
|
s := strings.Join(l, ",")
|
|
|
|
write(true, "rua", s)
|
|
|
|
}
|
|
|
|
if len(r.FailureReportAddresses) > 0 {
|
|
|
|
l := make([]string, len(r.FailureReportAddresses))
|
|
|
|
for i, a := range r.FailureReportAddresses {
|
|
|
|
l[i] = a.String()
|
|
|
|
}
|
|
|
|
s := strings.Join(l, ",")
|
|
|
|
write(true, "ruf", s)
|
|
|
|
}
|
2023-08-23 15:27:21 +03:00
|
|
|
write(r.ADKIM != "" && r.ADKIM != "r", "adkim", string(r.ADKIM))
|
|
|
|
write(r.ASPF != "" && r.ASPF != "r", "aspf", string(r.ASPF))
|
2023-01-30 16:27:06 +03:00
|
|
|
write(r.AggregateReportingInterval != DefaultRecord.AggregateReportingInterval, "ri", fmt.Sprintf("%d", r.AggregateReportingInterval))
|
2023-08-23 15:27:21 +03:00
|
|
|
if len(r.FailureReportingOptions) > 1 || len(r.FailureReportingOptions) == 1 && r.FailureReportingOptions[0] != "0" {
|
2023-01-30 16:27:06 +03:00
|
|
|
write(true, "fo", strings.Join(r.FailureReportingOptions, ":"))
|
|
|
|
}
|
2023-08-23 15:27:21 +03:00
|
|
|
if len(r.ReportingFormat) > 1 || len(r.ReportingFormat) == 1 && !strings.EqualFold(r.ReportingFormat[0], "afrf") {
|
2023-01-30 16:27:06 +03:00
|
|
|
write(true, "rf", strings.Join(r.FailureReportingOptions, ":"))
|
|
|
|
}
|
|
|
|
write(r.Percentage != 100, "pct", fmt.Sprintf("%d", r.Percentage))
|
|
|
|
|
|
|
|
if !wrote {
|
|
|
|
b.WriteString(";")
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|