summaryrefslogtreecommitdiff
path: root/lib/auth/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/auth/auth.go')
-rw-r--r--lib/auth/auth.go145
1 files changed, 145 insertions, 0 deletions
diff --git a/lib/auth/auth.go b/lib/auth/auth.go
new file mode 100644
index 0000000..8a0a40f
--- /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
+ }
+}