aboutsummaryrefslogblamecommitdiffstats
path: root/lib/auth/auth.go
blob: 8a0a40fa7caa7dc495b946d418dec857ea57a675 (plain) (tree)
















































































































































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