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:
parent
d66930749a
commit
d64ceba2cc
3 changed files with 70 additions and 19 deletions
|
@ -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, ¶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)
|
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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue