From d64ceba2cc8d8d1624348a76c7e7a02e385e6d0a Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Thu, 24 Mar 2022 09:26:06 +0100 Subject: [PATCH] save: add -a option to save all attachments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow saving all message parts that have the content disposition "attachment" header to a folder. Suggested-by: Ondřej Synáček Signed-off-by: Robin Jarry Acked-by: Koni Marti Tested-by: Moritz Poldrack --- commands/msgview/save.go | 64 ++++++++++++++++++++++++++++++---------- doc/aerc.1.scd | 8 +++-- widgets/msgviewer.go | 17 +++++++++++ 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/commands/msgview/save.go b/commands/msgview/save.go index 48add98..350739a 100644 --- a/commands/msgview/save.go +++ b/commands/msgview/save.go @@ -13,6 +13,7 @@ import ( "github.com/mitchellh/go-homedir" "git.sr.ht/~rjarry/aerc/commands" + "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" @@ -33,31 +34,36 @@ func (Save) Complete(aerc *widgets.Aerc, args []string) []string { return commands.CompletePath(path) } +type saveParams struct { + force bool + createDirs bool + trailingSlash bool + attachments bool +} + func (Save) Execute(aerc *widgets.Aerc, args []string) error { - opts, optind, err := getopt.Getopts(args, "fp") + opts, optind, err := getopt.Getopts(args, "fpa") if err != nil { return err } - var ( - force bool - createDirs bool - trailingSlash bool - ) + var params saveParams for _, opt := range opts { switch opt.Option { case 'f': - force = true + params.force = true case 'p': - createDirs = true + params.createDirs = true + case 'a': + params.attachments = true } } defaultPath := aerc.Config().General.DefaultSavePath // we either need a path or a defaultPath if defaultPath == "" && len(args) == optind { - return errors.New("Usage: :save [-fp] ") + return errors.New("Usage: :save [-fpa] ") } // as a convenience we join with spaces, so that the user doesn't need to @@ -68,10 +74,10 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error { // it gets stripped by Clean. // we auto generate a name if a directory was given if len(path) > 0 { - trailingSlash = path[len(path)-1] == '/' + params.trailingSlash = path[len(path)-1] == '/' } else if len(defaultPath) > 0 && len(path) == 0 { // empty path, so we might have a default that ends in a trailingSlash - trailingSlash = defaultPath[len(defaultPath)-1] == '/' + params.trailingSlash = defaultPath[len(defaultPath)-1] == '/' } // Absolute paths are taken as is so that the user can override the default @@ -89,27 +95,53 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error { if !ok { return fmt.Errorf("SelectedTab is not a MessageViewer") } - pi := mv.SelectedMessagePart() - if trailingSlash || isDirExists(path) { + store := mv.Store() + + if params.attachments { + parts := mv.AttachmentParts() + if len(parts) == 0 { + return fmt.Errorf("This message has no attachments") + } + params.trailingSlash = true + for _, pi := range parts { + if err := savePart(pi, path, store, aerc, ¶ms); err != nil { + return err + } + } + return nil + } + + pi := mv.SelectedMessagePart() + return savePart(pi, path, store, aerc, ¶ms) +} + +func savePart( + pi *widgets.PartInfo, + path string, + store *lib.MessageStore, + aerc *widgets.Aerc, + params *saveParams, +) error { + + if params.trailingSlash || isDirExists(path) { filename := generateFilename(pi.Part) path = filepath.Join(path, filename) } dir := filepath.Dir(path) - if createDirs && dir != "" { + if params.createDirs && dir != "" { err := os.MkdirAll(dir, 0755) if err != nil { return err } } - if pathExists(path) && !force { + if pathExists(path) && !params.force { return fmt.Errorf("%q already exists and -f not given", path) } ch := make(chan error, 1) - store := mv.Store() store.FetchBodyPart(pi.Msg.Uid, pi.Index, func(reader io.Reader) { f, err := os.Create(path) if err != nil { diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 27384ad..dda8aed 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -337,12 +337,12 @@ message list, the message in the message viewer, etc). Saves the current message part in a temporary file and opens it with the system handler. Any given args are forwarded to the open handler -*save* [-fp] +*save* [-fpa] Saves the current message part to the given path. If the path is not an absolute path, general.default-save-path will be prepended to the path given. - If path ends in a trailing slash or if a folder exists on disc, - aerc assumes it to be a directory. + If path ends in a trailing slash or if a folder exists on disc or if -a + is specified, aerc assumes it to be a directory. When passed a directory :save infers the filename from the mail part if possible, or if that fails, uses "aerc_$DATE". @@ -350,6 +350,8 @@ message list, the message in the message viewer, etc). *-p*: Create any directories in the path that do not exist + *-a*: Save all attachments. Individual filenames cannot be specified. + *mark* [-atv] Marks messages. Commands will execute on all marked messages instead of the highlighted one if applicable. The flags below can be combined as needed. diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 91baf02..cefa9bb 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -303,6 +303,23 @@ func (mv *MessageViewer) SelectedMessagePart() *PartInfo { } } +func (mv *MessageViewer) AttachmentParts() []*PartInfo { + var attachments []*PartInfo + + for _, p := range mv.switcher.parts { + if p.part.Disposition == "attachment" { + pi := &PartInfo{ + Index: p.index, + Msg: p.msg.MessageInfo(), + Part: p.part, + } + attachments = append(attachments, pi) + } + } + + return attachments +} + func (mv *MessageViewer) PreviousPart() { switcher := mv.switcher for {