Setting up Emacs with Mu4e, Protonmail, and Gmail

7 min read January 11, 2025 1441 words

Overview

Here's how to configure MU4e to work with Protonmail and Gmail simultaneously

App password/XOauth

I'm using app passwords while they still last. Eventually this section will contain information on setting up XOAUTH 2.0 when app passwords are fully phased out.

Create your app password for your gmail account by doing the following:

The app password will be used by offlineimap to sign into your Gmail account.

Mu4e Installation/Configuration

Dependencies

First install the mu4e dependencies. We're building the application from source

sudo apt install -y git meson libgmime-3.0-dev libxapian-dev gnutls-bin texinfo libcld2-dev cmake guile-3.0

Compiling Mu4e

Run the following commands to compile MU4E.

mkdir -p ~/Sources
git clone https://github.com/emacsmirror/mu4e ~/Sources/mu4e
cd ~/Sources/mu4e
./autogen.sh
make
sudo make install

GPG Keys (Encrypting Password)

Generate your own GPG pair. This will be used to decrypt your password by offlineimap. Please use the full-key-gen flag instead of following the example below. I'm creating a passwordless GPG pair. Its absolutely not secure. This avoids having to type a passphrase to decrypt your password files

gpg --batch --passphrase '' --quick-gen-key [email protected] default default
gpg --list-keys
pass init <GPGID>

Offline Imap (Downloading Emails)

OfflineImap will be used to download the emails. Emacs will be used to send the emails using its SMTP functionality

Setup

Create a folder to store your emails

mkdir -p ~/Mail/[email protected]

Create the following .offlineimap.py script. This will be used in the .offlineimaprc to decrypt our password file

cat <<OEM > ~/.offlineimap.py
#! /usr/bin/env python
from subprocess import check_output

def get_pass(account):
    return check_output("pass email/" + account, bash=True).splitlines()[0]
OEM

Now create the main configuration used to download the emails. The only thing you need to replace is any reference to [email protected]

cat <<OEM > ~/.offlineimaprc
[general]
accounts = main
maxsyncaccounts = 3
socktimeout = 60
pythonfile = ~/.offlineimap.py

ui = TTY.TTYUI

[Account main]
# identifier for local repository i.e. /user/dericbytes/mail
localrepository = main-local
# identifier for remote repository i.e. imap.google.com
remoterepository = gmail-remote

[Repository main-local]
# Sync with a mail directory (Maildir) or another IMAP mail server (IMAP)
type = Maildir
# Where to store synced mail
localfolders = /home/user/Mail/[email protected]
nametrans = lambda folder: re.sub ('important', '^\[Gmail\].Important',
                           re.sub('all', '[Gmail].All Mail'       ,
                           re.sub('sent'    , '[Gmail].Sent Mail' ,
                           re.sub('drafts'  , '[Gmail].Drafts'    ,
                           folder))))
folderfilter = lambda folder: folder not in ['important']



[Repository gmail-remote]
type = Gmail
remoteuser = [email protected]
remotepasseval = get_pass("[email protected]")

nametrans = lambda folder: re.sub ('^\[Gmail\].Important', 'important',
                           re.sub('.*All Mail$'  , 'all',
                           re.sub('.*Sent Mail$' , 'sent',
                           re.sub('.*Drafts$'    , 'drafts',
                           folder))))
folderfilter = lambda folder: folder not in ['[Gmail]/All Mail',
                                             '[Gmail]/Important',
                                             '[Gmail]/Starred',
                                             ]

# Necessary as of OfflineIMAP 6.5.4
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
# Necessary to work around https://github.com/OfflineIMAP/offlineimap/issues/573 (versions 7.0.12, 7.2.1)
ssl_version = tls1_2
OEM

Run the following to commands. offlineimap is used to start the email download. Mu will index the downloaded emails.

offlineimap

mu init --maildir=~/Mail \
    --my-address=[email protected] \

Configuring Emacs

At this point you would have installed the emacs mu4e package from source, downloaded your emails to a folder, and lastly indexed it with Mu4e.

Now its time to configure Emacs to show you emails in that folder. I won't explain this configuration file. I'll leave this up to your responsibility.

This configuration file will work with your email. I stripped out the mu4e-folding and mu4e-thread packages from the config. They provide UI improvements to Mu4e and the installation of these plugins are out of the scope of this guide.

Basic Config

(require 'auth-source)
(setq auth-sources '("~/.authinfo.gpg"))
(setf epg-pinentry-mode 'loopback)
(setq auth-source-debug nil)
(defun pinentry-emacs (desc prompt ok error)
  (let ((str (read-passwd
              (concat (replace-regexp-in-string "%22" "\""
                                                (replace-regexp-in-string "%0A" "\n" desc)) prompt ": "))))
    str))

(when (file-directory-p "/usr/local/share/emacs/site-lisp/mu4e")
  (require 'mu4e)
  (require 'mu4e-contrib)
  (setq mu4e-maildir "~/Mail") ; Default folder containing email
  (setq mu4e-attachment-dir "~/Downloads") ; Default folder for downloaded items
  (setq message-kill-buffer-on-exit t) ; Don't keep message buffers around
  (setq mu4e-compose-keep-self-cc nil) ; Avoid keeping self in CC
  (setq send-mail-function 'smtpmail-send-it) ; Function to send mail (via SMTP)
  (setq smtpmail-stream-type 'starttls) ; Specify the type of SMTP connections to use
  (setq mu4e-html2text-command "w3m -T text/html") ; Command to convert HTML emails to plain text
  (setq mu4e-headers-auto-update  t) ; Automatically update headers
  (setq mu4e-view-show-images  t) ; Enable inline images in emails
  (setq mu4e-compose-signature-auto-include  nil); Disables automatic inclusion of signatures in new emails
  (setq mu4e-search-full t) ; Search for all results rather than up to 'mu4e-search-results-limit'
  (setq mu4e-use-fancy-chars  t) ; Use fancy characters in the interface
  (setq mail-user-agent 'mu4e-user-agent) ; Set mu4e as the default email agent
  (setq mu4e-completing-read-function 'ivy-completing-read) ; Use Ivy for completing read prompts
  (setq mu4e-confirm-quit nil)
  (setq mu4e-headers-show-threads t) ; Show threads
  (setq mu4e-compose-in-new-frame t) ; Allows reading other emails while composing
  (setq mu4e-compose-dont-reply-to-self t) ; Dont include self when replying
  (setq message-citation-line-function 'message-insert-formatted-citation-line) ;; Citation
  (setq mu4e-sent-messages-behavior 'delete) ; Don't save message to Sent Messages, IMAP takes care of this
  (setq mu4e-get-mail-command "offlineimap")
  (setq mu4e-change-filenames-when-moving t) ; needed for mbsync
  (setq mu4e-update-interval 60) ; Update 30 seconds
  (add-hook 'mu4e-view-mode-hook #'visual-line-mode) ; Enable visual-line-mode in the email view mode
  (add-hook 'mu4e-compose-mode-hook #'(lambda () (auto-save-mode 1)))
  (add-hook 'message-send-hook ; Confirmation before sending
	    (lambda ()
	      (unless (yes-or-no-p "Sure you want to send this?")
		(signal 'quit nil))))
  (add-hook 'mu4e-compose-mode-hook ; Spell Check
	    (defun my-do-compose-stuff ()
	      "My settings for message composition."
	      (set-fill-column 80)
	      (jinx-mode 1)))
  (add-hook 'minibuffer-setup-hook (lambda () (setq mu4e-hide-index-messages t))) ; Hide Mu4E update log from minibuffer
  (add-hook 'minibuffer-exit-hook (lambda () (setq mu4e-hide-index-messages nil)))
  (setq ; Custom header glyphs
   mu4e-headers-draft-mark     '("D"  . "✎")
   mu4e-headers-flagged-mark   '("F"  . "⚑")
   mu4e-headers-new-mark       '("N"  . "🔥")
   mu4e-headers-passed-mark    '(">"  . "➜")
   mu4e-headers-replied-mark   '("<"  . "↶")
   mu4e-headers-seen-mark      '("✓"  . "✔")
   mu4e-headers-trashed-mark   '("X"  . "☠")
   mu4e-headers-attach-mark    '("A"  . "⎘")
   mu4e-headers-encrypted-mark '("E"  . "🔒")
   mu4e-headers-signed-mark    '("S"  . "✍")
   mu4e-headers-unread-mark    '("U"  . "⬢")
   mu4e-headers-calendar-mark  '("C"  . "⏳"))
  )
(setq mu4e-headers-thread-child-prefix '(" L  " . " │  ") ; Custom thread icons
      mu4e-headers-thread-connection-prefix '(" |  " . " │  ")
      mu4e-headers-thread-duplicate-prefix '(" =  " . " ≡  ")
      mu4e-headers-thread-first-child-prefix '(" L  " . " ⚬  ")
      mu4e-headers-thread-last-child-prefix '(" └─ " . " └─ "))

(defun compose-reply-wide-or-not-please-ask ()
  "Ask whether to reply-to-all or not."
  (interactive)
  (mu4e-compose-reply (yes-or-no-p "Reply to all?")))
(define-key mu4e-compose-minor-mode-map (kbd "R") #'compose-reply-wide-or-not-please-ask)
(define-key mu4e-headers-mode-map (kbd "R") 'compose-reply-wide-or-not-please-ask)
(define-key mu4e-view-mode-map (kbd "R") 'compose-reply-wide-or-not-please-ask)


(defun my-add-header ()
  "Add CC and BCC headers automatically"
  (save-excursion (message-add-header
                   (concat "CC: " "\n")
                   ;; pre hook above changes user-mail-address.
                   (concat "Bcc: " "\n"))))
(add-hook 'mu4e-compose-mode-hook 'my-add-header)

(setq mu4e-view-fields '(:from :to :subject :date :maildir :tags))
(setq mu4e-view-hide-cited t)

(defun mu4e-set-all-as-read ()
  "Make all emails read."
  (interactive)
  (require 'mu4e-contrib)
  (with-temp-buffer
    (mu4e-headers-search-bookmark "flag:unread AND NOT flag:trashed")
    (sleep-for 0.15)
    (mu4e-headers-mark-all-unread-read)
    (mu4e-mark-execute-all 'no-confirmation)))

(setq user-mail-address "[email protected]"
      user-full-name "First Last"
      mu4e-drafts-folder "/[email protected]/[Gmail]/Drafts"
      mu4e-sent-folder "/[email protected]/[Gmail]/Sent Mail"
      mu4e-refile-folder "/[email protected]/[Gmail]/All Mail"
      mu4e-trash-folder "/[email protected]/[Gmail]/Trash"
      smtpmail-smtp-server "smtp.gmail.com"
      smtpmail-smtp-service 587
      mu4e-compose-reply-ignore-address '("no-?reply" "[email protected]")
      mu4e-maildir-shortcuts '(("/[email protected]/INBOX" . ?i)
                               ("/[email protected]/sent" . ?s)
                               ("/[email protected]/[Gmail].Trash" . ?t)
                               ("/[email protected]/[Gmail].Spam" . ?j)
                               ("/[email protected][Gmail]/Drafts" . ?d)))
(setq mu4e-bookmarks ; Bookmarks for quick email search
      '((:name  "Unread messages"
                :query "flag:unread and maildir:/[email protected]/INBOX"
                :key   ?u)
        (:name  "Today's messages"
                :query "date:today..now"
                :key ?t)
        (:name  "Last 7 days"
                :query "date:7d..now"
                :key ?7)
        (:name  "Messages with PDF"
                :query "mime:application/pdf"
                :key ?p)
        (:name  "Messages with images"
                :query "mime:image/*"
                :key ?I)
        (:name  "Messages with calendar event"
                :query "mime:text/calendar"
                :key ?e)
        (:name  "Messages with Word docs"
                :query "mime:application/msword OR mime:application/vnd.openxmlformats-officedocument.wordprocessingml.document"
                :key ?w)
	(:name  "Inbox"
                :query "maildir:/[email protected]/INBOX"
                :key   ?i)
        (:name  "Sent"
                :query "maildir:/[email protected]/sent"
                :key   ?s)
        (:name  "Trash"
                :query "maildir:/[email protected]/[Gmail].Trash"
                :key   ?T)
        (:name  "Spam"
                :query "maildir:/[email protected]/[Gmail].Spam"
                :key   ?J)
	(:name  "Year 2025"
                :query "date:20250101..20251231"
                :key ?5)
        (:name  "Year 2024"
                :query "date:20240101..20241231"
                :key ?4)
        (:name  "Year 2023"
                :query "date:20230101..20231231"
                :key ?3)
        (:name  "Year 2022"
                :query "date:20220101..20221231"
                :key ?2)
        (:name  "Year 2021"
                :query "date:20210101..20211231"
                :key ?1)
        (:name  "Year 2020"
                :query "date:20200101..20201231"
                :key ?0)
	))


(defun message-attachment-present-p () ; Warn if no attachments are present, but if the text talks about attachments:
  "Return t if an attachment is found in the current message."
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (when (search-forward "<#part" nil t) t))))
(defcustom message-attachment-intent-re
  (regexp-opt '("attach"
                "attached"
                "joint"
                "joins"
                "PDF"
                "attachment"))
  "A regex which - if found in the message, and if there is no
attachment - should launch the no-attachment warning.")
(defcustom message-attachment-reminder
  "Are you sure you want to send this message without any attachment? "
  "The default question asked when trying to send a message
containing `message-attachment-intent-re' without an
actual attachment.")
(defun message-warn-if-no-attachments ()
  "Ask the user if s?he wants to send the message even though
there are no attachments."
  (when (and (save-excursion
               (save-restriction
                 (widen)
                 (goto-char (point-min))
                 (re-search-forward message-attachment-intent-re nil t)))
             (not (message-attachment-present-p)))
    (unless (y-or-n-p message-attachment-reminder)
      (keyboard-quit))))
;; add hook to message-send-hook (so also works with gnus)
(add-hook 'message-send-hook #'message-warn-if-no-attachments)

Creating .authinfo.gpg file

This file will contain your SMTP password used by emacs to send emails.

machine smtp.gmail.com login [email protected] password PASSWORDHERE port 587

Using Mu4e

Evaluate the code blocks above and launch Mu4e

M-x mu4e