From 60052c607011ab09fe204cf5adc0cc9e29b34cdd Mon Sep 17 00:00:00 2001 From: Koni Marti Date: Tue, 28 Jun 2022 23:42:08 +0200 Subject: [PATCH] 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 Tested-by: Tim Culverhouse --- commands/msg/forward.go | 67 +++++++++++++++++++++++++++-------- doc/aerc.1.scd | 8 +++-- lib/structure_helpers.go | 27 ++++++++++++++ lib/structure_helpers_test.go | 46 ++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 lib/structure_helpers_test.go diff --git a/commands/msg/forward.go b/commands/msg/forward.go index 9b234ef..bc5953f 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -6,9 +6,11 @@ import ( "fmt" "io" "io/ioutil" + "math/rand" "os" "path" "strings" + "sync" "git.sr.ht/~rjarry/aerc/lib" "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 { - opts, optind, err := getopt.Getopts(args, "AT:") + opts, optind, err := getopt.Getopts(args, "AFT:") if err != nil { return err } - attach := false + attachAll := false + attachFull := false template := "" - var tolist []*mail.Address for _, opt := range opts { switch opt.Option { case 'A': - attach = true - 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) - } - } + attachAll = true + case 'F': + attachFull = true case 'T': template = opt.Value } } + if attachAll && attachFull { + return errors.New("Options -A and -F are mutually exclusive") + } + widget := aerc.SelectedTab().(widgets.ProvidesMessage) acct := widget.SelectedAccount() if acct == nil { @@ -77,6 +78,14 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error { subject := "Fwd: " + msg.Envelope.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 { h.SetAddressList("to", tolist) } @@ -112,7 +121,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error { return composer, nil } - if attach { + if attachFull { tmpDir, err := ioutil.TempDir("", "aerc-tmp-attachment") if err != nil { return err @@ -144,7 +153,6 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error { template = aerc.Config().Templates.Forwards } - // TODO: add attachments! part := lib.FindPlaintext(msg.BodyStructure, nil) if part == 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.ReadFrom(reader) 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 diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 2c4fd31..f1a6a5b 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -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 postpone directory. -*forward* [-A] [-T ] [address...] +*forward* [-A | -F] [-T ] [address...] 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* 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 _aerc.conf_. diff --git a/lib/structure_helpers.go b/lib/structure_helpers.go index ac6950a..1b4a6e6 100644 --- a/lib/structure_helpers.go +++ b/lib/structure_helpers.go @@ -52,3 +52,30 @@ func FindFirstNonMultipart(bs *models.BodyStructure, path []int) []int { } 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 +} diff --git a/lib/structure_helpers_test.go b/lib/structure_helpers_test.go new file mode 100644 index 0000000..f670735 --- /dev/null +++ b/lib/structure_helpers_test.go @@ -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]) + } + } + +}