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
}
}