Mu4E - Save attachments faster with ivy

EDIT - 2021-08-16: See the Edits section further down.

Issue I want to fix

The default way of saving my attachments is to hit a keybinding for mu4e-view-attachment-action.

I then get asked to select between save or save multi. After the selection, the chosen completion framework takes over and I can then save my attachments(s) in the directory dictated by the variable mu4e-attachment-dir. I cannot change the directory in which to save the attachments.

This grinds my gear to no end. Why you ask? Well, most of the time i can see the number of the attachments in my email. Sometime I want to save all of them and sometime I want to save just one or a range. The worst thing though is that I get prompted for save or save multi even if I only have one attachment 😠.

I want two different functions (two different keybindings) for single and for multi. Then to be able to complete (for me with ivy) to the destination directory I want.

My Solution to the issue

The crucial part upfront. The variable mu4e-save-multiple-attachments-without-asking needs to be set to true. From the documentation:

If non-nil, saving multiple attachments asks once for a directory and saves all attachments in the chosen directory.

Let us set it then.

(setq mu4e-save-multiple-attachments-without-asking t)

Setting the keybindings should be trivial.

Now to the functions.

case #1 - I want to save all attachments in the email

I took the default mu4e-view-save-attachment-multi and hacked on it till I got a custom function that preselects all (attachments). Plus, That way I can skip the prompt since I skipped the mu4e-view-attachment-action function. This is particularly sexy since I can use the function if I only have one attachment in the email as well.

(defun my/mu4e-view-save-attachments (&optional msg)
  "Save All Attachements in a selected directory using `ivy'.
This is a modified version of `mu4e-view-save-attachment-multi'."
  (interactive)
  (let* ((msg (or msg (mu4e-message-at-point)))
         (attachstr "a") ;; this is the part that preselects all
         (count (hash-table-count mu4e~view-attach-map))
         (attachnums (mu4e-split-ranges-to-numbers attachstr count)))
    (if mu4e-save-multiple-attachments-without-asking
        (let* ((path (concat (mu4e~get-attachment-dir) "/"))
               (attachdir (mu4e~view-request-attachments-dir path)))
          (dolist (num attachnums)
            (let* ((att (mu4e~view-get-attach msg num))
                   (fname  (plist-get att :name))
                   (index (plist-get att :index))
                   (retry t)
                   fpath)
              (while retry
                (setq fpath (expand-file-name (concat attachdir fname) path))
                (setq retry
                      (and (file-exists-p fpath)
                           (not (y-or-n-p
                                 (mu4e-format "Overwrite '%s'?" fpath))))))
              (mu4e~proc-extract
               'save (mu4e-message-field msg :docid)
               index mu4e-decryption-policy fpath))))
      (dolist (num attachnums)
        (mu4e-view-save-attachment-single msg num)))))

case #2 - I want to select a range or a single attachment in the email

This is rather the defaults function. Just copied it and renamed it in case I want to hack on it some more

(defun my/mu4e-view-save-attachment (&optional msg)
  "Save All Attachements in a selected directory using `ivy'.
This is a modified version of `mu4e-view-save-attachment-multi'."
  (interactive)
  (let* ((msg (or msg (mu4e-message-at-point)))
         (attachstr (mu4e~view-get-attach-num
                     "Attachment number range (or 'a' for 'all')" msg t))
         (count (hash-table-count mu4e~view-attach-map))
         (attachnums (mu4e-split-ranges-to-numbers attachstr count)))
    (if mu4e-save-multiple-attachments-without-asking
        (let* ((path (concat (mu4e~get-attachment-dir) "/"))
               (attachdir (mu4e~view-request-attachments-dir path)))
          (dolist (num attachnums)
            (let* ((att (mu4e~view-get-attach msg num))
                   (fname  (plist-get att :name))
                   (index (plist-get att :index))
                   (retry t)
                   fpath)
              (while retry
                (setq fpath (expand-file-name (concat attachdir fname) path))
                (setq retry
                      (and (file-exists-p fpath)
                           (not (y-or-n-p
                                 (mu4e-format "Overwrite '%s'?" fpath))))))
              (mu4e~proc-extract
               'save (mu4e-message-field msg :docid)
               index mu4e-decryption-policy fpath))))
      (dolist (num attachnums)
        (mu4e-view-save-attachment-single msg num)))))

Edits

2021-08-16 - Fixing breaking mu/mu4e 1.6 updates:

As of version 1.6 mu/mu4e implemented new functionalities – including the new gnus-article-mode – and deprecated others. See the release notes. In the snippets above I use mu4e-view-save-attachment-multi, which was carved out in the process.

I had to rewrite my functions to use mu4e-view-save-attachments. Below is the result.

BONUS: This works with any completion framework if you set mu4e-completing-read-function correctly.

for case #1:

(defun timu/mu4e-view-save-attachments ()
  "Save All Attachements in a selected directory using completion.
This is a modified version of `mu4e-view-save-attachments'."
  (interactive)
  (cl-assert (and (eq major-mode 'mu4e-view-mode)
                  (derived-mode-p 'gnus-article-mode)))
  (let* ((parts (mu4e~view-gather-mime-parts))
         (handles '())
         (files '())
         dir)
    (dolist (part parts)
      (let ((fname (cdr (assoc 'filename (assoc "attachment" (cdr part))))))
        (when fname
          (push `(,fname . ,(cdr part)) handles)
          (push fname files))))
    (if files
        (progn
          (setq dir (read-directory-name "Save to directory: "))
          (cl-loop for (f . h) in handles
                   when (member f files)
                   do (mm-save-part-to-file h (expand-file-name f dir))))
      (mu4e-message "No attached files found"))))

for case #2:

(defun timu/mu4e-view-save-attachment ()
  "Save one attachements in a selected directory using completion.
This is a modified version of `mu4e-view-save-attachments'."
  (interactive)
  (cl-assert (and (eq major-mode 'mu4e-view-mode)
                  (derived-mode-p 'gnus-article-mode)))
  (let* ((parts (mu4e~view-gather-mime-parts))
         (handles '())
         (files '())
         dir)
    (dolist (part parts)
      (let ((fname (cdr (assoc 'filename (assoc "attachment" (cdr part))))))
        (when fname
          (push `(,fname . ,(cdr part)) handles)
          (push fname files))))
    (if files
        (progn
          (setq files (completing-read-multiple "Save part(s): " files)
                dir (read-directory-name "Save to directory: "))
          (cl-loop for (f . h) in handles
                   when (member f files)
                   do (mm-save-part-to-file h (expand-file-name f dir))))
      (mu4e-message "No attached files found"))))