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 <robin@jarry.cc>
Reviewed-by: Koni Marti <koni.marti@gmail.com>
Tested-by: akspecs <akspecs@gmail.com>
This commit is contained in:
Robin Jarry 2022-02-24 21:10:30 +01:00
parent c26d08103b
commit 115dabb634
2 changed files with 107 additions and 11 deletions

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"os/exec" "os/exec"
"sort"
"time" "time"
"git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/commands"
@ -108,22 +109,73 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
} }
if pipeFull { if pipeFull {
store := provider.Store() var uids []uint32
if store == nil { var title string
return errors.New("Cannot perform action. Messages still loading")
} h := newHelper(aerc)
msg, err := provider.SelectedMessage() store, err := h.store()
if err != nil { if err != nil {
return err return err
} }
store.FetchFull([]uint32{msg.Uid}, func(fm *types.FullMessage) { uids, err = h.markedOrSelectedUids()
if background { if err != nil {
doExec(fm.Content.Reader) return err
} else { }
doTerm(fm.Content.Reader, fmt.Sprintf(
"%s <%s", cmd[0], msg.Envelope.Subject)) 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 { } else if pipePart {
p := provider.SelectedMessagePart() p := provider.SelectedMessagePart()
if p == nil { if p == nil {
@ -143,3 +195,44 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
return nil 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
}

View file

@ -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 message part is used in the message viewer and the full message is used in
the message list. 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 *-b*: Run the command in the background instead of opening a terminal tab
*-m*: Pipe the full message *-m*: Pipe the full message