Aerc Proton
Email with Proton Bridge, mbsync, and aerc
I had been using Neomutt with Proton bridge as terminal email setup for awhile and it just wasn’t working for me. After doing some investigation I landed on Aerc as a potential replacement. In the process I made some changes to my bridge setup. Previously I had setup a seperate user to avoid issues with pass with git commit signing and bridge. Now I have a seperate store for bridge. On thing I have disabled is tls between bridge and clients, the threat model tls protects against is irrelevant with communication only on localhost. If an attacker can see local host traffic I have bigger problems.
Proton Bridge
Proton bridge accesses and writes to a secrets store quite frequently. I have commit signing on by default for git and my pass store is a git repo. Given that my gpg key is on a hardware yubikey I end up having to enter my pin quite frequently. Previously this was solved with a seperate user to run bridge, howver this is painful to setup. On my new laptop I created a script to start bridge that uses a seperate per host pass store, this eliminates the signing issue for bridge use. On initial login to bridge run ‘info’ and then use pass to insert the displayed password into your store.
pass local/<hostname>-bridge
# ~/bin/start-bridge.sh
#!/bin/sh
PASSWORD_STORE_DIR=$HOME/.local/share/bridge-pass exec bridge --cli "$@"
Initialize the store before first run:
PASSWORD_STORE_DIR=~/.local/share/bridge-pass pass init <your-gpg-key-id>
Note: use PASSWORD_STORE_DIR=... pass init not pass init --path — the
--path flag initializes a subdirectory within an existing store, not a new
store at an arbitrary location.
SSL on localhost
Bridge generates a self-signed cert for its local IMAP endpoint. For a
localhost-only connection this provides essentially no security benefit — an
attacker with local access has bigger problems available to them. Use
SSLType None in mbsync for simplicity:
# ~/.mbsyncrc
TLSType None
mbsync
Full config with hostname-based pass lookup
Using $HOSTNAME in the pass path makes the config portable across machines — each machine stores its own Bridge password under local/$HOSTNAME-bridge and the same .mbsyncrc works everywhere:
IMAPAccount proton
Host 127.0.0.1
Port 1143
User youremail@example.com
PassCmd "/usr/bin/pass local/$HOSTNAME-bridge"
TLSType None
IMAPStore protonmail-remote
Account protonmail
MaildirStore protonmail-local
Path ~/.mail/example.com/
Inbox ~/.mail/example.com/Inbox
Flatten .
Channel example.com
Far :protonmail-remote:
Near :protonmail-local:
Patterns * !"All Mail" !Inbox !tmpsearch
aerc
Proton Bridge will create a ‘Recovered Email’ box if emails get lost between the local store and bridge. To avoid this we want to make sure we move emails instead of deleting. To enforce this I enabled the ‘restrict-delete’ option in aerc. This is a per account option so probably need to enable it for gmail or other providers as well. We set smtp+insecure to match mbsyncrc and make setup easier.
accounts.conf
The outgoing URL requires URL-encoding the @ in the email address (the @
separating username from hostname must remain unencoded):
#.config/aerc/accounts.conf
[Proton]
source = maildir://~/.mail/example.com/
outgoing = smtp+insecure://youremail%40example.com@127.0.0.1:1025
outgoing-cred-cmd = pass local/$HOSTNAME-bridge
check-mail-cmd = mbsync example.com
check-mail-timeout = 30s
default = INBOX
from = Your Name <youremail@example.com>
copy-to = Sent
folders-sort = Inbox, Starred, Sent, Labels.followup, Labels.readlater
restrict-delete = true
aerc.conf — filters and pager
The critical combination for readable HTML email:
# .config/aerc/aerc.conf
#...
[ui]
sort="-r date"
#...
[viewer]
pager = bat --style=plain --theme=ansi
#...
[filters]
text/plain = colorize
text/calendar = calendar
message/delivery-status = colorize
message/rfc822 = colorize
text/html = html2text --ignore-links | sed 's/^/\x1b[39m/'
#...
--theme=ansi is essential — without it bat applies its own color theme
which produces invisible text in light terminal themes. ansi defers color
decisions entirely to the terminal, making it compatible with both light and dark
mode switching.
The sed ANSI reset on each line ensures html2text’s plain text output uses
the terminal’s default foreground color rather than whatever the pager happens
to have active.
Address book
khard integration for tab-completion in the composer:
# .config/aerc/aerc.conf
#...
[compose]
address-book-cmd = khard email --parsable --remove-first-line %s
#...
Completion is triggered by <C-y> (defined in binds.conf as $complete) like vim.
binds.conf — custom keys
Key customizations made from the defaults:
# .config/aerc/binds.conf
#...
[messages]
# Semantic remapping — m for move, c for compose
O = :check-mail<Enter>
m = :menu -dc fzf :move<Enter>
c = :compose<Enter>
# Delete moves to Trash (safer than expunge with restrict-delete=true)
dd = :read<Enter>:move Trash<Enter>
# Folder navigation with g-prefix
gi = :cf Inbox<Enter>
gr = :cf Labels.readlater<Enter>
gf = :cf Labels.followup<Enter>
gs = :cf Sent<Enter>
gt = :cf Trash<Enter>
gd = :cf Drafts<Enter>
# Fuzzy folder picker via fzf
gf = :menu -c :cf -f 'fzf --layout=reverse'<Enter>
[view]
m = :menu -dc fzf :move<Enter>
dd = :read<Enter>:move Trash<Enter>
fzf folder picker
aerc’s :menu command pipes a list to any external filter and passes the
selection back to any command. This gives fuzzy folder selection for both
navigation and moving messages:
# Fuzzy move
m = :menu -dc fzf :move<Enter>
# Fuzzy folder switch
gf = :menu -c :cf -f 'fzf --layout=reverse'<Enter>
The -dc flag in :menu -dc fzf :move sets fzf as the default menu command
for this invocation.
URL handling with context
aerc’s :pipe sends the raw message source to a command regardless of what
filter rendered the view. To get urlscan with readable context from HTML emails,
pipe through html2text first:
gx = :pipe sh -c 'html2text --ignore-links | urlscan'<Enter>
Light/dark mode compatibility
The full terminal color inheritance chain requires every tool to defer color decisions upward:
- bat:
--theme=ansi— uses terminal ANSI colors, not bat’s own theme - aerc styleset: default
- html filter: ANSI reset escape in the sed pipe ensures plain text output uses terminal default foreground
With these in place, aerc inherits from the terminal, which inherits from your Sway/Ghostty theme toggle. One toggle, everything follows.
Security considerations
What the Bridge architecture protects
- Mail is E2EE on Proton’s servers — Proton cannot read it
- Transit to Proton’s servers uses TLS — network interception is mitigated
- Bridge decrypts locally — your private keys never leave your machine
What it doesn’t protect
- Local filesystem access means access to your decrypted Maildir
- The Bridge IMAP password (localhost only, low stakes) is stored in an isolated pass store protected by your main GPG key on a Yubikey
- Yubikey requirement means Bridge sync only works with the key physically present
Per-device Bridge vs centralized server
Running Bridge on each device (rather than a homelab server) keeps decrypted mail off always-on network-accessible machines. Manual sync via mbsync reinforces this — mail only syncs when you’re present and intentional.
Dependencies
protonmail-bridge
isync (mbsync)
aerc
pass
html2text (python-html2text)
bat
urlscan
khard
fzf