148 lines
3.3 KiB
Go
148 lines
3.3 KiB
Go
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
|
|
}
|
|
}
|