M. Rincón Guided by asteriks

Adding mu4e Actions

2023-02-22

I like text emails. To the point that I would argue that html doesn't add significant value to most messages. But sometimes it does. So I open those messages on my browser. Unfortunately, snaps cannot access the /temp/ directory, and by default, that's the location Emacs and mu4e use to save a message before calling the browser. Fortunately, the default behavior can be easily changed. Better yet, it's easy to leverage that change to add new actions to mu4e, like quickly converting an email into a pdf.

The function mu4e-action-view-in-browser does two things. First, it saves the html part of an email into a temporary folder. Then, it calls the function associated with browse-url-browser-function to opens the file. In Emacs the location of the temporary directory is set by the temporary-file-directory variable. To open a file using a snap, I had to set the variable to something in my home directory. I thought about adding a new directory to the .cache, but I soon discovered that Firefox doesn't have access to it either1. So I settled for the rather inelegant solution of creating a temporary directory inside Downloads.

(defvar extra-temp-email-dir "~/Downloads/tmp-mu/"
  "Location of temporary files for Emacs mu4e.")

(defun extra-open-email-in-browser (msg &optional skip-headers)
  "Show current MSG in browser if it includes an HTML-part.
This is a wrapper function for `mu4e-action-view-in-browser`
needed to get around snaps inability to access the temp
directory. If SKIP-HEADERS is set, do not show include message
headers. The variables `browse-url-browser-function',
`browse-url-handlers', and `browse-url-default-handlers'
determine which browser function to use."
  (let* ((temporary-file-directory extra-temp-email-dir))
    (unless (file-directory-p temporary-file-directory)
      (make-directory temporary-file-directory t))
    (mu4e-action-view-in-browser msg skip-headers)))

(setq mu4e-view-actions '(("capture message"  . mu4e-action-capture-message)
                          ("view in browser"  . extra-open-email-in-browser)
                          ("show this thread" . mu4e-action-show-thread)))

Having gone this far, I decided to go a bit further and create a function, which instead of opening the file, makes a pdf.

(defun extra-email-to-pdf (msg &optional args)
  "Pdf temp file MSG to a new name with ARGS ignored."
  (let* ((async-shell-command-display-buffer nil)
         (temp (format-time-string "~/Downloads/%Y-%m-%dT%H:%M.pdf"))
         (name (read-string "File name: " temp))
         (html (replace-regexp-in-string (regexp-quote "file://") "" msg t t)))
    (if args (message "Additional optional argument was ignored when saving to PDF."))
    (async-shell-command (concat "pandoc " html " -o " name))))

(defun extra-print-email-to-pdf (msg &optional skip-headers)
  "Save current MSG as a pdf if it includes an HTML-part.
If SKIP-HEADERS is set, do not show include message headers."
  (let* ((browse-url-browser-function  'extra-email-to-pdf))
    (mu4e-action-view-in-browser msg skip-headers)))

And another function to save the html part of the email.

(defun extra-move-temp-email-location (msg &optional args)
  "Move and rename temp file MSG to a new location with ARGS ignored."
  (let* ((temp (format-time-string (concat extra-temp-email-dir
                                           "%Y-%m-%dT%H:%M.html")))
         (name (read-string "File name: " temp))
         (file (replace-regexp-in-string (regexp-quote "file://") "" msg t t)))
    (if args (message "Additional optional argument was ignored when saving to HTML."))
    (rename-file file name)))

(defun extra-save-email-html (msg &optional skip-headers)
  "Save current MSG HTML-part.
If SKIP-HEADERS is set, do not show include message headers."
  (let* ((extra-temp-email-dir "~/Downloads/")
         (browse-url-browser-function  'extra-move-temp-email-location))
    (mu4e-action-view-in-browser msg skip-headers)))

(setq mu4e-view-actions '(("capture message"  . mu4e-action-capture-message)
                          ("view in browser"  . extra-open-email-in-browser)
                          ("download as html"  . extra-save-email-html)
                          ("print to PDF"  . extra-print-email-to-pdf)
                          ("show this thread" . mu4e-action-show-thread)))

And another function to clean the temporary files.

(defun extra-clean-temp-email-directory ()
  "Remove all files from DIR if possible."
  (when (file-directory-p extra-temp-email-dir)
    (let ((files (directory-files extra-temp-email-dir
                                  t
                                  directory-files-no-dot-files-regexp)))
      (dolist (file files)
        (condition-case nil
            (delete-file file)
          (error nil)))
      (condition-case nil
          (delete-directory extra-temp-email-dir)
        (error nil)))))

(add-hook 'kill-emacs-hook 'extra-clean-temp-email-directory)

So now, after opening an email, I can call mu4e-view-action and print the email or save it. And if all goes well, after closing Emacs, I won't have a temporary folder in my Downloads directory.

Finally, to encourage the use to text over html on emails, I added two more lines to my configuration.

(setq mm-discouraged-alternatives (list "image/.*" "text/html" "text/richtext")
      gnus-inhibit-images t

1

I hope the benefits of Snaps offset the frustrating access rights, auto updates, and other myriad of tiny annoyances —in addition to the fact that apt install sneakily contacts the snap store but makes you use snap refresh, because you know, apt, apt-get, cargo, and pip are just not enough.