aerc/commands/msg/recall.go
Koni Marti e19b411e52 recall: support pgp/mime messages
PGP/MIME messages are stored encrypted and/or signed in the draft folder
for security reasons. Recall will open them through the lib.MessageView
interface in order to display the message content properly in the
composer tab. If the stored message was encrypted or signed, the
recalled message in the composer will also be encrypted or signed.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-07-10 20:39:53 +02:00

220 lines
5.1 KiB
Go

package msg
import (
"fmt"
"io"
"math/rand"
"sync"
"time"
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
"github.com/pkg/errors"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~sircmpwn/getopt"
)
type Recall struct{}
func init() {
register(Recall{})
}
func (Recall) Aliases() []string {
return []string{"recall"}
}
func (Recall) Complete(aerc *widgets.Aerc, args []string) []string {
return nil
}
func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
force := false
opts, optind, err := getopt.Getopts(args, "f")
if err != nil {
return err
}
for _, opt := range opts {
switch opt.Option {
case 'f':
force = true
}
}
if len(args) != optind {
return errors.New("Usage: recall [-f]")
}
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
acct := widget.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if acct.SelectedDirectory() != acct.AccountConfig().Postpone && !force {
return errors.New("Use -f to recall from outside the " +
acct.AccountConfig().Postpone + " directory.")
}
store := widget.Store()
if store == nil {
return errors.New("Cannot perform action. Messages still loading")
}
msgInfo, err := widget.SelectedMessage()
if err != nil {
return errors.Wrap(err, "Recall failed")
}
acct.Logger().Println("Recalling message " + msgInfo.Envelope.MessageId)
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
acct.AccountConfig(), acct.Worker(), "", msgInfo.RFC822Headers,
models.OriginalMail{})
if err != nil {
return errors.Wrap(err, "Cannot open a new composer")
}
// focus the terminal since the header fields are likely already done
composer.FocusTerminal()
addTab := func() {
subject := msgInfo.Envelope.Subject
if subject == "" {
subject = "Recalled email"
}
tab := aerc.NewTab(composer, subject)
composer.OnHeaderChange("Subject", func(subject string) {
if subject == "" {
tab.Name = "New email"
} else {
tab.Name = subject
}
tab.Content.Invalidate()
})
composer.OnClose(func(composer *widgets.Composer) {
worker := composer.Worker()
uids := []uint32{msgInfo.Uid}
if acct.SelectedDirectory() != acct.AccountConfig().Postpone {
return
}
deleteMessage := func() {
worker.PostAction(&types.DeleteMessages{
Uids: uids,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
aerc.PushStatus("Recalled message deleted", 10*time.Second)
case *types.Error:
aerc.PushError(msg.Error.Error())
}
})
}
if composer.Sent() {
deleteMessage()
} else {
confirm := widgets.NewSelectorDialog(
"Delete recalled message?",
"If you proceed, the recalled message will be deleted.",
[]string{"Cancel", "Proceed"}, 0, aerc.SelectedAccountUiConfig(),
func(option string, err error) {
aerc.CloseDialog()
switch option {
case "Proceed":
deleteMessage()
default:
}
return
},
)
aerc.AddDialog(confirm)
}
})
}
lib.NewMessageStoreView(msgInfo, store, aerc.Crypto, aerc.DecryptKeys,
func(msg lib.MessageView, err error) {
if err != nil {
aerc.PushError(err.Error())
return
}
var (
path []int
part *models.BodyStructure
)
if len(msg.BodyStructure().Parts) != 0 {
path = lib.FindPlaintext(msg.BodyStructure(), path)
}
part, err = msg.BodyStructure().PartAtIndex(path)
if part == nil || err != nil {
part = msg.BodyStructure()
}
msg.FetchBodyPart(path, func(reader io.Reader) {
header := message.Header{}
header.SetText(
"Content-Transfer-Encoding", part.Encoding)
header.SetContentType(part.MIMEType, part.Params)
header.SetText("Content-Description", part.Description)
entity, err := message.New(header, reader)
if err != nil {
aerc.PushError(err.Error())
addTab()
return
}
mreader := mail.NewReader(entity)
part, err := mreader.NextPart()
if err != nil {
aerc.PushError(err.Error())
addTab()
return
}
composer.SetContents(part.Body)
if md := msg.MessageDetails(); md != nil {
if md.IsEncrypted {
composer.SetEncrypt(md.IsEncrypted)
}
if md.IsSigned {
composer.SetSign(md.IsSigned)
}
}
addTab()
// add attachements if present
var mu sync.Mutex
parts := lib.FindAllNonMultipart(msg.BodyStructure(), nil, nil)
for _, p := range parts {
if lib.EqualParts(p, path) {
continue
}
bs, err := msg.BodyStructure().PartAtIndex(p)
if err != nil {
acct.Logger().Println("recall: PartAtIndex:", err)
continue
}
msg.FetchBodyPart(p, func(reader io.Reader) {
mime := fmt.Sprintf("%s/%s", bs.MIMEType, bs.MIMESubType)
name, ok := bs.Params["name"]
if !ok {
name = fmt.Sprintf("%s_%s_%d", bs.MIMEType, bs.MIMESubType, rand.Uint64())
}
mu.Lock()
composer.AddPartAttachment(name, mime, bs.Params, reader)
mu.Unlock()
})
}
})
})
return nil
}