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"
"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] <path>")
return errors.New("Usage: :save [-fpa] <path>")
}
// 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, &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)
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 {

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
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.
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.

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() {
switcher := mv.switcher
for {