about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2019-03-31 14:24:53 -0400
committerDrew DeVault <sir@cmpwn.com>2019-03-31 14:24:53 -0400
commitbbdf9df75e8597e38cf1d90145f22aab9bd95178 (patch)
treee721502779327758956694edffbd6e01762801e9
parent711d22891bd50646d1cf925fbf0b8a760c638fd0 (diff)
downloadaerc-bbdf9df75e8597e38cf1d90145f22aab9bd95178.tar.gz
Add basic filter implementation
-rw-r--r--commands/account/view-message.go2
-rw-r--r--config/aerc.conf45
-rw-r--r--config/config.go47
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--widgets/msgviewer.go81
6 files changed, 142 insertions, 42 deletions
diff --git a/commands/account/view-message.go b/commands/account/view-message.go
index bef2740..edafd09 100644
--- a/commands/account/view-message.go
+++ b/commands/account/view-message.go
@@ -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
diff --git a/config/aerc.conf b/config/aerc.conf
index 5a4317f..3a89151 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -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>
diff --git a/config/config.go b/config/config.go
index e5e332e..8d460ca 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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
diff --git a/go.mod b/go.mod
index 0492447..1a29797 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 7d2cf96..9eab1ee 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index ab42ee0..3a3e962 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -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 {