~sircmpwn/aerc

maildir: configurable external command for search. v1 PROPOSED

Antoine POPINEAU: 1
 maildir: configurable external command for search.

 4 files changed, 105 insertions(+), 0 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/15171/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH] maildir: configurable external command for search. Export this patch

A new configration parameter is introduced: search-cmd allows to set an
optional external command that will be used to search for emails in the current
directory, replacing the default internal search method, when the account has a
maildir:// source.

In the search-command value, the placeholder {} will be replaced by the
provided terms. So, for example, a search-cmd value `notmuch search {}`, with
the command `:search FOOBAR` will be executed as `notmuch search "FOOBAR"`.

This command must output the list of email absolute paths, one per line.
---
 config/config.go         |  1 +
 doc/aerc-maildir.5.scd   | 14 +++++++++
 worker/maildir/search.go | 61 ++++++++++++++++++++++++++++++++++++++++
 worker/maildir/worker.go | 29 +++++++++++++++++++
 4 files changed, 105 insertions(+)

diff --git a/config/config.go b/config/config.go
index 51982d2..c566e7d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -87,6 +87,7 @@ type AccountConfig struct {
	SignatureFile   string
	SignatureCmd    string
	FoldersSort     []string `ini:"folders-sort" delim:","`
	SearchCmd       []string `ini:"search-cmd" delim:" "`
}

type BindingConfig struct {
diff --git a/doc/aerc-maildir.5.scd b/doc/aerc-maildir.5.scd
index 37a654b..14f0a2e 100644
--- a/doc/aerc-maildir.5.scd
+++ b/doc/aerc-maildir.5.scd
@@ -29,6 +29,20 @@ The following maildir-specific options are available:

		source = maildir://~/mail

*search-cmd*
	Specifies the command to execute to run a search on the current maildir and
	directory. The placeholder *{}* will be substituted with the provided search
	terms. The command must return the absolute paths to matching emails, one per
	line.

	The path to the executed script must be must be either an absolute path
	prefixed by */* or a command included in your environment's *PATH*. For
	example:

		search-cmd = /home/me/search-command search {}

		search-cmd = search-command search {}

# SEE ALSO

*aerc*(1) *aerc-config*(5) *aerc-smtp*(5) *aerc-notmuch*(5)
diff --git a/worker/maildir/search.go b/worker/maildir/search.go
index ad3a45f..49b60a2 100644
--- a/worker/maildir/search.go
+++ b/worker/maildir/search.go
@@ -1,8 +1,10 @@
package maildir

import (
	"errors"
	"io/ioutil"
	"net/textproto"
	"os/exec"
	"strings"
	"unicode"

@@ -108,6 +110,65 @@ func (w *Worker) search(criteria *searchCriteria) ([]uint32, error) {
	return matchedUids, nil
}

func (w *Worker) searchByCmd(cmdspec []string, term string) ([]uint32, error) {
	keys, err := w.c.UIDs(*w.selected)
	if err != nil {
		return nil, err
	}

	bin := cmdspec[0]
	args := make([]string, len(cmdspec)-1)
	hasTerm := false

	if len(args) > 0 {
		// Builds the args list, replacing the first found {} placeholder with
		// the provided search term.
		for idx, a := range cmdspec[1:] {
			if !hasTerm && a == "{}" {
				args[idx] = term
				hasTerm = true
			} else {
				args[idx] = a
			}
		}
	}

	if !hasTerm {
		return nil, errors.New("Search command does not include '{}', search term cannot be used.")
	}

	w.c.log.Printf("Searching with external command %s %s", bin, args)

	cmd := exec.Command(bin, args...)
	stdout, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	matches := strings.Split(strings.TrimSpace(string(stdout)), "\n")
	uids := make([]uint32, 0)

	for _, key := range keys {
		msg, err := w.c.Message(*w.selected, key)
		if err != nil {
			continue
		}

		filename, err := w.selected.Filename(msg.key)
		if err != nil {
			continue
		}

		for _, m := range matches {
			if m == filename {
				uids = append(uids, key)
			}
		}
	}

	return uids, nil
}

// Execute the search criteria for the given key, returns true if search succeeded
func (w *Worker) searchKey(key uint32, criteria *searchCriteria,
	parts MsgParts) (bool, error) {
diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
index 4a7ae51..48a7015 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -7,6 +7,7 @@ import (
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/emersion/go-maildir"
	"github.com/fsnotify/fsnotify"
@@ -31,6 +32,7 @@ type Worker struct {
	worker              *types.Worker
	watcher             *fsnotify.Watcher
	currentSortCriteria []*types.SortCriterion
	searchCmd           []string
}

// NewWorker creates a new maildir worker with the provided worker.
@@ -234,6 +236,8 @@ func (w *Worker) handleConfigure(msg *types.Configure) error {
	}
	w.c = c
	w.worker.Logger.Printf("configured base maildir: %s", dir)
	w.searchCmd = msg.Config.SearchCmd
	w.worker.Logger.Printf("configured external search command: %s", msg.Config.SearchCmd)
	return nil
}

@@ -575,6 +579,11 @@ func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error {

func (w *Worker) handleSearchDirectory(msg *types.SearchDirectory) error {
	w.worker.Logger.Printf("Searching directory %v with args: %v", *w.selected, msg.Argv)

	if w.searchCmd != nil {
		return w.handleDirectorySearchByCmd(msg)
	}

	criteria, err := parseSearch(msg.Argv)
	if err != nil {
		return err
@@ -590,3 +599,23 @@ func (w *Worker) handleSearchDirectory(msg *types.SearchDirectory) error {
	}, nil)
	return nil
}

func (w *Worker) handleDirectorySearchByCmd(msg *types.SearchDirectory) error {
	if len(msg.Argv) < 2 {
		return errors.New("No search term was provided")
	}

	term := strings.Join(msg.Argv[1:], " ")

	uids, err := w.searchByCmd(w.searchCmd, term)
	if err != nil {
		return err
	}

	w.worker.PostMessage(&types.SearchResults{
		Message: types.RespondTo(msg),
		Uids:    uids,
	}, nil)

	return nil
}
-- 
2.29.2