summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go3
-rw-r--r--doc/aerc-config.5.scd11
-rw-r--r--go.mod1
-rw-r--r--go.sum7
-rw-r--r--lib/auth/auth.go145
-rw-r--r--widgets/authinfo.go93
-rw-r--r--widgets/msgviewer.go41
7 files changed, 295 insertions, 6 deletions
diff --git a/config/config.go b/config/config.go
index f87649c..923a1a0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -116,6 +116,9 @@ type AccountConfig struct {
PgpKeyId string `ini:"pgp-key-id"`
PgpAutoSign bool `ini:"pgp-auto-sign"`
PgpOpportunisticEncrypt bool `ini:"pgp-opportunistic-encrypt"`
+
+ // AuthRes
+ TrustedAuthRes []string `ini:"trusted-authres" delim:","`
}
type BindingConfig struct {
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index b7fba82..15c6ca3 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -383,6 +383,11 @@ These options are configured in the *[viewer]* section of aerc.conf.
Rows will be hidden if none of their specified headers are present in the
message.
+ Authentication information from the Authentication-Results header can be
+ displayed by adding DKIM, SPF or DMARC. To show more information
+ than just the authentication result, append a plus sign (+) to the header name
+ (e.g. DKIM+).
+
Default: From|To,Cc|Bcc,Date,Subject
*show-headers*
@@ -649,6 +654,12 @@ Note that many of these configuration options are written for you, such as
signature to be added to emails sent from this account. If the command
fails then *signature-file* is used instead.
+*trusted-authres*
+ Comma-separated list of trustworthy hostnames from which the
+ Authentication Results header will be displayed. Entries can be regular
+ expressions. If you want to trust any host (e.g. for debugging),
+ use the wildcard \*.
+
# BINDS.CONF
This file is used for configuring keybindings used in the aerc interactive
diff --git a/go.mod b/go.mod
index 923ec07..a3b32be 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/emersion/go-imap-sortthread v1.2.0
github.com/emersion/go-maildir v0.2.0
github.com/emersion/go-message v0.15.0
+ github.com/emersion/go-msgauth v0.6.5 // indirect
github.com/emersion/go-pgpmail v0.2.0
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
github.com/emersion/go-smtp v0.15.0
diff --git a/go.sum b/go.sum
index 92d3246..4539610 100644
--- a/go.sum
+++ b/go.sum
@@ -69,8 +69,13 @@ github.com/emersion/go-imap-sortthread v1.2.0/go.mod h1:UhenCBupR+vSYRnqJkpjSq84
github.com/emersion/go-maildir v0.2.0 h1:fC4+UVGl8GcQGbFF7AWab2JMf4VbKz+bMNv07xxhzs8=
github.com/emersion/go-maildir v0.2.0/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-milter v0.3.2/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
+github.com/emersion/go-msgauth v0.6.5 h1:UaXBtrjYBM3SWw9BBODeSp0uYtScx3CuIF7/RQfkeWo=
+github.com/emersion/go-msgauth v0.6.5/go.mod h1:/jbQISFJgtT12T8akRs20l+wI4HcyN/kWy7VRdHEAmA=
github.com/emersion/go-pgpmail v0.2.0 h1:BU9kEGQcDVXi6n0v3JBsWAikyo63xsUGZ1lnVaWa6ks=
github.com/emersion/go-pgpmail v0.2.0/go.mod h1:8mQ8Rpn+w28DDaiP8HvJuZjSAymaWr87K3zA/bwwkU0=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
@@ -178,6 +183,7 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
+github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
@@ -341,6 +347,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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
+ }
+}
diff --git a/widgets/authinfo.go b/widgets/authinfo.go
new file mode 100644
index 0000000..0879554
--- /dev/null
+++ b/widgets/authinfo.go
@@ -0,0 +1,93 @@
+package widgets
+
+import (
+ "fmt"
+
+ "git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/auth"
+ "git.sr.ht/~rjarry/aerc/lib/ui"
+ "github.com/gdamore/tcell/v2"
+ "github.com/mattn/go-runewidth"
+)
+
+type AuthInfo struct {
+ ui.Invalidatable
+ authdetails *auth.Details
+ showInfo bool
+ uiConfig config.UIConfig
+}
+
+func NewAuthInfo(auth *auth.Details, showInfo bool, uiConfig config.UIConfig) *AuthInfo {
+ return &AuthInfo{authdetails: auth, showInfo: showInfo, uiConfig: uiConfig}
+}
+
+func (a *AuthInfo) Draw(ctx *ui.Context) {
+ defaultStyle := a.uiConfig.GetStyle(config.STYLE_DEFAULT)
+ style := a.uiConfig.GetStyle(config.STYLE_DEFAULT)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
+ var text string
+ if a.authdetails == nil {
+ text = "(no header)"
+ ctx.Printf(0, 0, defaultStyle, text)
+ } else if a.authdetails.Err != nil {
+ style = a.uiConfig.GetStyle(config.STYLE_ERROR)
+ text = a.authdetails.Err.Error()
+ ctx.Printf(0, 0, style, text)
+ } else {
+ checkBounds := func(x int) bool {
+ if x < ctx.Width() {
+ return true
+ } else {
+ return false
+ }
+ }
+ setResult := func(result auth.Result) (string, tcell.Style) {
+ switch result {
+ case auth.ResultNone:
+ return "none", defaultStyle
+ case auth.ResultNeutral:
+ return "neutral", a.uiConfig.GetStyle(config.STYLE_WARNING)
+ case auth.ResultPolicy:
+ return "policy", a.uiConfig.GetStyle(config.STYLE_WARNING)
+ case auth.ResultPass:
+ return "✓", a.uiConfig.GetStyle(config.STYLE_SUCCESS)
+ case auth.ResultFail:
+ return "✗", a.uiConfig.GetStyle(config.STYLE_ERROR)
+ default:
+ return string(result), a.uiConfig.GetStyle(config.STYLE_ERROR)
+ }
+ }
+ x := 1
+ for i := 0; i < len(a.authdetails.Results); i++ {
+ if checkBounds(x) {
+ text, style := setResult(a.authdetails.Results[i])
+ if i > 0 {
+ text = " " + text
+ }
+ x += ctx.Printf(x, 0, style, text)
+ }
+ }
+ if a.showInfo {
+ infoText := ""
+ for i := 0; i < len(a.authdetails.Infos); i++ {
+ if i > 0 {
+ infoText += ","
+ }
+ infoText += a.authdetails.Infos[i]
+ if reason := a.authdetails.Reasons[i]; reason != "" {
+ infoText += reason
+ }
+ }
+ if checkBounds(x) && infoText != "" {
+ if trunc := ctx.Width() - x - 3; trunc > 0 {
+ text = runewidth.Truncate(infoText, trunc, "…")
+ x += ctx.Printf(x, 0, defaultStyle, fmt.Sprintf(" (%s)", text))
+ }
+ }
+ }
+ }
+}
+
+func (a *AuthInfo) Invalidate() {
+ a.DoInvalidate(a)
+}
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index c88c981..9876467 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -17,6 +17,7 @@ import (
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
+ "git.sr.ht/~rjarry/aerc/lib/auth"
"git.sr.ht/~rjarry/aerc/lib/format"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging"
@@ -61,13 +62,29 @@ func NewMessageViewer(acct *AccountView,
layout := hf.forMessage(msg.MessageInfo())
header, headerHeight := layout.grid(
func(header string) ui.Drawable {
- return &HeaderView{
+ hv := &HeaderView{
conf: conf,
Name: header,
Value: fmtHeader(msg.MessageInfo(), header,
acct.UiConfig().TimestampFormat),
uiConfig: acct.UiConfig(),
}
+ showInfo := false
+ if i := strings.IndexRune(header, '+'); i > 0 {
+ header = header[:i]
+ hv.Name = header
+ showInfo = true
+ }
+ if parser := auth.New(header); parser != nil {
+ details, err := parser(msg.MessageInfo().RFC822Headers, acct.AccountConfig().TrustedAuthRes)
+ if err != nil {
+ hv.Value = err.Error()
+ } else {
+ hv.ValueField = NewAuthInfo(details, showInfo, acct.UiConfig())
+ }
+ hv.Invalidate()
+ }
+ return hv
},
)
@@ -134,6 +151,10 @@ func fmtHeader(msg *models.MessageInfo, header string, timefmt string) string {
return "error: no envelope for this message"
}
+ if v := auth.New(header); v != nil {
+ return "Fetching.."
+ }
+
switch header {
case "From":
return format.FormatAddresses(msg.Envelope.From)
@@ -796,16 +817,20 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
type HeaderView struct {
ui.Invalidatable
- conf *config.AercConfig
- Name string
- Value string
- uiConfig config.UIConfig
+ conf *config.AercConfig
+ Name string
+ Value string
+ ValueField ui.Drawable
+ uiConfig config.UIConfig
}
func (hv *HeaderView) Draw(ctx *ui.Context) {
name := hv.Name
size := runewidth.StringWidth(name + ":")
lim := ctx.Width() - size - 1
+ if lim <= 0 || ctx.Height() <= 0 {
+ return
+ }
value := runewidth.Truncate(" "+hv.Value, lim, "…")
vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
@@ -818,7 +843,11 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
ctx.Printf(0, 0, hstyle, "%s:", name)
- ctx.Printf(size, 0, vstyle, "%s", value)
+ if hv.ValueField == nil {
+ ctx.Printf(size, 0, vstyle, "%s", value)
+ } else {
+ hv.ValueField.Draw(ctx.Subcontext(size, 0, lim, 1))
+ }
}
func (hv *HeaderView) Invalidate() {