aboutsummaryrefslogtreecommitdiffstats
path: root/lib/auth/auth.go
blob: ea32ecd3269b4c331bbb5e00707d433307911373 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
	}
}