Files
go-msgauth/dmarc/lookup.go
T
Hui CaoandSimon Ser 2470a25200 Fix dkim verification issues and dmarc alignment default
Change-Id: I0e023a602004bb18f573321c5811d1c876312663
2019-11-20 23:26:28 +01:00

206 lines
4.5 KiB
Go

package dmarc
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
)
type tempFailError string
func (err tempFailError) Error() string {
return "dmarc: " + string(err)
}
// IsTempFail returns true if the error returned by Lookup is a temporary
// failure.
func IsTempFail(err error) bool {
_, ok := err.(tempFailError)
return ok
}
var ErrNoPolicy = errors.New("dmarc: no policy found for domain")
// Lookup queries a DMARC record for a specified domain.
func Lookup(domain string) (*Record, error) {
txts, err := net.LookupTXT("_dmarc." + domain)
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
return nil, tempFailError("TXT record unavailable: " + err.Error())
} else if err != nil {
if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
return nil, ErrNoPolicy
}
return nil, errors.New("dmarc: failed to lookup TXT record: " + err.Error())
}
if len(txts) == 0 {
return nil, ErrNoPolicy
}
// Long keys are split in multiple parts
txt := strings.Join(txts, "")
return Parse(txt)
}
func Parse(txt string) (*Record, error) {
params, err := parseParams(txt)
if err != nil {
return nil, err
}
if params["v"] != "DMARC1" {
return nil, errors.New("dmarc: unsupported DMARC version")
}
rec := new(Record)
p, ok := params["p"]
if !ok {
return nil, errors.New("dmarc: record is missing a 'p' parameter")
}
rec.Policy, err = parsePolicy(p, "p")
if err != nil {
return nil, err
}
rec.DKIMAlignment = AlignmentRelaxed
if adkim, ok := params["adkim"]; ok {
rec.DKIMAlignment, err = parseAlignmentMode(adkim, "adkim")
if err != nil {
return nil, err
}
}
rec.SPFAlignment = AlignmentRelaxed
if aspf, ok := params["aspf"]; ok {
rec.SPFAlignment, err = parseAlignmentMode(aspf, "aspf")
if err != nil {
return nil, err
}
}
if fo, ok := params["fo"]; ok {
rec.FailureOptions, err = parseFailureOptions(fo)
if err != nil {
return nil, err
}
}
if pct, ok := params["pct"]; ok {
i, err := strconv.Atoi(pct)
if err != nil {
return nil, fmt.Errorf("dmarc: invalid parameter 'pct': %v", err)
}
if i < 0 || i > 100 {
return nil, fmt.Errorf("dmarc: invalid parameter 'pct': value %v out of bounds", i)
}
rec.Percent = &i
}
if rf, ok := params["rf"]; ok {
l := strings.Split(rf, ":")
rec.ReportFormat = make([]ReportFormat, len(l))
for i, f := range l {
switch f {
case "afrf":
rec.ReportFormat[i] = ReportFormat(f)
default:
return nil, errors.New("dmarc: invalid parameter 'rf'")
}
}
}
if ri, ok := params["ri"]; ok {
i, err := strconv.Atoi(ri)
if err != nil {
return nil, fmt.Errorf("dmarc: invalid parameter 'ri': %v", err)
}
if i <= 0 {
return nil, fmt.Errorf("dmarc: invalid parameter 'ri': negative or zero duration")
}
rec.ReportInterval = time.Duration(i) * time.Second
}
if rua, ok := params["rua"]; ok {
rec.ReportURIAggregate = parseURIList(rua)
}
if ruf, ok := params["ruf"]; ok {
rec.ReportURIFailure = parseURIList(ruf)
}
if sp, ok := params["sp"]; ok {
rec.SubdomainPolicy, err = parsePolicy(sp, "sp")
if err != nil {
return nil, err
}
}
return rec, nil
}
func parseParams(s string) (map[string]string, error) {
pairs := strings.Split(s, ";")
params := make(map[string]string)
for _, s := range pairs {
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
if strings.TrimSpace(s) == "" {
continue
}
return params, errors.New("dmarc: malformed params")
}
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
}
return params, nil
}
func parsePolicy(s, param string) (Policy, error) {
switch s {
case "none", "quarantine", "reject":
return Policy(s), nil
default:
return "", fmt.Errorf("dmarc: invalid policy for parameter '%v'", param)
}
}
func parseAlignmentMode(s, param string) (AlignmentMode, error) {
switch s {
case "r", "s":
return AlignmentMode(s), nil
default:
return "", fmt.Errorf("dmarc: invalid alignment mode for parameter '%v'", param)
}
}
func parseFailureOptions(s string) (FailureOptions, error) {
l := strings.Split(s, ":")
var opts FailureOptions
for _, o := range l {
switch strings.TrimSpace(o) {
case "0":
opts |= FailureAll
case "1":
opts |= FailureAny
case "d":
opts |= FailureDKIM
case "s":
opts |= FailureSPF
default:
return 0, errors.New("dmarc: invalid failure option in parameter 'fo'")
}
}
return opts, nil
}
func parseURIList(s string) []string {
l := strings.Split(s, ",")
for i, u := range l {
l[i] = strings.TrimSpace(u)
}
return l
}