forward: provide option to append all attachments

Append all non-multipart attachments with the -A flag. Rename the flag
for forwarding a full message as an RFC2822 attachments to -F.

Suggested-by: psykose
Signed-off-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Koni Marti 2022-06-28 23:42:08 +02:00 committed by Robin Jarry
parent 9d90b70b4e
commit 60052c6070
4 changed files with 131 additions and 17 deletions

View File

@ -6,9 +6,11 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math/rand"
"os" "os"
"path" "path"
"strings" "strings"
"sync"
"git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/lib/format"
@ -35,29 +37,28 @@ func (forward) Complete(aerc *widgets.Aerc, args []string) []string {
} }
func (forward) Execute(aerc *widgets.Aerc, args []string) error { func (forward) Execute(aerc *widgets.Aerc, args []string) error {
opts, optind, err := getopt.Getopts(args, "AT:") opts, optind, err := getopt.Getopts(args, "AFT:")
if err != nil { if err != nil {
return err return err
} }
attach := false attachAll := false
attachFull := false
template := "" template := ""
var tolist []*mail.Address
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
case 'A': case 'A':
attach = true attachAll = true
to := strings.Join(args[optind:], ", ") case 'F':
if strings.Contains(to, "@") { attachFull = true
tolist, err = mail.ParseAddressList(to)
if err != nil {
return fmt.Errorf("invalid to address(es): %v", err)
}
}
case 'T': case 'T':
template = opt.Value template = opt.Value
} }
} }
if attachAll && attachFull {
return errors.New("Options -A and -F are mutually exclusive")
}
widget := aerc.SelectedTab().(widgets.ProvidesMessage) widget := aerc.SelectedTab().(widgets.ProvidesMessage)
acct := widget.SelectedAccount() acct := widget.SelectedAccount()
if acct == nil { if acct == nil {
@ -77,6 +78,14 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
subject := "Fwd: " + msg.Envelope.Subject subject := "Fwd: " + msg.Envelope.Subject
h.SetSubject(subject) h.SetSubject(subject)
var tolist []*mail.Address
to := strings.Join(args[optind:], ", ")
if strings.Contains(to, "@") {
tolist, err = mail.ParseAddressList(to)
if err != nil {
return fmt.Errorf("invalid to address(es): %v", err)
}
}
if len(tolist) > 0 { if len(tolist) > 0 {
h.SetAddressList("to", tolist) h.SetAddressList("to", tolist)
} }
@ -112,7 +121,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
return composer, nil return composer, nil
} }
if attach { if attachFull {
tmpDir, err := ioutil.TempDir("", "aerc-tmp-attachment") tmpDir, err := ioutil.TempDir("", "aerc-tmp-attachment")
if err != nil { if err != nil {
return err return err
@ -144,7 +153,6 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
template = aerc.Config().Templates.Forwards template = aerc.Config().Templates.Forwards
} }
// TODO: add attachments!
part := lib.FindPlaintext(msg.BodyStructure, nil) part := lib.FindPlaintext(msg.BodyStructure, nil)
if part == nil { if part == nil {
part = lib.FindFirstNonMultipart(msg.BodyStructure, nil) part = lib.FindFirstNonMultipart(msg.BodyStructure, nil)
@ -158,7 +166,38 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(reader) buf.ReadFrom(reader)
original.Text = buf.String() original.Text = buf.String()
addTab()
// create composer
composer, err := addTab()
if err != nil {
return
}
// add attachments
if attachAll {
var mu sync.Mutex
parts := lib.FindAllNonMultipart(msg.BodyStructure, nil, nil)
for _, p := range parts {
if lib.EqualParts(p, part) {
continue
}
bs, err := msg.BodyStructure.PartAtIndex(p)
if err != nil {
acct.Logger().Println("forward: PartAtIndex:", err)
continue
}
store.FetchBodyPart(msg.Uid, 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 return nil

View File

@ -135,14 +135,16 @@ message list, the message in the message viewer, etc).
directory. The original message will be deleted only if it is in the directory. The original message will be deleted only if it is in the
postpone directory. postpone directory.
*forward* [-A] [-T <template-file>] [address...] *forward* [-A | -F] [-T <template-file>] [address...]
Opens the composer to forward the selected message to another recipient. Opens the composer to forward the selected message to another recipient.
*-A*: Forward the message as an RFC 2822 attachment. *-A*: Forward the message and all attachments.
*-F*: Forward the full message as an RFC 2822 attachment.
*-T* <template-file> *-T* <template-file>
Use the specified template file for creating the initial Use the specified template file for creating the initial
message body. Unless *-A* is specified, this defaults to what message body. Unless *-F* is specified, this defaults to what
is set as _forwards_ in the _[templates]_ section of is set as _forwards_ in the _[templates]_ section of
_aerc.conf_. _aerc.conf_.

View File

@ -52,3 +52,30 @@ func FindFirstNonMultipart(bs *models.BodyStructure, path []int) []int {
} }
return nil return nil
} }
func FindAllNonMultipart(bs *models.BodyStructure, path []int, pathlist [][]int) [][]int {
for i, part := range bs.Parts {
cur := append(path, i+1)
mimetype := strings.ToLower(part.MIMEType)
if mimetype != "multipart" {
pathlist = append(pathlist, cur)
} else if mimetype == "multipart" {
if sub := FindAllNonMultipart(part, cur, nil); len(sub) > 0 {
pathlist = append(pathlist, sub...)
}
}
}
return pathlist
}
func EqualParts(a []int, b []int) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@ -0,0 +1,46 @@
package lib_test
import (
"testing"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/models"
)
func TestLib_FindAllNonMultipart(t *testing.T) {
testStructure := &models.BodyStructure{
MIMEType: "multipart",
Parts: []*models.BodyStructure{
&models.BodyStructure{},
&models.BodyStructure{
MIMEType: "multipart",
Parts: []*models.BodyStructure{
&models.BodyStructure{},
&models.BodyStructure{},
},
},
&models.BodyStructure{},
},
}
expected := [][]int{
[]int{1},
[]int{2, 1},
[]int{2, 2},
[]int{3},
}
parts := lib.FindAllNonMultipart(testStructure, nil, nil)
if len(expected) != len(parts) {
t.Errorf("incorrect dimensions; expected: %v, got: %v", expected, parts)
}
for i := 0; i < len(parts); i++ {
if !lib.EqualParts(expected[i], parts[i]) {
t.Errorf("incorrect values; expected: %v, got: %v", expected[i], parts[i])
}
}
}