save: add -a option to save all attachments

Allow saving all message parts that have the content disposition
"attachment" header to a folder.

Suggested-by: Ondřej Synáček <ondrej@synacek.org>
Signed-off-by: Robin Jarry <robin@jarry.cc>
Acked-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Moritz Poldrack <moritz@poldrack.dev>
This commit is contained in:
Robin Jarry 2022-03-24 09:26:06 +01:00
parent d66930749a
commit d64ceba2cc
3 changed files with 70 additions and 19 deletions

View file

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
@ -33,31 +34,36 @@ func (Save) Complete(aerc *widgets.Aerc, args []string) []string {
return commands.CompletePath(path) return commands.CompletePath(path)
} }
type saveParams struct {
force bool
createDirs bool
trailingSlash bool
attachments bool
}
func (Save) Execute(aerc *widgets.Aerc, args []string) error { 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 { if err != nil {
return err return err
} }
var ( var params saveParams
force bool
createDirs bool
trailingSlash bool
)
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
case 'f': case 'f':
force = true params.force = true
case 'p': case 'p':
createDirs = true params.createDirs = true
case 'a':
params.attachments = true
} }
} }
defaultPath := aerc.Config().General.DefaultSavePath defaultPath := aerc.Config().General.DefaultSavePath
// we either need a path or a defaultPath // we either need a path or a defaultPath
if defaultPath == "" && len(args) == optind { if defaultPath == "" && len(args) == optind {
return errors.New("Usage: :save [-fp] <path>") return errors.New("Usage: :save [-fpa] <path>")
} }
// as a convenience we join with spaces, so that the user doesn't need to // 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. // it gets stripped by Clean.
// we auto generate a name if a directory was given // we auto generate a name if a directory was given
if len(path) > 0 { if len(path) > 0 {
trailingSlash = path[len(path)-1] == '/' params.trailingSlash = path[len(path)-1] == '/'
} else if len(defaultPath) > 0 && len(path) == 0 { } else if len(defaultPath) > 0 && len(path) == 0 {
// empty path, so we might have a default that ends in a trailingSlash // 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 // 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 { if !ok {
return fmt.Errorf("SelectedTab is not a MessageViewer") 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, &params); err != nil {
return err
}
}
return nil
}
pi := mv.SelectedMessagePart()
return savePart(pi, path, store, aerc, &params)
}
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) filename := generateFilename(pi.Part)
path = filepath.Join(path, filename) path = filepath.Join(path, filename)
} }
dir := filepath.Dir(path) dir := filepath.Dir(path)
if createDirs && dir != "" { if params.createDirs && dir != "" {
err := os.MkdirAll(dir, 0755) err := os.MkdirAll(dir, 0755)
if err != nil { if err != nil {
return err return err
} }
} }
if pathExists(path) && !force { if pathExists(path) && !params.force {
return fmt.Errorf("%q already exists and -f not given", path) return fmt.Errorf("%q already exists and -f not given", path)
} }
ch := make(chan error, 1) ch := make(chan error, 1)
store := mv.Store()
store.FetchBodyPart(pi.Msg.Uid, pi.Index, func(reader io.Reader) { store.FetchBodyPart(pi.Msg.Uid, pi.Index, func(reader io.Reader) {
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {

View file

@ -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 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 with the system handler. Any given args are forwarded to the open handler
*save* [-fp] <path> *save* [-fpa] <path>
Saves the current message part to the given path. Saves the current message part to the given path.
If the path is not an absolute path, general.default-save-path will be If the path is not an absolute path, general.default-save-path will be
prepended to the path given. prepended to the path given.
If path ends in a trailing slash or if a folder exists on disc, If path ends in a trailing slash or if a folder exists on disc or if -a
aerc assumes it to be a directory. is specified, aerc assumes it to be a directory.
When passed a directory :save infers the filename from the mail part if When passed a directory :save infers the filename from the mail part if
possible, or if that fails, uses "aerc_$DATE". 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 *-p*: Create any directories in the path that do not exist
*-a*: Save all attachments. Individual filenames cannot be specified.
*mark* [-atv] *mark* [-atv]
Marks messages. Commands will execute on all marked messages instead of the Marks messages. Commands will execute on all marked messages instead of the
highlighted one if applicable. The flags below can be combined as needed. highlighted one if applicable. The flags below can be combined as needed.

View file

@ -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() { func (mv *MessageViewer) PreviousPart() {
switcher := mv.switcher switcher := mv.switcher
for { for {