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:
parent
9d90b70b4e
commit
60052c6070
|
@ -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
|
||||||
|
|
|
@ -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_.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue