From 115dabb6346383c88525586c2ec75d60df6f25d3 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Thu, 24 Feb 2022 21:10:30 +0100 Subject: [PATCH] pipe: allow piping multiple marked messages When messages are marked, pipe their contents into the specified command. The messages are ordered according to their respective Message-Id headers. This allows applying complete patch series with a single command. When piping more than one message, make sure to write them in the mbox format as git am expects them to be. Link: https://en.wikipedia.org/wiki/Mbox Link: https://github.com/git/git/blob/v2.35.1/builtin/mailsplit.c#L15-L44 Signed-off-by: Robin Jarry Reviewed-by: Koni Marti Tested-by: akspecs --- commands/msg/pipe.go | 115 ++++++++++++++++++++++++++++++++++++++----- doc/aerc.1.scd | 3 ++ 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go index 58764fb..5d8a042 100644 --- a/commands/msg/pipe.go +++ b/commands/msg/pipe.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os/exec" + "sort" "time" "git.sr.ht/~rjarry/aerc/commands" @@ -108,22 +109,73 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { } if pipeFull { - store := provider.Store() - if store == nil { - return errors.New("Cannot perform action. Messages still loading") - } - msg, err := provider.SelectedMessage() + var uids []uint32 + var title string + + h := newHelper(aerc) + store, err := h.store() if err != nil { return err } - store.FetchFull([]uint32{msg.Uid}, func(fm *types.FullMessage) { - if background { - doExec(fm.Content.Reader) - } else { - doTerm(fm.Content.Reader, fmt.Sprintf( - "%s <%s", cmd[0], msg.Envelope.Subject)) + uids, err = h.markedOrSelectedUids() + if err != nil { + return err + } + + if len(uids) == 1 { + info := store.Messages[uids[0]] + if info != nil { + envelope := info.Envelope + if envelope != nil { + title = envelope.Subject + } + } + } + if title == "" { + title = fmt.Sprintf("%d messages", len(uids)) + } + + var messages []*types.FullMessage + done := make(chan bool, 1) + + store.FetchFull(uids, func(fm *types.FullMessage) { + messages = append(messages, fm) + if len(messages) == len(uids) { + done <- true } }) + + go func() { + select { + case <-done: + break + case <-time.After(30 * time.Second): + // TODO: find a better way to determine if store.FetchFull() + // has finished with some errors. + aerc.PushError("Failed to fetch all messages") + if len(messages) == 0 { + return + } + } + + // Sort all messages by increasing Message-Id header. + // This will ensure that patch series are applied in order. + sort.Slice(messages, func(i, j int) bool { + infoi := store.Messages[messages[i].Content.Uid] + infoj := store.Messages[messages[j].Content.Uid] + if infoi == nil || infoj == nil { + return false + } + return infoi.Envelope.MessageId < infoj.Envelope.MessageId + }) + + reader := newMessagesReader(messages) + if background { + doExec(reader) + } else { + doTerm(reader, fmt.Sprintf("%s <%s", cmd[0], title)) + } + }() } else if pipePart { p := provider.SelectedMessagePart() if p == nil { @@ -143,3 +195,44 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { return nil } + +// The actual sender address does not matter, nor does the date. This is mostly indended +// for git am which requires separators to look like something valid. +// https://github.com/git/git/blame/v2.35.1/builtin/mailsplit.c#L15-L44 +var mboxSeparator []byte = []byte("From ???@??? Tue Jun 23 16:32:49 1981\n") + +type messagesReader struct { + messages []*types.FullMessage + mbox bool + separatorNeeded bool +} + +func newMessagesReader(messages []*types.FullMessage) io.Reader { + needMboxSeparator := len(messages) > 1 + return &messagesReader{messages, needMboxSeparator, needMboxSeparator} +} + +func (mr *messagesReader) Read(p []byte) (n int, err error) { + for len(mr.messages) > 0 { + if mr.separatorNeeded { + offset := copy(p, mboxSeparator) + n, err = mr.messages[0].Content.Reader.Read(p[offset:]) + n += offset + mr.separatorNeeded = false + } else { + n, err = mr.messages[0].Content.Reader.Read(p) + } + if err == io.EOF { + mr.messages = mr.messages[1:] + mr.separatorNeeded = mr.mbox + } + if n > 0 || err != io.EOF { + if err == io.EOF && len(mr.messages) > 0 { + // Don't return EOF yet. More messages remain. + err = nil + } + return n, err + } + } + return 0, io.EOF +} diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 648bde6..27384ad 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -146,6 +146,9 @@ message list, the message in the message viewer, etc). message part is used in the message viewer and the full message is used in the message list. + Operates on multiple messages when they are marked. When piping multiple + messages, aerc will write them with mbox format separators. + *-b*: Run the command in the background instead of opening a terminal tab *-m*: Pipe the full message