2019-03-30 19:12:04 +01:00
|
|
|
package widgets
|
|
|
|
|
|
|
|
import (
|
2019-06-07 17:14:40 +02:00
|
|
|
"bufio"
|
2019-07-10 02:04:21 +02:00
|
|
|
"errors"
|
2019-03-31 18:14:37 +02:00
|
|
|
"fmt"
|
2019-03-30 19:12:04 +01:00
|
|
|
"io"
|
2020-07-28 09:59:03 +02:00
|
|
|
"os"
|
2019-03-30 19:12:04 +01:00
|
|
|
"os/exec"
|
2019-06-07 17:14:40 +02:00
|
|
|
"regexp"
|
2019-04-07 16:34:38 +02:00
|
|
|
"strings"
|
2019-03-30 19:12:04 +01:00
|
|
|
|
2019-03-31 20:24:53 +02:00
|
|
|
"github.com/danwakefield/fnmatch"
|
2020-11-30 23:07:03 +01:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2019-03-31 20:24:53 +02:00
|
|
|
"github.com/google/shlex"
|
2019-03-30 19:12:04 +01:00
|
|
|
"github.com/mattn/go-runewidth"
|
|
|
|
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/auth"
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/format"
|
2022-06-14 21:10:48 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/parse"
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
2022-03-22 09:52:27 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/logging"
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
2019-03-30 19:12:04 +01:00
|
|
|
)
|
|
|
|
|
2020-01-04 20:36:54 +01:00
|
|
|
var ansi = regexp.MustCompile("\x1B\\[[0-?]*[ -/]*[@-~]")
|
2019-06-07 17:14:40 +02:00
|
|
|
|
2019-12-18 06:33:58 +01:00
|
|
|
var _ ProvidesMessages = (*MessageViewer)(nil)
|
|
|
|
|
2019-03-30 19:12:04 +01:00
|
|
|
type MessageViewer struct {
|
2019-05-20 22:42:44 +02:00
|
|
|
ui.Invalidatable
|
2019-06-02 07:15:04 +02:00
|
|
|
acct *AccountView
|
2019-05-20 20:56:52 +02:00
|
|
|
conf *config.AercConfig
|
|
|
|
err error
|
|
|
|
grid *ui.Grid
|
2019-05-20 22:42:44 +02:00
|
|
|
switcher *PartSwitcher
|
2020-03-03 22:20:07 +01:00
|
|
|
msg lib.MessageView
|
2022-07-03 17:11:12 +02:00
|
|
|
uiConfig *config.UIConfig
|
2019-05-20 22:42:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type PartSwitcher struct {
|
|
|
|
ui.Invalidatable
|
2019-07-17 22:51:02 +02:00
|
|
|
parts []*PartViewer
|
|
|
|
selected int
|
|
|
|
showHeaders bool
|
|
|
|
alwaysShowMime bool
|
2019-09-06 00:32:36 +02:00
|
|
|
|
|
|
|
height int
|
|
|
|
mv *MessageViewer
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
func NewMessageViewer(acct *AccountView,
|
2022-07-31 22:16:40 +02:00
|
|
|
conf *config.AercConfig, msg lib.MessageView,
|
|
|
|
) *MessageViewer {
|
2019-12-23 12:51:59 +01:00
|
|
|
hf := HeaderLayoutFilter{
|
|
|
|
layout: HeaderLayout(conf.Viewer.HeaderLayout),
|
|
|
|
keep: func(msg *models.MessageInfo, header string) bool {
|
2022-03-09 22:48:00 +01:00
|
|
|
return fmtHeader(msg, header, "2") != ""
|
2019-12-23 12:51:59 +01:00
|
|
|
},
|
|
|
|
}
|
2020-03-03 22:20:07 +01:00
|
|
|
layout := hf.forMessage(msg.MessageInfo())
|
2019-07-23 01:29:07 +02:00
|
|
|
header, headerHeight := layout.grid(
|
|
|
|
func(header string) ui.Drawable {
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
hv := &HeaderView{
|
2020-07-27 10:03:55 +02:00
|
|
|
conf: conf,
|
2020-03-03 22:20:07 +01:00
|
|
|
Name: header,
|
|
|
|
Value: fmtHeader(msg.MessageInfo(), header,
|
|
|
|
acct.UiConfig().TimestampFormat),
|
2020-07-27 10:03:55 +02:00
|
|
|
uiConfig: acct.UiConfig(),
|
2019-07-23 01:29:07 +02:00
|
|
|
}
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
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
|
2019-07-23 01:29:07 +02:00
|
|
|
},
|
|
|
|
)
|
2019-03-30 19:12:04 +01:00
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
rows := []ui.GridSpec{
|
2022-03-18 09:53:02 +01:00
|
|
|
{Strategy: ui.SIZE_EXACT, Size: ui.Const(headerHeight)},
|
2020-03-03 22:20:07 +01:00
|
|
|
}
|
|
|
|
|
2022-06-22 12:19:40 +02:00
|
|
|
if msg.MessageDetails() != nil || conf.Ui.IconUnencrypted != "" {
|
2020-03-03 22:20:07 +01:00
|
|
|
height := 1
|
2022-06-22 12:19:40 +02:00
|
|
|
if msg.MessageDetails() != nil && msg.MessageDetails().IsSigned && msg.MessageDetails().IsEncrypted {
|
2020-03-03 22:20:07 +01:00
|
|
|
height = 2
|
|
|
|
}
|
2022-03-18 09:53:02 +01:00
|
|
|
rows = append(rows, ui.GridSpec{Strategy: ui.SIZE_EXACT, Size: ui.Const(height)})
|
2020-03-03 22:20:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
rows = append(rows, []ui.GridSpec{
|
2022-03-18 09:53:02 +01:00
|
|
|
{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)},
|
|
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
2020-03-03 22:20:07 +01:00
|
|
|
}...)
|
|
|
|
|
|
|
|
grid := ui.NewGrid().Rows(rows).Columns([]ui.GridSpec{
|
2022-03-18 09:53:02 +01:00
|
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
2019-03-30 19:12:04 +01:00
|
|
|
})
|
|
|
|
|
2019-05-20 22:42:44 +02:00
|
|
|
switcher := &PartSwitcher{}
|
2020-03-03 22:20:07 +01:00
|
|
|
err := createSwitcher(acct, switcher, conf, msg)
|
2019-06-07 10:26:14 +02:00
|
|
|
if err != nil {
|
2019-07-15 20:56:44 +02:00
|
|
|
return &MessageViewer{
|
2020-07-27 10:03:55 +02:00
|
|
|
err: err,
|
|
|
|
grid: grid,
|
|
|
|
msg: msg,
|
|
|
|
uiConfig: acct.UiConfig(),
|
2019-07-15 20:56:44 +02:00
|
|
|
}
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
|
|
|
|
2021-10-26 22:42:07 +02:00
|
|
|
borderStyle := acct.UiConfig().GetStyle(config.STYLE_BORDER)
|
2021-11-29 23:48:35 +01:00
|
|
|
borderChar := acct.UiConfig().BorderCharHorizontal
|
2021-10-26 22:42:07 +02:00
|
|
|
|
2019-07-15 20:56:44 +02:00
|
|
|
grid.AddChild(header).At(0, 0)
|
2022-06-22 12:19:40 +02:00
|
|
|
if msg.MessageDetails() != nil || conf.Ui.IconUnencrypted != "" {
|
2022-04-25 15:30:43 +02:00
|
|
|
grid.AddChild(NewPGPInfo(msg.MessageDetails(), acct.UiConfig())).At(1, 0)
|
2021-11-29 23:48:35 +01:00
|
|
|
grid.AddChild(ui.NewFill(borderChar, borderStyle)).At(2, 0)
|
2020-03-03 22:20:07 +01:00
|
|
|
grid.AddChild(switcher).At(3, 0)
|
|
|
|
} else {
|
2021-11-29 23:48:35 +01:00
|
|
|
grid.AddChild(ui.NewFill(borderChar, borderStyle)).At(1, 0)
|
2020-03-03 22:20:07 +01:00
|
|
|
grid.AddChild(switcher).At(2, 0)
|
|
|
|
}
|
2019-05-20 20:56:52 +02:00
|
|
|
|
2019-09-06 00:32:36 +02:00
|
|
|
mv := &MessageViewer{
|
2019-06-02 07:15:04 +02:00
|
|
|
acct: acct,
|
2019-06-07 10:26:14 +02:00
|
|
|
conf: conf,
|
2019-05-20 22:42:44 +02:00
|
|
|
grid: grid,
|
|
|
|
msg: msg,
|
|
|
|
switcher: switcher,
|
2020-07-27 10:03:55 +02:00
|
|
|
uiConfig: acct.UiConfig(),
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
2019-09-06 00:32:36 +02:00
|
|
|
switcher.mv = mv
|
|
|
|
|
|
|
|
return mv
|
2019-07-15 20:56:44 +02:00
|
|
|
}
|
2019-05-20 20:56:52 +02:00
|
|
|
|
2019-12-18 19:26:25 +01:00
|
|
|
func fmtHeader(msg *models.MessageInfo, header string, timefmt string) string {
|
2022-04-01 13:39:15 +02:00
|
|
|
if msg == nil || msg.Envelope == nil {
|
|
|
|
return "error: no envelope for this message"
|
|
|
|
}
|
|
|
|
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
if v := auth.New(header); v != nil {
|
|
|
|
return "Fetching.."
|
|
|
|
}
|
|
|
|
|
2019-07-15 20:56:44 +02:00
|
|
|
switch header {
|
|
|
|
case "From":
|
2020-08-19 12:01:45 +02:00
|
|
|
return format.FormatAddresses(msg.Envelope.From)
|
2019-07-15 20:56:44 +02:00
|
|
|
case "To":
|
2020-08-19 12:01:45 +02:00
|
|
|
return format.FormatAddresses(msg.Envelope.To)
|
2019-07-15 20:56:44 +02:00
|
|
|
case "Cc":
|
2020-08-19 12:01:45 +02:00
|
|
|
return format.FormatAddresses(msg.Envelope.Cc)
|
2019-07-15 20:56:44 +02:00
|
|
|
case "Bcc":
|
2020-08-19 12:01:45 +02:00
|
|
|
return format.FormatAddresses(msg.Envelope.Bcc)
|
2019-07-15 20:56:44 +02:00
|
|
|
case "Date":
|
2019-12-18 19:26:25 +01:00
|
|
|
return msg.Envelope.Date.Local().Format(timefmt)
|
2019-07-15 20:56:44 +02:00
|
|
|
case "Subject":
|
|
|
|
return msg.Envelope.Subject
|
2019-12-23 12:51:59 +01:00
|
|
|
case "Labels":
|
|
|
|
return strings.Join(msg.Labels, ", ")
|
2019-07-15 20:56:44 +02:00
|
|
|
default:
|
|
|
|
return msg.RFC822Headers.Get(header)
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
func enumerateParts(acct *AccountView, conf *config.AercConfig,
|
|
|
|
msg lib.MessageView, body *models.BodyStructure,
|
2022-07-31 22:16:40 +02:00
|
|
|
index []int,
|
|
|
|
) ([]*PartViewer, error) {
|
2019-05-20 22:42:44 +02:00
|
|
|
var parts []*PartViewer
|
|
|
|
for i, part := range body.Parts {
|
2022-07-31 14:32:48 +02:00
|
|
|
curindex := append(index, i+1) //nolint:gocritic // intentional append to different slice
|
2019-05-20 22:42:44 +02:00
|
|
|
if part.MIMEType == "multipart" {
|
|
|
|
// Multipart meta-parts are faked
|
|
|
|
pv := &PartViewer{part: part}
|
|
|
|
parts = append(parts, pv)
|
|
|
|
subParts, err := enumerateParts(
|
2020-03-03 22:20:07 +01:00
|
|
|
acct, conf, msg, part, curindex)
|
2019-05-20 22:42:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
parts = append(parts, subParts...)
|
|
|
|
continue
|
|
|
|
}
|
2020-03-03 22:20:07 +01:00
|
|
|
pv, err := NewPartViewer(acct, conf, msg, part, curindex)
|
2019-05-20 22:42:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
parts = append(parts, pv)
|
|
|
|
}
|
|
|
|
return parts, nil
|
|
|
|
}
|
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
func createSwitcher(acct *AccountView, switcher *PartSwitcher,
|
2022-07-31 22:16:40 +02:00
|
|
|
conf *config.AercConfig, msg lib.MessageView,
|
|
|
|
) error {
|
2019-06-07 10:26:14 +02:00
|
|
|
var err error
|
2019-07-19 23:26:43 +02:00
|
|
|
switcher.selected = -1
|
2019-07-17 22:49:28 +02:00
|
|
|
switcher.showHeaders = conf.Viewer.ShowHeaders
|
2019-07-17 22:51:02 +02:00
|
|
|
switcher.alwaysShowMime = conf.Viewer.AlwaysShowMime
|
2019-06-07 10:26:14 +02:00
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
if len(msg.BodyStructure().Parts) == 0 {
|
2019-07-17 23:09:35 +02:00
|
|
|
switcher.selected = 0
|
2020-06-19 17:58:08 +02:00
|
|
|
pv, err := NewPartViewer(acct, conf, msg, msg.BodyStructure(), nil)
|
2019-06-07 10:26:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
switcher.parts = []*PartViewer{pv}
|
|
|
|
pv.OnInvalidate(func(_ ui.Drawable) {
|
|
|
|
switcher.Invalidate()
|
|
|
|
})
|
|
|
|
} else {
|
2020-03-03 22:20:07 +01:00
|
|
|
switcher.parts, err = enumerateParts(acct, conf, msg,
|
|
|
|
msg.BodyStructure(), []int{})
|
2019-06-07 10:26:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-06-08 20:29:26 +02:00
|
|
|
selectedPriority := -1
|
2022-07-19 22:31:51 +02:00
|
|
|
logging.Infof("Selecting best message from %v", conf.Viewer.Alternatives)
|
2019-06-07 10:26:14 +02:00
|
|
|
for i, pv := range switcher.parts {
|
|
|
|
pv.OnInvalidate(func(_ ui.Drawable) {
|
|
|
|
switcher.Invalidate()
|
|
|
|
})
|
2019-06-08 20:29:26 +02:00
|
|
|
// Switch to user's preferred mimetype
|
2019-06-07 10:26:14 +02:00
|
|
|
if switcher.selected == -1 && pv.part.MIMEType != "multipart" {
|
|
|
|
switcher.selected = i
|
2019-07-19 23:26:43 +02:00
|
|
|
}
|
2019-08-01 22:19:57 +02:00
|
|
|
mime := strings.ToLower(pv.part.MIMEType) +
|
|
|
|
"/" + strings.ToLower(pv.part.MIMESubType)
|
|
|
|
for idx, m := range conf.Viewer.Alternatives {
|
|
|
|
if m != mime {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
priority := len(conf.Viewer.Alternatives) - idx
|
|
|
|
if priority > selectedPriority {
|
|
|
|
selectedPriority = priority
|
|
|
|
switcher.selected = i
|
2019-06-08 20:29:26 +02:00
|
|
|
}
|
2019-06-07 10:26:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
|
|
|
if mv.err != nil {
|
2020-07-27 10:03:55 +02:00
|
|
|
style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
|
|
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
|
|
|
ctx.Printf(0, 0, style, "%s", mv.err.Error())
|
2019-05-20 20:56:52 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
mv.grid.Draw(ctx)
|
|
|
|
}
|
|
|
|
|
2019-09-06 00:32:36 +02:00
|
|
|
func (mv *MessageViewer) MouseEvent(localX int, localY int, event tcell.Event) {
|
|
|
|
if mv.err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
mv.grid.MouseEvent(localX, localY, event)
|
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (mv *MessageViewer) Invalidate() {
|
|
|
|
mv.grid.Invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) {
|
|
|
|
mv.grid.OnInvalidate(func(_ ui.Drawable) {
|
|
|
|
fn(mv)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-06-02 07:15:04 +02:00
|
|
|
func (mv *MessageViewer) Store() *lib.MessageStore {
|
2020-03-03 22:20:07 +01:00
|
|
|
return mv.msg.Store()
|
2019-06-02 07:15:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (mv *MessageViewer) SelectedAccount() *AccountView {
|
|
|
|
return mv.acct
|
|
|
|
}
|
|
|
|
|
2022-06-23 18:26:19 +02:00
|
|
|
func (mv *MessageViewer) MessageView() lib.MessageView {
|
|
|
|
return mv.msg
|
|
|
|
}
|
|
|
|
|
2019-07-10 02:04:21 +02:00
|
|
|
func (mv *MessageViewer) SelectedMessage() (*models.MessageInfo, error) {
|
|
|
|
if mv.msg == nil {
|
|
|
|
return nil, errors.New("no message selected")
|
|
|
|
}
|
2020-03-03 22:20:07 +01:00
|
|
|
return mv.msg.MessageInfo(), nil
|
2019-06-02 07:15:04 +02:00
|
|
|
}
|
|
|
|
|
2020-05-09 11:50:31 +02:00
|
|
|
func (mv *MessageViewer) MarkedMessages() ([]uint32, error) {
|
2022-08-08 22:21:41 +02:00
|
|
|
return mv.acct.MarkedMessages()
|
2019-12-18 06:33:58 +01:00
|
|
|
}
|
|
|
|
|
2019-06-07 10:26:14 +02:00
|
|
|
func (mv *MessageViewer) ToggleHeaders() {
|
|
|
|
switcher := mv.switcher
|
2022-07-23 01:17:46 +02:00
|
|
|
switcher.Cleanup()
|
2019-07-17 22:49:28 +02:00
|
|
|
mv.conf.Viewer.ShowHeaders = !mv.conf.Viewer.ShowHeaders
|
2020-03-03 22:20:07 +01:00
|
|
|
err := createSwitcher(mv.acct, switcher, mv.conf, mv.msg)
|
2019-06-07 10:26:14 +02:00
|
|
|
if err != nil {
|
2022-07-19 22:31:51 +02:00
|
|
|
logging.Errorf("cannot create switcher: %v", err)
|
2019-06-07 10:26:14 +02:00
|
|
|
}
|
|
|
|
switcher.Invalidate()
|
|
|
|
}
|
|
|
|
|
2022-03-14 04:03:34 +01:00
|
|
|
func (mv *MessageViewer) ToggleKeyPassthrough() bool {
|
|
|
|
mv.conf.Viewer.KeyPassthrough = !mv.conf.Viewer.KeyPassthrough
|
|
|
|
return mv.conf.Viewer.KeyPassthrough
|
|
|
|
}
|
|
|
|
|
2019-07-05 18:21:12 +02:00
|
|
|
func (mv *MessageViewer) SelectedMessagePart() *PartInfo {
|
2019-05-26 23:37:39 +02:00
|
|
|
switcher := mv.switcher
|
|
|
|
part := switcher.parts[switcher.selected]
|
|
|
|
|
|
|
|
return &PartInfo{
|
|
|
|
Index: part.index,
|
2020-03-03 22:20:07 +01:00
|
|
|
Msg: part.msg.MessageInfo(),
|
2019-05-26 23:37:39 +02:00
|
|
|
Part: part.part,
|
2022-06-14 21:10:48 +02:00
|
|
|
Links: part.links,
|
2019-05-26 23:37:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-24 09:26:06 +01:00
|
|
|
func (mv *MessageViewer) AttachmentParts() []*PartInfo {
|
|
|
|
var attachments []*PartInfo
|
|
|
|
|
|
|
|
for _, p := range mv.switcher.parts {
|
|
|
|
if p.part.Disposition == "attachment" {
|
|
|
|
pi := &PartInfo{
|
|
|
|
Index: p.index,
|
|
|
|
Msg: p.msg.MessageInfo(),
|
|
|
|
Part: p.part,
|
|
|
|
}
|
|
|
|
attachments = append(attachments, pi)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return attachments
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:49:39 +02:00
|
|
|
func (mv *MessageViewer) PreviousPart() {
|
|
|
|
switcher := mv.switcher
|
|
|
|
for {
|
|
|
|
switcher.selected--
|
|
|
|
if switcher.selected < 0 {
|
|
|
|
switcher.selected = len(switcher.parts) - 1
|
|
|
|
}
|
|
|
|
if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mv.Invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mv *MessageViewer) NextPart() {
|
|
|
|
switcher := mv.switcher
|
|
|
|
for {
|
|
|
|
switcher.selected++
|
|
|
|
if switcher.selected >= len(switcher.parts) {
|
|
|
|
switcher.selected = 0
|
|
|
|
}
|
|
|
|
if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mv.Invalidate()
|
|
|
|
}
|
|
|
|
|
2022-03-14 04:03:34 +01:00
|
|
|
func (mv *MessageViewer) Bindings() string {
|
|
|
|
if mv.conf.Viewer.KeyPassthrough {
|
|
|
|
return "view::passthrough"
|
|
|
|
} else {
|
|
|
|
return "view"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-16 19:29:36 +02:00
|
|
|
func (mv *MessageViewer) Close() error {
|
2019-10-15 11:01:47 +02:00
|
|
|
mv.switcher.Cleanup()
|
2020-04-16 19:29:36 +02:00
|
|
|
return nil
|
2019-10-15 11:01:47 +02:00
|
|
|
}
|
|
|
|
|
2022-08-08 22:04:04 +02:00
|
|
|
func (mv *MessageViewer) UpdateScreen() {
|
|
|
|
if mv.switcher == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
parts := mv.switcher.parts
|
|
|
|
selected := mv.switcher.selected
|
|
|
|
if selected < 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(parts) > 0 && selected < len(parts) {
|
|
|
|
if part := parts[selected]; part != nil {
|
|
|
|
part.UpdateScreen()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:42:44 +02:00
|
|
|
func (ps *PartSwitcher) Invalidate() {
|
|
|
|
ps.DoInvalidate(ps)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ps *PartSwitcher) Focus(focus bool) {
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(focus)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ps *PartSwitcher) Event(event tcell.Event) bool {
|
2019-11-15 21:28:34 +01:00
|
|
|
return ps.parts[ps.selected].Event(event)
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
|
|
|
|
2019-05-20 22:42:44 +02:00
|
|
|
func (ps *PartSwitcher) Draw(ctx *ui.Context) {
|
|
|
|
height := len(ps.parts)
|
2019-07-17 22:51:02 +02:00
|
|
|
if height == 1 && !ps.alwaysShowMime {
|
2019-05-20 22:42:44 +02:00
|
|
|
ps.parts[ps.selected].Draw(ctx)
|
|
|
|
return
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
2019-05-20 22:42:44 +02:00
|
|
|
// TODO: cap height and add scrolling for messages with many parts
|
2019-09-06 00:32:36 +02:00
|
|
|
ps.height = ctx.Height()
|
2019-05-20 22:42:44 +02:00
|
|
|
y := ctx.Height() - height
|
|
|
|
for i, part := range ps.parts {
|
2020-07-27 10:03:55 +02:00
|
|
|
style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
|
|
|
if ps.selected == i {
|
|
|
|
style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
|
|
|
|
}
|
2019-05-20 22:42:44 +02:00
|
|
|
ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
|
2019-05-20 23:03:37 +02:00
|
|
|
name := fmt.Sprintf("%s/%s",
|
2019-05-20 22:42:44 +02:00
|
|
|
strings.ToLower(part.part.MIMEType),
|
|
|
|
strings.ToLower(part.part.MIMESubType))
|
2019-05-20 23:03:37 +02:00
|
|
|
if filename, ok := part.part.DispositionParams["filename"]; ok {
|
|
|
|
name += fmt.Sprintf(" (%s)", filename)
|
2019-12-07 19:58:12 +01:00
|
|
|
} else if filename, ok := part.part.Params["name"]; ok {
|
|
|
|
// workaround golang not supporting RFC2231 besides ASCII and UTF8
|
|
|
|
name += fmt.Sprintf(" (%s)", filename)
|
2019-05-20 23:03:37 +02:00
|
|
|
}
|
|
|
|
ctx.Printf(len(part.index)*2, y+i, style, "%s", name)
|
2019-05-20 22:42:44 +02:00
|
|
|
}
|
|
|
|
ps.parts[ps.selected].Draw(ctx.Subcontext(
|
|
|
|
0, 0, ctx.Width(), ctx.Height()-height))
|
|
|
|
}
|
|
|
|
|
2019-09-06 00:32:36 +02:00
|
|
|
func (ps *PartSwitcher) MouseEvent(localX int, localY int, event tcell.Event) {
|
2022-07-31 14:32:48 +02:00
|
|
|
if event, ok := event.(*tcell.EventMouse); ok {
|
2019-09-06 00:32:36 +02:00
|
|
|
switch event.Buttons() {
|
|
|
|
case tcell.Button1:
|
|
|
|
height := len(ps.parts)
|
|
|
|
y := ps.height - height
|
2019-11-15 20:02:50 +01:00
|
|
|
if localY < y && ps.parts[ps.selected].term != nil {
|
2019-09-06 00:32:36 +02:00
|
|
|
ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
|
|
|
|
}
|
2020-05-31 13:37:46 +02:00
|
|
|
for i := range ps.parts {
|
2019-09-06 00:32:36 +02:00
|
|
|
if localY != y+i {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if ps.parts[i].part.MIMEType == "multipart" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(false)
|
|
|
|
}
|
|
|
|
ps.selected = i
|
|
|
|
ps.Invalidate()
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case tcell.WheelDown:
|
|
|
|
height := len(ps.parts)
|
|
|
|
y := ps.height - height
|
2020-02-23 09:21:29 +01:00
|
|
|
if localY < y && ps.parts[ps.selected].term != nil {
|
2019-09-06 00:32:36 +02:00
|
|
|
ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
|
|
|
|
}
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(false)
|
|
|
|
}
|
|
|
|
ps.mv.NextPart()
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(true)
|
|
|
|
}
|
|
|
|
case tcell.WheelUp:
|
|
|
|
height := len(ps.parts)
|
|
|
|
y := ps.height - height
|
2020-02-23 09:21:29 +01:00
|
|
|
if localY < y && ps.parts[ps.selected].term != nil {
|
2019-09-06 00:32:36 +02:00
|
|
|
ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
|
|
|
|
}
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(false)
|
|
|
|
}
|
|
|
|
ps.mv.PreviousPart()
|
|
|
|
if ps.parts[ps.selected].term != nil {
|
|
|
|
ps.parts[ps.selected].term.Focus(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-15 11:01:47 +02:00
|
|
|
func (ps *PartSwitcher) Cleanup() {
|
|
|
|
for _, partViewer := range ps.parts {
|
|
|
|
partViewer.Cleanup()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:42:44 +02:00
|
|
|
func (mv *MessageViewer) Event(event tcell.Event) bool {
|
|
|
|
return mv.switcher.Event(event)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mv *MessageViewer) Focus(focus bool) {
|
|
|
|
mv.switcher.Focus(focus)
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type PartViewer struct {
|
2019-05-20 22:42:44 +02:00
|
|
|
ui.Invalidatable
|
2020-07-27 10:03:55 +02:00
|
|
|
conf *config.AercConfig
|
2022-04-20 14:21:06 +02:00
|
|
|
acctConfig *config.AccountConfig
|
2019-06-07 10:26:14 +02:00
|
|
|
err error
|
|
|
|
fetched bool
|
|
|
|
filter *exec.Cmd
|
|
|
|
index []int
|
2020-03-03 22:20:07 +01:00
|
|
|
msg lib.MessageView
|
2019-06-07 10:26:14 +02:00
|
|
|
pager *exec.Cmd
|
|
|
|
pagerin io.WriteCloser
|
2019-07-08 04:43:58 +02:00
|
|
|
part *models.BodyStructure
|
2019-06-07 10:26:14 +02:00
|
|
|
showHeaders bool
|
|
|
|
sink io.WriteCloser
|
|
|
|
source io.Reader
|
|
|
|
term *Terminal
|
2019-11-15 21:28:34 +01:00
|
|
|
grid *ui.Grid
|
2022-07-03 17:11:12 +02:00
|
|
|
uiConfig *config.UIConfig
|
2022-06-14 21:10:48 +02:00
|
|
|
|
|
|
|
links []string
|
2019-05-20 20:56:52 +02:00
|
|
|
}
|
|
|
|
|
2019-11-15 21:28:34 +01:00
|
|
|
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
2020-03-03 22:20:07 +01:00
|
|
|
msg lib.MessageView, part *models.BodyStructure,
|
2022-07-31 22:16:40 +02:00
|
|
|
index []int,
|
|
|
|
) (*PartViewer, error) {
|
2019-03-31 20:24:53 +02:00
|
|
|
var (
|
|
|
|
filter *exec.Cmd
|
|
|
|
pager *exec.Cmd
|
|
|
|
pipe io.WriteCloser
|
|
|
|
pagerin io.WriteCloser
|
2019-03-31 20:32:26 +02:00
|
|
|
term *Terminal
|
2019-03-31 20:24:53 +02:00
|
|
|
)
|
|
|
|
cmd, err := shlex.Split(conf.Viewer.Pager)
|
|
|
|
if err != nil {
|
2019-05-20 20:56:52 +02:00
|
|
|
return nil, err
|
2019-03-31 20:24:53 +02:00
|
|
|
}
|
2019-05-20 20:56:52 +02:00
|
|
|
|
2019-03-31 20:24:53 +02:00
|
|
|
pager = exec.Command(cmd[0], cmd[1:]...)
|
|
|
|
|
2020-03-03 22:20:07 +01:00
|
|
|
info := msg.MessageInfo()
|
2019-03-31 20:24:53 +02:00
|
|
|
for _, f := range conf.Filters {
|
2019-05-20 20:56:52 +02:00
|
|
|
mime := strings.ToLower(part.MIMEType) +
|
|
|
|
"/" + strings.ToLower(part.MIMESubType)
|
2019-03-31 20:24:53 +02:00
|
|
|
switch f.FilterType {
|
|
|
|
case config.FILTER_MIMETYPE:
|
|
|
|
if fnmatch.Match(f.Filter, mime, 0) {
|
2019-03-31 21:21:04 +02:00
|
|
|
filter = exec.Command("sh", "-c", f.Command)
|
2019-03-31 20:24:53 +02:00
|
|
|
}
|
2019-03-31 20:42:18 +02:00
|
|
|
case config.FILTER_HEADER:
|
|
|
|
var header string
|
|
|
|
switch f.Header {
|
|
|
|
case "subject":
|
2020-03-03 22:20:07 +01:00
|
|
|
header = info.Envelope.Subject
|
2019-03-31 20:42:18 +02:00
|
|
|
case "from":
|
2020-08-19 12:01:45 +02:00
|
|
|
header = format.FormatAddresses(info.Envelope.From)
|
2019-03-31 20:42:18 +02:00
|
|
|
case "to":
|
2020-08-19 12:01:45 +02:00
|
|
|
header = format.FormatAddresses(info.Envelope.To)
|
2019-03-31 20:42:18 +02:00
|
|
|
case "cc":
|
2020-08-19 12:01:45 +02:00
|
|
|
header = format.FormatAddresses(info.Envelope.Cc)
|
2021-10-25 18:13:24 +02:00
|
|
|
default:
|
|
|
|
header = msg.MessageInfo().RFC822Headers.Get(f.Header)
|
2019-03-31 20:42:18 +02:00
|
|
|
}
|
|
|
|
if f.Regex.Match([]byte(header)) {
|
2019-03-31 21:21:04 +02:00
|
|
|
filter = exec.Command("sh", "-c", f.Command)
|
2019-03-31 20:42:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if filter != nil {
|
|
|
|
break
|
2019-03-31 20:24:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if filter != nil {
|
2019-05-20 20:56:52 +02:00
|
|
|
if pipe, err = filter.StdinPipe(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if pagerin, _ = pager.StdinPipe(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-05-20 22:42:44 +02:00
|
|
|
if term, err = NewTerminal(pager); err != nil {
|
2019-05-20 20:56:52 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2019-03-31 20:24:53 +02:00
|
|
|
|
2019-11-15 21:28:34 +01:00
|
|
|
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
2022-03-18 09:53:02 +01:00
|
|
|
{Strategy: ui.SIZE_EXACT, Size: ui.Const(3)}, // Message
|
|
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
2019-11-15 21:28:34 +01:00
|
|
|
}).Columns([]ui.GridSpec{
|
2022-03-18 09:53:02 +01:00
|
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
2019-11-15 21:28:34 +01:00
|
|
|
})
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
pv := &PartViewer{
|
2020-07-27 10:03:55 +02:00
|
|
|
conf: conf,
|
2022-04-20 14:21:06 +02:00
|
|
|
acctConfig: acct.AccountConfig(),
|
2019-06-07 10:26:14 +02:00
|
|
|
filter: filter,
|
|
|
|
index: index,
|
|
|
|
msg: msg,
|
|
|
|
pager: pager,
|
|
|
|
pagerin: pagerin,
|
|
|
|
part: part,
|
2019-07-17 22:49:28 +02:00
|
|
|
showHeaders: conf.Viewer.ShowHeaders,
|
2019-06-07 10:26:14 +02:00
|
|
|
sink: pipe,
|
|
|
|
term: term,
|
2019-11-15 21:28:34 +01:00
|
|
|
grid: grid,
|
2020-07-27 10:03:55 +02:00
|
|
|
uiConfig: acct.UiConfig(),
|
2019-03-31 18:14:37 +02:00
|
|
|
}
|
|
|
|
|
2019-05-20 22:42:44 +02:00
|
|
|
if term != nil {
|
|
|
|
term.OnStart = func() {
|
|
|
|
pv.attemptCopy()
|
|
|
|
}
|
|
|
|
term.OnInvalidate(func(_ ui.Drawable) {
|
|
|
|
pv.Invalidate()
|
|
|
|
})
|
2019-03-31 18:35:51 +02:00
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
return pv, nil
|
|
|
|
}
|
2019-03-31 20:32:26 +02:00
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (pv *PartViewer) SetSource(reader io.Reader) {
|
|
|
|
pv.source = reader
|
|
|
|
pv.attemptCopy()
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
2022-08-08 22:04:04 +02:00
|
|
|
func (pv *PartViewer) UpdateScreen() {
|
|
|
|
if pv.term != nil {
|
|
|
|
pv.term.Invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (pv *PartViewer) attemptCopy() {
|
2020-07-28 09:51:36 +02:00
|
|
|
if pv.source == nil || pv.pager == nil || pv.pager.Process == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if pv.filter != nil {
|
2022-07-31 22:16:40 +02:00
|
|
|
pv.copyFilterOutToPager() // delayed until we write to the sink
|
2020-07-28 09:51:36 +02:00
|
|
|
}
|
|
|
|
go func() {
|
2022-03-22 09:52:27 +01:00
|
|
|
defer logging.PanicHandler()
|
|
|
|
|
2020-07-28 09:51:36 +02:00
|
|
|
pv.writeMailHeaders()
|
2020-09-06 16:35:28 +02:00
|
|
|
if strings.EqualFold(pv.part.MIMEType, "text") {
|
2020-07-28 09:51:36 +02:00
|
|
|
// if the content is plain we can strip ansi control chars
|
|
|
|
pv.copySourceToSinkStripAnsi()
|
|
|
|
} else {
|
|
|
|
// if it's binary we have to rely on the filter to be sane
|
2022-07-29 22:31:54 +02:00
|
|
|
_, err := io.Copy(pv.sink, pv.source)
|
|
|
|
if err != nil {
|
2022-10-02 11:36:05 +02:00
|
|
|
logging.Warnf("failed to copy: %v", err)
|
2022-07-29 22:31:54 +02:00
|
|
|
}
|
2019-03-31 20:24:53 +02:00
|
|
|
}
|
2020-07-28 09:51:36 +02:00
|
|
|
pv.sink.Close()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pv *PartViewer) writeMailHeaders() {
|
|
|
|
info := pv.msg.MessageInfo()
|
|
|
|
if pv.showHeaders && info.RFC822Headers != nil {
|
|
|
|
// header need to bypass the filter, else we run into issues
|
|
|
|
// with the filter messing with newlines etc.
|
|
|
|
// hence all writes in this block go directly to the pager
|
|
|
|
fields := info.RFC822Headers.Fields()
|
|
|
|
for fields.Next() {
|
|
|
|
var value string
|
|
|
|
var err error
|
|
|
|
if value, err = fields.Text(); err != nil {
|
|
|
|
// better than nothing, use the non decoded version
|
|
|
|
value = fields.Value()
|
2019-06-07 10:26:14 +02:00
|
|
|
}
|
2020-07-28 09:51:36 +02:00
|
|
|
field := fmt.Sprintf(
|
|
|
|
"%s: %s\n", fields.Key(), value)
|
2022-07-29 22:31:54 +02:00
|
|
|
_, err = pv.pagerin.Write([]byte(field))
|
|
|
|
if err != nil {
|
|
|
|
logging.Errorf("failed to write to stdin of pager: %v", err)
|
|
|
|
}
|
2020-07-28 09:51:36 +02:00
|
|
|
}
|
|
|
|
// virtual header
|
|
|
|
if len(info.Labels) != 0 {
|
|
|
|
labels := fmtHeader(info, "Labels", "")
|
2022-07-29 22:31:54 +02:00
|
|
|
_, err := pv.pagerin.Write([]byte(fmt.Sprintf("Labels: %s\n", labels)))
|
|
|
|
if err != nil {
|
|
|
|
logging.Errorf("failed to write to stdin of pager: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_, err := pv.pagerin.Write([]byte{'\n'})
|
|
|
|
if err != nil {
|
|
|
|
logging.Errorf("failed to write to stdin of pager: %v", err)
|
2020-07-28 09:51:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-06-07 10:26:14 +02:00
|
|
|
|
2022-06-14 21:10:48 +02:00
|
|
|
func (pv *PartViewer) hyperlinks(r io.Reader) (reader io.Reader) {
|
|
|
|
if !pv.conf.Viewer.ParseHttpLinks {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
reader, pv.links = parse.HttpLinks(r)
|
|
|
|
return reader
|
|
|
|
}
|
|
|
|
|
2020-07-28 09:51:36 +02:00
|
|
|
func (pv *PartViewer) copyFilterOutToPager() {
|
|
|
|
stdout, _ := pv.filter.StdoutPipe()
|
|
|
|
stderr, _ := pv.filter.StderrPipe()
|
2022-07-29 22:31:54 +02:00
|
|
|
err := pv.filter.Start()
|
|
|
|
if err != nil {
|
|
|
|
logging.Warnf("failed to start filter: %v", err)
|
|
|
|
}
|
2020-07-28 09:51:36 +02:00
|
|
|
ch := make(chan interface{})
|
|
|
|
go func() {
|
2022-03-22 09:52:27 +01:00
|
|
|
defer logging.PanicHandler()
|
|
|
|
|
2020-07-28 09:51:36 +02:00
|
|
|
_, err := io.Copy(pv.pagerin, stdout)
|
|
|
|
if err != nil {
|
|
|
|
pv.err = err
|
|
|
|
pv.Invalidate()
|
|
|
|
}
|
|
|
|
stdout.Close()
|
|
|
|
ch <- nil
|
|
|
|
}()
|
|
|
|
go func() {
|
2022-03-22 09:52:27 +01:00
|
|
|
defer logging.PanicHandler()
|
|
|
|
|
2020-07-28 09:51:36 +02:00
|
|
|
_, err := io.Copy(pv.pagerin, stderr)
|
|
|
|
if err != nil {
|
|
|
|
pv.err = err
|
|
|
|
pv.Invalidate()
|
|
|
|
}
|
|
|
|
stderr.Close()
|
|
|
|
ch <- nil
|
|
|
|
}()
|
|
|
|
go func() {
|
2022-03-22 09:52:27 +01:00
|
|
|
defer logging.PanicHandler()
|
|
|
|
|
2020-07-28 09:51:36 +02:00
|
|
|
<-ch
|
|
|
|
<-ch
|
2022-07-29 22:31:54 +02:00
|
|
|
err := pv.filter.Wait()
|
|
|
|
if err != nil {
|
|
|
|
logging.Warnf("failed to wait for the filter process: %v", err)
|
|
|
|
}
|
2020-07-28 09:51:36 +02:00
|
|
|
pv.pagerin.Close()
|
2022-09-14 21:09:41 +02:00
|
|
|
// If the pager command doesn't keep the terminal running, we
|
|
|
|
// risk not drawing the screen until user input unless we
|
|
|
|
// invalidate after writing
|
|
|
|
pv.Invalidate()
|
2020-07-28 09:51:36 +02:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pv *PartViewer) copySourceToSinkStripAnsi() {
|
2022-06-14 21:10:48 +02:00
|
|
|
scanner := bufio.NewScanner(pv.hyperlinks(pv.source))
|
2020-07-28 09:59:03 +02:00
|
|
|
// some people send around huge html without any newline in between
|
|
|
|
// this did overflow the default 64KB buffer of bufio.Scanner.
|
|
|
|
// If something can't fit in a GB there's no hope left
|
|
|
|
scanner.Buffer(nil, 1024*1024*1024)
|
2020-07-28 09:51:36 +02:00
|
|
|
for scanner.Scan() {
|
|
|
|
text := scanner.Text()
|
|
|
|
text = ansi.ReplaceAllString(text, "")
|
2022-07-29 22:31:54 +02:00
|
|
|
_, err := io.WriteString(pv.sink, text+"\n")
|
|
|
|
if err != nil {
|
|
|
|
logging.Warnf("failed write ", err)
|
|
|
|
}
|
2019-03-31 18:35:51 +02:00
|
|
|
}
|
2020-07-28 09:59:03 +02:00
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "failed to read line: %v\n", err)
|
|
|
|
}
|
2019-03-31 18:35:51 +02:00
|
|
|
}
|
|
|
|
|
2022-04-20 14:21:06 +02:00
|
|
|
var noFilterConfiguredCommands = [][]string{
|
|
|
|
{":open<enter>", "Open using the system handler"},
|
|
|
|
{":save<space>", "Save to file"},
|
|
|
|
{":pipe<space>", "Pipe to shell command"},
|
|
|
|
}
|
|
|
|
|
|
|
|
func newNoFilterConfigured(pv *PartViewer) *ui.Grid {
|
|
|
|
bindings := pv.conf.MergeContextualBinds(
|
|
|
|
pv.conf.Bindings.MessageView,
|
|
|
|
config.BIND_CONTEXT_ACCOUNT,
|
|
|
|
pv.acctConfig.Name,
|
|
|
|
"view",
|
|
|
|
)
|
|
|
|
|
|
|
|
var actions []string
|
|
|
|
|
|
|
|
for _, command := range noFilterConfiguredCommands {
|
|
|
|
cmd := command[0]
|
|
|
|
name := command[1]
|
|
|
|
strokes, _ := config.ParseKeyStrokes(cmd)
|
|
|
|
var inputs []string
|
|
|
|
for _, input := range bindings.GetReverseBindings(strokes) {
|
|
|
|
inputs = append(inputs, config.FormatKeyStrokes(input))
|
|
|
|
}
|
|
|
|
actions = append(actions, fmt.Sprintf(" %-6s %-29s %s",
|
2022-07-31 14:32:48 +02:00
|
|
|
strings.Join(inputs, ", "), name, cmd))
|
2022-04-20 14:21:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
spec := []ui.GridSpec{
|
|
|
|
{Strategy: ui.SIZE_EXACT, Size: ui.Const(2)},
|
|
|
|
}
|
|
|
|
for i := 0; i < len(actions)-1; i++ {
|
|
|
|
spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)})
|
|
|
|
}
|
|
|
|
// make the last element fill remaining space
|
|
|
|
spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)})
|
|
|
|
|
|
|
|
grid := ui.NewGrid().Rows(spec).Columns([]ui.GridSpec{
|
|
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
|
|
})
|
|
|
|
|
|
|
|
uiConfig := pv.conf.Ui
|
|
|
|
|
|
|
|
noFilter := fmt.Sprintf(`No filter configured for this mimetype ('%s/%s')
|
|
|
|
What would you like to do?`, pv.part.MIMEType, pv.part.MIMESubType)
|
|
|
|
grid.AddChild(ui.NewText(noFilter,
|
|
|
|
uiConfig.GetStyle(config.STYLE_TITLE))).At(0, 0)
|
|
|
|
for i, action := range actions {
|
|
|
|
grid.AddChild(ui.NewText(action,
|
|
|
|
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i+1, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
return grid
|
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (pv *PartViewer) Invalidate() {
|
2019-05-20 22:42:44 +02:00
|
|
|
pv.DoInvalidate(pv)
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
2019-05-20 20:56:52 +02:00
|
|
|
func (pv *PartViewer) Draw(ctx *ui.Context) {
|
2020-07-27 10:03:55 +02:00
|
|
|
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
2019-05-20 22:42:44 +02:00
|
|
|
if pv.filter == nil {
|
2020-07-27 10:03:55 +02:00
|
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
2022-04-20 14:21:06 +02:00
|
|
|
newNoFilterConfigured(pv).Draw(ctx)
|
2019-05-20 22:42:44 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if !pv.fetched {
|
2020-05-17 11:44:38 +02:00
|
|
|
pv.msg.FetchBodyPart(pv.index, pv.SetSource)
|
2019-05-20 22:42:44 +02:00
|
|
|
pv.fetched = true
|
|
|
|
}
|
2019-05-20 20:56:52 +02:00
|
|
|
if pv.err != nil {
|
2020-07-27 10:03:55 +02:00
|
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
|
|
|
ctx.Printf(0, 0, style, "%s", pv.err.Error())
|
2019-05-20 20:56:52 +02:00
|
|
|
return
|
2019-03-31 20:32:26 +02:00
|
|
|
}
|
2022-09-15 21:39:04 +02:00
|
|
|
if pv.term != nil {
|
|
|
|
pv.term.Draw(ctx)
|
|
|
|
}
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
2019-10-15 11:01:47 +02:00
|
|
|
func (pv *PartViewer) Cleanup() {
|
2022-09-15 16:39:29 +02:00
|
|
|
if pv.term != nil {
|
|
|
|
pv.term.Close(nil)
|
2019-10-15 11:01:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-15 21:28:34 +01:00
|
|
|
func (pv *PartViewer) Event(event tcell.Event) bool {
|
|
|
|
if pv.term != nil {
|
|
|
|
return pv.term.Event(event)
|
|
|
|
}
|
2022-04-20 14:21:06 +02:00
|
|
|
return false
|
2019-11-15 21:28:34 +01:00
|
|
|
}
|
|
|
|
|
2019-03-30 19:12:04 +01:00
|
|
|
type HeaderView struct {
|
2019-04-27 18:47:59 +02:00
|
|
|
ui.Invalidatable
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
conf *config.AercConfig
|
|
|
|
Name string
|
|
|
|
Value string
|
|
|
|
ValueField ui.Drawable
|
2022-07-03 17:11:12 +02:00
|
|
|
uiConfig *config.UIConfig
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
2019-03-31 18:14:37 +02:00
|
|
|
name := hv.Name
|
2021-11-05 10:34:10 +01:00
|
|
|
size := runewidth.StringWidth(name + ":")
|
2019-03-31 18:14:37 +02:00
|
|
|
lim := ctx.Width() - size - 1
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
if lim <= 0 || ctx.Height() <= 0 {
|
|
|
|
return
|
|
|
|
}
|
2019-03-31 18:14:37 +02:00
|
|
|
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
2020-07-27 10:03:55 +02:00
|
|
|
|
|
|
|
vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
|
|
|
hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)
|
|
|
|
|
2019-03-30 21:50:14 +01:00
|
|
|
// TODO: Make this more robust and less dumb
|
2019-03-30 20:55:21 +01:00
|
|
|
if hv.Name == "PGP" {
|
2020-07-27 10:03:55 +02:00
|
|
|
vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
2019-03-30 20:55:21 +01:00
|
|
|
}
|
2020-07-27 10:03:55 +02:00
|
|
|
|
2019-03-30 21:50:14 +01:00
|
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
2021-11-05 10:34:10 +01:00
|
|
|
ctx.Printf(0, 0, hstyle, "%s:", name)
|
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>
2022-05-30 00:20:41 +02:00
|
|
|
if hv.ValueField == nil {
|
|
|
|
ctx.Printf(size, 0, vstyle, "%s", value)
|
|
|
|
} else {
|
|
|
|
hv.ValueField.Draw(ctx.Subcontext(size, 0, lim, 1))
|
|
|
|
}
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (hv *HeaderView) Invalidate() {
|
2019-04-27 18:47:59 +02:00
|
|
|
hv.DoInvalidate(hv)
|
2019-03-30 19:12:04 +01:00
|
|
|
}
|