1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
package auth
import (
"fmt"
"regexp"
"strings"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-msgauth/authres"
)
const (
AuthHeader = "Authentication-Results"
)
type Method string
const (
DKIM Method = "dkim"
SPF Method = "spf"
DMARC Method = "dmarc"
)
type Result string
const (
ResultNone Result = "none"
ResultPass Result = "pass"
ResultFail Result = "fail"
ResultNeutral Result = "neutral"
ResultPolicy Result = "policy"
)
type Details struct {
Results []Result
Infos []string
Reasons []string
Err error
}
func (d *Details) add(r Result, info string, reason string) {
d.Results = append(d.Results, r)
d.Infos = append(d.Infos, info)
d.Reasons = append(d.Reasons, reason)
}
type ParserFunc func(*mail.Header, []string) (*Details, error)
func New(s string) ParserFunc {
if i := strings.IndexRune(s, '+'); i > 0 {
s = s[:i]
}
m := Method(strings.ToLower(s))
switch m {
case DKIM, SPF, DMARC:
return CreateParser(m)
}
return nil
}
func trust(s string, trusted []string) bool {
for _, t := range trusted {
if matched, _ := regexp.MatchString(t, s); matched || t == "*" {
return true
}
}
return false
}
var cleaner = regexp.MustCompile(`(\(.*);(.*\))`)
func CreateParser(m Method) func(*mail.Header, []string) (*Details, error) {
return func(header *mail.Header, trusted []string) (*Details, error) {
details := &Details{}
found := false
hf := header.FieldsByKey(AuthHeader)
for hf.Next() {
headerText, err := hf.Text()
if err != nil {
return nil, err
}
identifier, results, err := authres.Parse(headerText)
// TODO: refactor to use errors.Is
switch {
case err != nil && err.Error() == "msgauth: unsupported version":
// Some MTA write their authres header without an identifier
// which does not conform to RFC but still exists in the wild
identifier, results, err = authres.Parse("unknown;" + headerText)
if err != nil {
return nil, err
}
case err != nil && err.Error() == "msgauth: malformed authentication method and value":
// the go-msgauth parser doesn't like semi-colons in the comments
// as a work-around we remove those
cleanHeader := cleaner.ReplaceAllString(headerText, "${1}${2}")
identifier, results, err = authres.Parse(cleanHeader)
if err != nil {
return nil, err
}
case err != nil:
return nil, err
}
// implements recommendation from RFC 7601 Sec 7.1 to
// have an explicit list of trustworthy hostnames
// before displaying AuthRes results
if !trust(identifier, trusted) {
return nil, fmt.Errorf("%s is not trusted", identifier)
}
for _, result := range results {
switch r := result.(type) {
case *authres.DKIMResult:
if m == DKIM {
info := r.Identifier
if info == "" && r.Domain != "" {
info = r.Domain
}
details.add(Result(r.Value), info, r.Reason)
found = true
}
case *authres.SPFResult:
if m == SPF {
info := r.From
if info == "" && r.Helo != "" {
info = r.Helo
}
details.add(Result(r.Value), info, r.Reason)
found = true
}
case *authres.DMARCResult:
if m == DMARC {
details.add(Result(r.Value), r.From, r.Reason)
found = true
}
}
}
}
if !found {
details.add(ResultNone, "", "")
}
return details, nil
}
}
|