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>
This commit is contained in:
parent
115447e57f
commit
83e0e2638d
7 changed files with 295 additions and 6 deletions
widgets
93
widgets/authinfo.go
Normal file
93
widgets/authinfo.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue