Email with aerc
Table of contents
Introduction
aerc is an email client for the terminal written in Go. Originally created by Drew DeVault, it is currently maintained by Robin Jarry and a group of developers: the maintained fork can be found on sourcehut.
It is customizable, fast, features vi-style keybindings and ex-style commands (while still having support for mouse), support for various backends, and most importantly it is free and open-source software.
Installation
Installing aerc is straigthforward: there are packages available for Alpine, Arch, Debian, Fedora and macOS (through Homebrew). As I use Fedora, I installed the corresponding RPM package. However, at some point I encountered a bug whose fix wasn't yet in the latest package version so I decided to install it from source.
The repo's instructions are quite clear (see the From
Source section): install a recent
version of Go (at least 1.18 at the time of writing) and
scdoc1. To install scdoc, clone the
repo (https://git.sr.ht/~sircmpwn/scdoc), run make
to compile it, and sudo make install
to install it. After installing scdoc, clone aerc's repo
(https://git.sr.ht/~rjarry/aerc), run make
to compile it, and sudo make install
to install it.
This is another of Drew's projects used to generate man pages.
Setup
On first launch aerc starts its interactive account setup. If the email account
supports IMAP/SMTP (and it doesn't require something like OAuth) you can use the
guided setup, which can be invoked later with the new-account
command. If your
accounts can be set up with this wizard then congrats, you have a working
terminal email client! If not, or if you are interested in saving your emails
offline, keep reading.
First, offline access: by default, aerc requires an internet connection as it fetches your emails each time you open it. However, we can leverage its support for other backends such as a Maildir folder and notmuch to download the emails once and point aerc to use those copies. The only caveat is that neither of those options can be directly configured with the wizard2.
I'm currently updating my guides, so while the publishing date will be 2024, the first version was written quite a while ago. Version 0.17.0 introduced the option to configure maildir and other backends.
Second, email providers such as Gmail may require some additional steps to work with aerc, as they use OAuth instead of a plain username+password combination.
Offline email with isync
isync is "a command line application which synchronizes mailboxes". It connects to remote servers with IMAP and stores the mails in local Maildir folders. aerc cannot synchronize Maildir folders itself, so using a tool like isync is necessary.
The executable is called mbsync
after "massive changes in the user interface",
which explains why its configuration lives in $HOME/.mbsyncrc
. I've based mine
on Drew's, shown in this blog
post. Here's
a simplified example to illustrate the file's format.
IMAPAccount migadu
Host imap.migadu.com
Port 993
User julio@julioloayzam.com
Passcmd "passage show email/migadu"
SSLType IMAPS
MaildirStore migadu-local
Path ~/.mail/migadu/
INBOX ~/.mail/migadu/INBOX
SubFolders Verbatim
IMAPStore migadu
Account migadu
Channel migadu
Far :migadu:
Near :migadu-local:
Patterns *
Create Both
Delete Both
Let's go through it step by step.
IMAPAccount migadu
Host imap.migadu.com
Port 993
User julio@julioloayzam.com
Passcmd "passage show email/migadu"
SSLType IMAPS
We first declare the account with its connection information, including name,
server address, username, and password. The password can be in plaintext, or
isync can get by running the command specified with Passcmd
. I use
passage, but any password manager with
a CLI works.
MaildirStore migadu-local
Path ~/.mail/migadu/
INBOX ~/.mail/migadu/INBOX
SubFolders Verbatim
Then we declare a MaildirStore
which indicates where to store the emails. The
SubFolders
option configures the "on-disk folder naming style", with
Verbatim
meaning that the hierarchy you use will be preserved (as long as the
Flatten
option is not used).
IMAPStore migadu
Account migadu
We associate the migadu
account information with the remote IMAPStore migadu
.
Channel migadu
Far :migadu:
Near :migadu-local:
Patterns *
Create Both
Delete Both
Finally we link both together with a channel
, indicating we want to
synchronize all folders (*
), new folders (Create both
), and deleted folders
(Delete both
).
After configuring mbsync
, set aerc to use this folder, and optionally the
command to refresh the mailbox manually:
# aerc/accounts.conf
[Migadu]
source = maildir://~/.mail/migadu
check-mail-cmd = mbsync migadu
check-mail-timeout = 30s
I've bound <C-r>
(Ctrl + R) under [messages]
to :check-mail<enter>
.
And that's it! If your account works with IMAP(S), this should be enough to get your emails and read them offline with aerc. However, as mentioned above there are services like Gmail that make use of other authentication methods such as OAuth2, which requires us to do some additional tinkering.
Using Gmail with aerc
Since May 30 2022, the "less secure apps" setting was disabled, meaning means that apps can no longer login with just a username and a password . Two options are available:
- Use a 16-digit app password. You can follow those instructions to generate an app password and use it when adding an account with the wizard.
- Use OAuth2, which is what clients like Thunderbird implement and use by default with Gmail. This prompts you to login through your browser (with 2FA and all) and negotiates a token that is used by the client to connect once the login is successful. This guide focuses on this option.
Reading the aerc-imap(5) manpage shows that OAuth2 is supported by aerc by using
the imaps+oauthbearer
scheme in the accounts.conf
file, but since I want to
have offline access I need an alternative.
To use OAuth2 we need a Google Cloud Platform application, and its corresponding
client-id
and client-secret
.
The following steps are quite compressed, I'd like to expand on them later.
- Create an app in Google Cloud Platform.
- Enable the Gmail API.
- Add your email address(es) to the test users.
- Create OAuth credentials.
Now we can use these credentials to obtain access tokens for mbsync. There are several options to do so: there is Google's oauth2.py script, which requires Python 2, using the mutt_oauth2.py script, or the solution I'm currently using which is Email OAuth 2.0 Proxy, an IMAP/SMTP proxy that deals with OAuth on behalf of aerc. While it still requires some manual intervention to re-authorize the proxy from time to time, I'm pretty satisfied with it.
To install the proxy, clone the repo and install the requirements.
python -m pip install -r requirements.txt
I use virtual environments managed with Pipenv, you can use this Pipfile instead:
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
cryptography = "*"
pillow = "*"
timeago = "*"
pystray = ">=0.19.4"
pygobject = "*"
pywebview = {extras = ["qt"], version = "*"}
pyqtwebengine = "*"
The proxy is configured with the emailproxy.config
file, and the corresponding
documentation is in the file itself. It even includes examples for major
providers like Outlook and Gmail. Use the Gmail example and fill the client_id
and client_secret
with the values you obtained from the OAuth credentials.
Here's a simplified example of my configuration file:
[IMAP-2993]
local_address = localhost
server_address = imap.gmail.com
server_port = 993
[SMTP-2465]
local_address = localhost
server_address = smtp.gmail.com
server_port = 465
[<username>@gmail.com]
permission_url = https://accounts.google.com/o/oauth2/auth
token_url = https://oauth2.googleapis.com/token
oauth2_scope = https://mail.google.com/
redirect_uri = http://localhost:8080
client_id = <client_id>
client_secret = <client_secret>
Let's see what each section does:
- The
[IMAP-2993]
section indicates the IMAP server to use, as well as setting 2993 as the port to use for IMAP locally. - In the same vein,
[SMTP-2465]
indicates the SMTP server to use and sets 2465 as the local port to use. - The third section configures the OAuth2 credentials to use to connect to the
account. You mainly have to change
client_id
,client_secret
, andredirect_uri
, which is the address used by the proxy to finish the login process. If you have multiple accounts, set a different port for each one.
Then run the proxy with:
python emailproxy.py
It has some options that come in handy:
- The
--config-file
option can be used to indicate which configuration file to use. I use it to keep my configuration files in~/.config
. - The
--log-file
points to which file the logs are written, can be useful for debugging. - The
--cache-store
option separates the configuration from the token cache, which is useful to be able to manage the configuration file with e.g. Chezmoi without having to deal with the frequent changes that occur with the tokens.
The resulting command to first start the server is:
python emailproxy.py \
--config-file ~/.config/email-oauth2-proxy/emailproxy.conf \
--log-file ~/.config/email-oauth2-proxy/proxy.log \
--cache-store ~/.cache/email-oauth2-proxy/cache
If everything works, an icon should appear in your taskbar3. You can click to see the accounts being proxied, quit the proxy, and most importantly, you can enable the option to launch it on start-up. Click it and the proxy restarts as a background service. The next time the system boots it should start automatically.
GNOME users, see the "AppIndicator and KStatusNotification Support" extension.
There's also an option to auto-start using systemd services. I used that feature once, and as far as I remember it worked correctly. See this issue for more information.
Now we can add a Gmail account to .mbsyncrc
:
IMAPAccount Gmail
Host localhost
Port 2993
User <username>@gmail.com
Passcmd "passage show email/gmail"
SSLType None
Instead of setting Host
to Gmail's IMAP address, we use localhost
as mbsync
should go through the proxy. We also adjust the port accordingly.
Notice that we still use Passcmd
. However, the password used is not the
same as your account, it is a password used by the proxy to encrypt the tokens
it gets, so it can (and should) be different than your account's. This does not
mean that the connection to the proxy is encrypted, but since we're using
localhost
for the local_address
it shouldn't be a problem.4
There is an option to provide a certificate to encrypt the connection to the proxy if you'd like.
So we've managed to retrieve our email even when using a Gmail account, but what about sending emails?
Offline-tolerant sending with msmtp
The problem with our current setup is that aerc doesn't have a way to queue outgoing email, meaning if it can't send the message right away e.g. because you don't have internet access, your only option is to save the draft and try again later. Not the end of the world, but requires you to remember to send the draft.
To solve this Drew uses postfix, so if you're familiar with it, check out his post. As for me, I've decided to go with msmtp.
It's configuration lives in ~/.msmtprc
or $XDG_CONFIG_HOME/msmtp/config
. To
begin, you can try to generate the configuration file with the --configure
option if your provider has correctly configured autodiscovery:
msmtp --configure user@domain.tld
Here is a simplified version of my configuration file:
# Set default values for all following accounts.
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.mail/msmtp.log
# Migadu
account Migadu
host smtp.migadu.com
port 465
tls_starttls off
from julio@julioloayzam.com
user julio_msmtp@julioloayzam.com
passwordeval passage show mail/migadu
# Gmail
account Gmail
host localhost
port 2465
auth plain
tls off
tls_starttls off
from <username>@gmail.com
user <username>@gmail.com
passwordeval passage show mail/gmail
Some comments:
- Notice the use of
localhost
and port 2465 for the Gmail account. This is similar to the mbsync setup, as we also want msmtp to go through the proxy in order to correctly connect to the Gmail account. - Since the connection to the proxy is unencrypted, we turn off
tls
and setauth
to plain. - As you can see from my Migadu account, the
user
and thefrom
fields can differ.user
indicates the username for authentication, whilefrom
controls the envelope-from address. - In my case I use Migadu's mailbox identities, which allow me to create an app-specific password to access that account.
- There is syntax highlighting for
Vim in
/usr/share/vim/vimfiles/syntax/msmtp.vim
, though in my case I had to copy the file from/usr/share/doc/msmtp/scripts/vim/msmtp.vim
to my~/.vim/syntax
folder. - msmtp can get the password from a password manager using
passwordeval
, or if it's stored in GNOME Keyring, msmtp can find it automatically. I usepassage
for this one too.
Alright, since we already did the work to get OAuth working we now have a
working msmtp setup. But wait! On its own, it still lacks the queueing
capability that we were looking for... Thankfully, there are a set of
community-written scripts bundled with msmtp that aim to solve this problem.
They can be found under /usr/share/doc/msmtp/scripts
. There are two
alternatives, msmtpq
and msmtpqueue
: I use the former.
msmtpq
consists of two scripts, the eponymous msmtpq
, which is the actual
script, and a wrapper called msmtp-queue
, which is used to managed the queue.
To use them, copy them to a directory in your $PATH such as ~/.local/bin
.
msmtpq
can be configured with environment variables, you can read its
well-commented code to see the available variables5 such as MSMTPQ_Q
for setting the queue directory. To manage the queue, check out the available
options with msmtp-queue -h
.
Since the script in my .local/bin
is my own copy, I've opted to
modify the variables inside the script directly.
Now we update accounts.conf
:
[Migadu]
...
outgoing = ~/.local/bin/msmtpq -a Migadu
With that, we have a setup that copies mails for offline access as well as queue outgoing messages for when a connection becomes available. We're ready to face spotty airport connections and cellular hotspots. :)
Refreshing the mailboxes periodically
Alright we have offline copies of our emails and a setup with msmtp that waits until there's a connection available to send outgoing mail: what could be missing? Well, while you can read already downloaded emails, you can't fetch new messages if you don't have a connection. Wouldn't it be useful to periodically refresh your mailboxes, minimizing the amount on unsynchronised email?
We can leverage systemd services for this purpose. While cron and alternatives would do the job just fine, I used this to learn about creating systemd services.
First, we need a script to refresh our mailboxes:
#!/usr/bin/env bash
mbsync migadu
mbsync gmail
This simply calls mbsync
twice, or once for each account we want to sync.
Then, we need to create the systemd service:
systemctl edit --user --force --full mbsync.service
Let's briefly go over the options used.
edit
because we want to edit a service.--user
as this is a user service.--force
ensures that the unit is created if it didn't exist.--full
this loads the entire unit file instead of offering a temporary one that is used to replace the current file. This is mostly a preference, as it clearly shows what we are editing.
The file contents are:
[Unit]
Description=Refresh the mailboxes with mbsync.
[Service]
Type=oneshot
ExecStart=/home/julioloayzam/.local/bin/mbsync-all.sh
StandardOutput=file:/home/julioloayzam/.mail/mbsync.log
StandardError=file:/home/julioloayzam/.mail/mbsync.log
[Install]
WantedBy=default.target
This will execute the script shown above, which I have called mbsync-all.sh
and copied over to ~/.local/bin
. For ease of access, I have set the service to
log stdout and stderr to ~/.mail/mbsync.log
, in case anything goes wrong.
Now we only need to define a timer:
systemctl edit --user --force --full mbsync.timer
With the following content:
[Unit]
Description=Timer to run mbsync-all
RefuseManualStart=no
RefuseManualStop=no
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Unit=mbsync.service
[Install]
WantedBy=timers.target
This will trigger the service two minutes after boot (OnBootSec
) and then
every fifteen minutes (OnUnitActiveSec
). I have set the RefuseManualStart
and RefuseManualStop
to no
in order to manually stop and start the timer in
case I need to debug something.
To start the timer:
systemctl start --user --enable mbsync.timer
And now we sit back, and watch as systemd refreshes our mail every fifteen
minutes. That's surely all left to do right? Well.. if you have more than a
couple of accounts you may have noticed that this approach refreshes each one
sequentially, waiting for the previous to end syncing. While mbsync is already
capable of parallelising some operations6, it doesn't have a built-in
way of refreshing mailbox in parallel. Instead, we can use parallel
, a GNU
utility to execute commands in parallel. We can even integrate it easily into
our script with the --shebang
option:
See the PipelineDepth
option.
#!/usr/bin/parallel --shebang --jobs 2 -r mbsync --verbose
migadu
gmail
Now when systemd calls the mbsync-all.sh
script it will execute the mbsync --verbose <argument>
command, where <argument>
is each of our accounts, using
up to two jobs, effectively syncing both mailboxes at the same time. This works
as mbsync uses a lock per mailbox, so as long as we don't try to refresh the
same mailbox twice at the same time we should be able to parallelise execution
just fine.
Side note: the
-r
option is a shorthand for--no-run-if-empty
, which stops the command from running if stdin only contains whitespace. I'm actually not sure why I have this option enabled.
And now we're done.
Conclusion
That's about everything on my current email setup: offline access with mbsync
,
resilience to a bad or nonexistent connection with msmtp
and msmtpq
, regular
synchronization of emails with systemd services, and a speed up of the last step
with parallel
.
Some details may be lacking, and while that may improve in the future as this guide evolves, don't hesitate to reach out if you have doubts or suggestions on how to improve it. :)