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.

1

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.

2

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.

  1. Create an app in Google Cloud Platform.
  2. Enable the Gmail API.
  3. Add your email address(es) to the test users.
  4. 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, and redirect_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.

3

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

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 set auth to plain.
  • As you can see from my Migadu account, the user and the from fields can differ. user indicates the username for authentication, while from 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 use passage 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.

5

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:

6

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. :)