~sircmpwn/aerc

bbdf9df75e8597e38cf1d90145f22aab9bd95178 — Drew DeVault 2 years ago 711d228
Add basic filter implementation
6 files changed, 142 insertions(+), 42 deletions(-)

M commands/account/view-message.go
M config/aerc.conf
M config/config.go
M go.mod
M go.sum
M widgets/msgviewer.go
M commands/account/view-message.go => commands/account/view-message.go +1 -1
@@ 19,7 19,7 @@ func ViewMessage(aerc *widgets.Aerc, args []string) error {
	acct := aerc.SelectedAccount()
	store := acct.Messages().Store()
	msg := acct.Messages().Selected()
	viewer := widgets.NewMessageViewer(store, msg)
	viewer := widgets.NewMessageViewer(aerc.Config(), store, msg)
	aerc.NewTab(viewer, runewidth.Truncate(
		msg.Envelope.Subject, 32, "…"))
	return nil

M config/aerc.conf => config/aerc.conf +20 -25
@@ 54,32 54,12 @@ empty-message=(no messages)

[viewer]
#
# We can use different programs to display various kinds of email attachments.
# These programs will have the mail piped into them and are expected to output
# it ready to display on a terminal (you can include terminal control
# characters if you like, for colors and such). Emails will be stripped of
# non-printable characters before being piped into these commands, and will be
# encoded with UTF-8. These commands are invoked with sh and run
# non-interactively, and their output is piped into your pager command
# (interactively). The following environment variables will be set:
# Specifies the pager to use when displaying emails. Note that some filters
# may add ANSI codes to add color to rendered emails, so you may want to use a
# pager which supports ANSI codes.
#
# $WIDTH: the width of the terminal window
# $HEIGHT: the height of the terminal window
# $MIMETYPE: the email's mimetype
#
# You can use * as a wildcard for any subtype of a given mimetype. When
# displaying a text/* message and no command matches, the message will just be
# piped directly into your pager (after being stripped of non-printable
# characters).

# Examples:
#
#text/html=w3m -T text/html -cols $WIDTH -dump -o display_image=false -o display_link_number=true
text/*=fold -sw $WIDTH

#
# Default: less -r
pager=less -r
# Default: less -R
pager=less -R

#
# If an email offers several versions (multipart), you can configure which


@@ 89,6 69,21 @@ pager=less -r
# Default: text/plain,text/html
alternatives=text/plain,text/html

[filters]
#
# Filters allow you to pipe an email body through a shell command to render
# certain emails differently, e.g. highlighting them with ANSI escape codes.
#
# The first filter which matches the email's mimetype will be used, so order
# them from most to least specific.
#
# You can also match on non-mimetypes, by prefixing with the header to match
# against (non-case-sensitive) and a colon, e.g. subject:text will match a
# subject which contains "text". Use header~:regex to match against a regex.
subject~:PATCH=contrib/hldiff.py
text/html=w3m -T text/html -cols $(tput cols) -dump -o display_image=false -o display_link_number=true
text/*=contrib/plaintext.py

[lbinds]
#
# Binds are of the form <input keys> = <output keys>

M config/config.go => config/config.go +47 -0
@@ 22,6 22,12 @@ type UIConfig struct {
	EmptyMessage      string   `ini:"empty-message"`
}

const (
	FILTER_MIMETYPE = iota
	FILTER_HEADER
	FILTER_HEADER_REGEX
)

type AccountConfig struct {
	Default string
	Name    string


@@ 38,10 44,23 @@ type BindingConfig struct {
	Terminal    *KeyBindings
}

type FilterConfig struct {
	FilterType int
	Filter     string
	Command    string
}

type ViewerConfig struct {
	Pager        string
	Alternatives []string
}

type AercConfig struct {
	Bindings BindingConfig
	Ini      *ini.File       `ini:"-"`
	Accounts []AccountConfig `ini:"-"`
	Filters  []FilterConfig  `ini:"-"`
	Viewer   ViewerConfig    `ini:"-"`
	Ui       UIConfig
}



@@ 135,6 154,34 @@ func LoadConfig(root *string) (*AercConfig, error) {
			EmptyMessage:      "(no messages)",
		},
	}
	if filters, err := file.GetSection("filters"); err == nil {
		// TODO: Parse the filter more finely, e.g. parse the regex
		for match, cmd := range filters.KeysHash() {
			filter := FilterConfig{
				Command: cmd,
				Filter:  match,
			}
			if strings.Contains(match, "~:") {
				filter.FilterType = FILTER_HEADER_REGEX
			} else if strings.ContainsRune(match, ':') {
				filter.FilterType = FILTER_HEADER
			} else {
				filter.FilterType = FILTER_MIMETYPE
			}
			config.Filters = append(config.Filters, filter)
		}
	}
	if viewer, err := file.GetSection("viewer"); err == nil {
		if err := viewer.MapTo(&config.Viewer); err != nil {
			return nil, err
		}
		for key, val := range viewer.KeysHash() {
			switch key {
			case "alternatives":
				config.Viewer.Alternatives = strings.Split(val, ",")
			}
		}
	}
	if ui, err := file.GetSection("ui"); err == nil {
		if err := ui.MapTo(&config.Ui); err != nil {
			return nil, err

M go.mod => go.mod +3 -0
@@ 3,9 3,12 @@ module git.sr.ht/~sircmpwn/aerc2
require (
	git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a
	git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9
	github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
	github.com/emersion/go-imap v1.0.0-beta.1
	github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b
	github.com/emersion/go-message v0.9.2
	github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect
	github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe // indirect
	github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635
	github.com/gdamore/tcell v1.0.0
	github.com/go-ini/ini v1.42.0

M go.sum => go.sum +6 -0
@@ 22,14 22,20 @@ git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a h1:ktjo0NVokh
git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a/go.mod h1:hT88+cTemwwESbMptwC7O33qrJfQX0SgRWbXlndUS2c=
git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9 h1:WWPN5lf6KzXp3xWRrPQZ4MLR3yrFEI4Ysz7HSQ1G/yo=
git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9/go.mod h1:8Jmcax8M9nYoEwBhVBhv2ixLRCoUqlbQPE95VpPu43I=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap v1.0.0-beta.1 h1:bTCaVlUnb5mKoW9lEukusxguSYYZPer+q0g5t+vw5X0=
github.com/emersion/go-imap v1.0.0-beta.1/go.mod h1:oydmHwiyv92ZOiNfQY9BDax5heePWN8P2+W1B2T6qjc=
github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b h1:q4qkNe/W10qFGD3RWd4meQTkD0+Zrz0L4ekMvlptg60=
github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-message v0.9.2 h1:rJmtGZO1Z71PJDQXbC31EwzlJCsA/8kya6GnebSGp6I=
github.com/emersion/go-message v0.9.2/go.mod h1:m3cK90skCWxm5sIMs1sXxly4Tn9Plvcf6eayHZJ1NzM=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 h1:rDJPbyliyym8ZL/Wt71kdolp6yaD4fLIQz638E6JEt0=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI=
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ=
github.com/gdamore/tcell v1.0.0 h1:oaly4AkxvDT5ffKHV/n4L8iy6FxG2QkAVl0M6cjryuE=

M widgets/msgviewer.go => widgets/msgviewer.go +65 -16
@@ 6,24 6,30 @@ import (
	"io"
	"os/exec"

	"github.com/danwakefield/fnmatch"
	"github.com/emersion/go-imap"
	"github.com/emersion/go-message"
	"github.com/emersion/go-message/mail"
	"github.com/gdamore/tcell"
	"github.com/google/shlex"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc2/config"
	"git.sr.ht/~sircmpwn/aerc2/lib"
	"git.sr.ht/~sircmpwn/aerc2/lib/ui"
	"git.sr.ht/~sircmpwn/aerc2/worker/types"
)

type MessageViewer struct {
	cmd    *exec.Cmd
	msg    *types.MessageInfo
	source io.Reader
	sink   io.WriteCloser
	grid   *ui.Grid
	term   *Terminal
	conf    *config.AercConfig
	filter  *exec.Cmd
	msg     *types.MessageInfo
	pager   *exec.Cmd
	source  io.Reader
	pagerin io.WriteCloser
	sink    io.WriteCloser
	grid    *ui.Grid
	term    *Terminal
}

func formatAddresses(addrs []*imap.Address) string {


@@ 43,7 49,7 @@ func formatAddresses(addrs []*imap.Address) string {
	return val.String()
}

func NewMessageViewer(store *lib.MessageStore,
func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore,
	msg *types.MessageInfo) *MessageViewer {

	grid := ui.NewGrid().Rows([]ui.GridSpec{


@@ 86,9 92,40 @@ func NewMessageViewer(store *lib.MessageStore,
		{ui.SIZE_EXACT, 20},
	})

	cmd := exec.Command("less")
	pipe, _ := cmd.StdinPipe()
	term, _ := NewTerminal(cmd)
	var (
		filter  *exec.Cmd
		pager   *exec.Cmd
		pipe    io.WriteCloser
		pagerin io.WriteCloser
	)
	cmd, err := shlex.Split(conf.Viewer.Pager)
	if err != nil {
		panic(err) // TODO: something useful
	}
	pager = exec.Command(cmd[0], cmd[1:]...)

	for _, f := range conf.Filters {
		cmd, err := shlex.Split(f.Command)
		if err != nil {
			panic(err) // TODO: Something useful
		}
		mime := msg.BodyStructure.MIMEType + "/" + msg.BodyStructure.MIMESubType
		switch f.FilterType {
		case config.FILTER_MIMETYPE:
			if fnmatch.Match(f.Filter, mime, 0) {
				filter = exec.Command(cmd[0], cmd[1:]...)
				fmt.Printf("Using filter for %s: %s\n", mime, f.Command)
			}
		}
	}
	if filter != nil {
		pipe, _ = filter.StdinPipe()
		pagerin, _ = pager.StdinPipe()
	} else {
		pipe, _ = pager.StdinPipe()
	}

	term, _ := NewTerminal(pager)
	// TODO: configure multipart view. I left a spot for it in the grid
	body.AddChild(term).At(0, 0).Span(1, 2)



@@ 96,11 133,13 @@ func NewMessageViewer(store *lib.MessageStore,
	grid.AddChild(body).At(1, 0)

	viewer := &MessageViewer{
		cmd:  cmd,
		grid: grid,
		msg:  msg,
		sink: pipe,
		term: term,
		filter:  filter,
		grid:    grid,
		msg:     msg,
		pager:   pager,
		pagerin: pagerin,
		sink:    pipe,
		term:    term,
	}

	store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {


@@ 116,12 155,22 @@ func NewMessageViewer(store *lib.MessageStore,
}

func (mv *MessageViewer) attemptCopy() {
	if mv.source != nil && mv.cmd.Process != nil {
	if mv.source != nil && mv.pager.Process != nil {
		header := make(message.Header)
		header.Set("Content-Transfer-Encoding", mv.msg.BodyStructure.Encoding)
		header.SetContentType(
			mv.msg.BodyStructure.MIMEType, mv.msg.BodyStructure.Params)
		header.SetContentDescription(mv.msg.BodyStructure.Description)
		if mv.filter != nil {
			stdout, _ := mv.filter.StdoutPipe()
			mv.filter.Start()
			go func() {
				_, err := io.Copy(mv.pagerin, stdout)
				if err != nil {
					io.WriteString(mv.sink, err.Error())
				}
			}()
		}
		go func() {
			entity, err := message.New(header, mv.source)
			if err != nil {