Emacs with Mu4e and Multiple Gmail Accounts
Preamble
Documented steps I take to setup Mu4e with two Gmail accounts. This page will contain the directions for using app password authentication. It will be updated when XOAUTH2.0 is required.
Installation
Gmail
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.
Two email accounts will be added to Mu4e. Each account will have its own "context" and account dependent keybindings.
Creating a Gmail App Password
- Open the Gmail account settings
- Enable two factor authentication
- Create your app password using https://myaccount.google.com/apppasswords
The app password will be used by offlineimap to sign into your Gmail account. Create app passwords for both your email accounts.
Compile Mu4e
The application will be compiled from source
Dependencies
sudo apt install -y git meson libgmime-3.0-dev libxapian-dev gnutls-bin texinfo libcld2-dev cmake guile-3.0
Compiling 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
Create Email Folder
Create a folder to store your emails
mkdir -p ~/Mail/[email protected]
Create password decrypting script
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
Create main configuration file
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, unix
maxsyncaccounts = 3
socktimeout = 60
pythonfile = ~/.offlineimap.py
ui = TTY.TTYUI
[Account main]
localrepository = main-local
remoterepository = gmail-remote
[Repository main-local]
type = Maildir
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']
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
ssl_version = tls1_2
[Account unix]
localrepository = unix-local
remoterepository = unix-remote
[Repository unix-local]
type = Maildir
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 unix-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']
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
ssl_version = tls1_2
OEM
Downloading and Indexing Emails
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] \
--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
;; Load mu4e (Maildir email client for Emacs) if its directory exists
(when (file-directory-p "/usr/local/share/emacs/site-lisp/mu4e")
;; Load mu4e and its contribution library
(require 'mu4e)
(require 'mu4e-contrib)
;; --- Basic mu4e Configuration ---
(setq mu4e-maildir "~/Mail") ; Root Maildir directory (where your emails are stored)
(setq mu4e-attachment-dir "~/Downloads") ; Default directory for saving email attachments
(setq message-kill-buffer-on-exit t) ; Close the buffer after sending/canceling an email
(setq mu4e-compose-keep-self-cc nil) ; Don't CC yourself when replying to your own emails
(add-hook 'mu4e-view-mode-hook #'visual-line-mode) ; Enable word wrapping in email view
(add-hook 'mu4e-compose-mode-hook #'(lambda () (auto-save-mode 1))) ; Auto-save drafts
;; --- SMTP (Outgoing Mail) Settings ---
(setq send-mail-function 'smtpmail-send-it) ; Use `smtpmail` for sending emails
(setq smtpmail-stream-type 'starttls) ; Use STARTTLS for secure SMTP connections
;; --- Email Display & Rendering ---
(setq mu4e-html2text-command "w3m -T text/html") ; Convert HTML emails to plain text using `w3m`
(setq mu4e-headers-auto-update t) ; Automatically refresh email headers
(setq mu4e-view-show-images t) ; Display inline images in emails
(setq mu4e-use-fancy-chars t) ; Use pretty symbols (e.g., arrows for threads)
(setq mu4e-headers-show-threads t) ; Enable email threading in the headers view
(setq mu4e-view-fields '(:from :to :subject :date :maildir :tags)) ; Fields to display in email view
(setq mu4e-view-hide-cited t) ; Hide quoted text in email view by default
;; --- Composing Emails ---
(setq mu4e-compose-signature-auto-include nil) ; Don't auto-insert signature
(setq mu4e-compose-in-new-frame t) ; Open compose window in a new frame
(setq mu4e-compose-dont-reply-to-self t) ; Exclude self when replying to a mailing list
(setq message-citation-line-function 'message-insert-formatted-citation-line) ; Format email citations
;; --- Search & Behavior ---
(setq mu4e-search-full t) ; Show all search results (no limit)
(setq mu4e-sent-messages-behavior 'delete) ; Don't save sent emails (IMAP server handles this)
(setq mu4e-change-filenames-when-moving t) ; Required for `mbsync` compatibility
(setq mu4e-confirm-quit nil) ; Don't ask for confirmation when quitting mu4e
(setq mu4e-index-cleanup t) ; Remove stale entries
(setq mu4e-index-lazy-check t) ; Only re-index if changed
(setq mu4e-index-updated-hook nil) ; Disable "lazy indexing completed" popup
(advice-add 'mu4e--index-cleanup-finished-message :override #'ignore)
;; --- Integration with Other Tools ---
(setq mail-user-agent 'mu4e-user-agent) ; Set mu4e as the default email client in Emacs
(setq mu4e-completing-read-function 'ivy-completing-read) ; Use Ivy for autocompletion
;; --- UI & Hooks ---
(add-hook 'minibuffer-setup-hook (lambda () (setq mu4e-hide-index-messages t))) ; Hide mu4e logs in minibuffer
(add-hook 'minibuffer-exit-hook (lambda () (setq mu4e-hide-index-messages nil)))) ; Restore logs after exiting minibuffer
;; Email Syncing
(setq mu4e-get-mail-command "true") ;; Don't set a mail fetch program. Rely on independant sync
(setq mu4e-update-interval 60) ; Check for new emails every 60 seconds
(defvar my/offlineimap-process nil
"Tracks the OfflineIMAP process to prevent multiple instances.")
(defun my/async-offlineimap-sync ()
"Run OfflineIMAP asynchronously without blocking Emacs, but avoid multiple instances."
(interactive)
(unless my/offlineimap-process
(let ((timestamp (current-time-string))
(buf (get-buffer-create "*offlineimap*")))
(with-current-buffer buf
(read-only-mode 0)
(erase-buffer)
(insert (format "Starting OfflineIMAP sync at: %s\n" timestamp))) ;; Insert timestamp into the buffer
(setq my/offlineimap-process
(start-process "offlineimap"
buf
"offlineimap" "-o")))))
(defun my/stop-offlineimap-sync ()
"Stop the OfflineIMAP process if it's running."
(when my/offlineimap-process
(delete-process my/offlineimap-process)
(setq my/offlineimap-process nil)))
;; Run OfflineIMAP every 45 seconds, but only if it's not already running
(run-with-timer 0 45 #'my/async-offlineimap-sync)
(add-hook 'mu4e-update-mail-hook #'mu4e-update-index)
;; Email Contexts
; This code handles which email accounts are visible in MU4E as well as configures
; context-aware keybindings for bookmarks.
(setq mu4e-contexts
(list
;; Office account
(make-mu4e-context
:name "Office"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/[email protected]" (mu4e-message-field msg :maildir))))
:vars '((user-mail-address . "[email protected]")
(user-full-name . "First Last")
(mu4e-drafts-folder . "/[email protected]/drafts")
(mu4e-sent-folder . "/[email protected]/sent")
(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-sent-messages-behavior . 'delete)
(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]/drafts" . ?d)))))
;; UnixFandom account
(make-mu4e-context
:name "Queztaz"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/[email protected]" (mu4e-message-field msg :maildir))))
:vars '((user-mail-address . "[email protected]")
(user-full-name . "First Last")
(mu4e-drafts-folder . "/[email protected]/drafts")
(mu4e-sent-folder . "/[email protected]/sent")
(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-sent-messages-behavior . 'delete)
(mu4e-compose-reply-ignore-address . '("no-?reply" "[email protected]"))
(mu4e-maildir-shortcuts . (("/[email protected]/INBOX" . ?u)
("/[email protected]/sent" . ?S)
("/[email protected]/[Gmail].Trash" . ?T)
("/[email protected]/[Gmail].Spam" . ?J)
("/[email protected]/drafts" . ?D)))))))
(defvar my/mu4e-bookmarks-by-context
'(("Office"
. ((:name "Inbox" :query "maildir:/[email protected]/INBOX" :key ?i)
(:name "Unread" :query "flag:unread and maildir:/[email protected]/INBOX" :key ?u)
(:name "All Mail" :query "maildir:/[email protected]/" :key ?a)
(:name "Sent" :query "maildir:/[email protected]/sent" :key ?s)
(:name "Drafts" :query "maildir:/[email protected]/drafts" :key ?d)
(:name "Spam" :query "maildir:/[email protected]/[Gmail].Spam" :key ?j)
(:name "Trash" :query "maildir:/[email protected]/[Gmail].Trash" :key ?t)
(:name "Last 7 Days" :query "date:7d..now and maildir:/[email protected]/INBOX" :key ?7)
(:name "PDFs" :query "mime:application/pdf and maildir:/[email protected]/INBOX" :key ?p)
(:name "Images" :query "mime:image/* and maildir:/[email protected]/INBOX" :key ?i)
(:name "With Calendar Event" :query "mime:text/calendar and maildir:/[email protected]/INBOX" :key ?c)
(:name "With Word Docs" :query "mime:application/msword OR mime:application/vnd.openxmlformats-officedocument.wordprocessingml.document and maildir:/[email protected]/INBOX" :key ?w)
))
("Queztaz"
. ((:name "Inbox" :query "maildir:/[email protected]/INBOX" :key ?i)
(:name "Unread" :query "flag:unread and maildir:/[email protected]/INBOX" :key ?u)
(:name "All Mail" :query "maildir:/[email protected]/" :key ?a)
(:name "Sent" :query "maildir:/[email protected]/sent" :key ?s)
(:name "Drafts" :query "maildir:/[email protected]/drafts" :key ?d)
(:name "Spam" :query "maildir:/[email protected]/[Gmail].Spam" :key ?j)
(:name "Trash" :query "maildir:/[email protected]/[Gmail].Trash" :key ?t)
(:name "Last 7 Days" :query "date:7d..now and maildir:/[email protected]/INBOX" :key ?7)
(:name "PDFs" :query "mime:application/pdf and maildir:/[email protected]/INBOX" :key ?p)
(:name "Images" :query "mime:image/* and maildir:/[email protected]/INBOX" :key ?i)
(:name "With Calendar Event" :query "mime:text/calendar and maildir:/[email protected]/INBOX" :key ?c)
(:name "With Word Docs" :query "mime:application/msword OR mime:application/vnd.openxmlformats-officedocument.wordprocessingml.document and maildir:/[email protected]/INBOX" :key ?w)
))))
(defun my/update-mu4e-bookmarks-based-on-context (context-name)
"Update `mu4e-bookmarks` when switching context."
(let ((bookmarks (cdr (assoc context-name my/mu4e-bookmarks-by-context))))
(when bookmarks
(setq mu4e-bookmarks bookmarks))))
(add-hook 'mu4e-context-changed-hook
(lambda ()
(my/update-mu4e-bookmarks-based-on-context
(mu4e-context-name (mu4e-context-current)))))
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
machine smtp.gmail.com login [email protected] password PASSWORDHERE port 587
Using Mu4e
Evaluate the code blocks above and launch Mu4e
M-x mu4e