summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2022-05-30 00:20:41 +0200
committerRobin Jarry <robin@jarry.cc>2022-06-09 09:42:23 +0200
commit83e0e2638df9da0801af7ad35058938dc8eb1cdc (patch)
tree6396833487c13e6cffbebdd43112779684bdcc60 /lib
parent115447e57f015b1805d2d58d1ae46beaff2299e5 (diff)
downloadaerc-83e0e2638df9da0801af7ad35058938dc8eb1cdc.zip
msgviewer: parse and display authentication results
Parse the Authentication-Results header and display it in the message viewer (not enabled by default). DKIM, SPF and DMARC authentication methods are supported. Implement recommendation from RFC 7601 Sec 7.1 to have an explicit list of trustworthy hostnames before displaying the authentication results. Be aware that the authentication headers can be forged. To display the results for a specific authentication method, add the corresponding name to the layout of headers in the viewer section of aerc.conf, e.g. to display all three, use: header-layout = From|To,Cc|Bcc,Date,Subject,DKIM|SPF|DMARC More information will be displayed when "+" is appended to the authentication method name, e.g. DKIM+ or SPF+ or DMARC+. Also, add the trustworthy hosts per account with the trusted-authres parameter, e.g. trusted-authres = * to trust every host or use regular expressions for a finer control. Multiple hosts can be entered as a comma-separated list. Authentication results will only be displayed when the host is listed in the trusted-authres list. Link: https://datatracker.ietf.org/doc/html/rfc7601 Signed-off-by: Koni Marti <koni.marti@gmail.com> Tested-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'lib')
-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
+ }
+}