diff options
Diffstat (limited to 'lib/auth/auth.go')
-rw-r--r-- | lib/auth/auth.go | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/lib/auth/auth.go b/lib/auth/auth.go new file mode 100644 index 00000000..8a0a40fa --- /dev/null +++ b/lib/auth/auth.go @@ -0,0 +1,145 @@ +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) + if 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 + } + } else if 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 + } + } else if 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 + } +} |