TUI frontend for easy NeoMutt management
  • Go 98.8%
  • Python 1%
  • Makefile 0.2%
Find a file
2026-06-16 12:53:48 +01:00
assets v1.3.1: Restructure into internal/ packages — declutter root, extract crypto + eventlog 2026-06-16 04:31:22 +01:00
internal Prevent issue with account form 2026-06-16 12:53:48 +01:00
.gitignore v1.2.0: CardDAV contacts — khard/vdirsyncer address books + recipient completion 2026-06-15 20:59:18 +01:00
go.mod Initial commit 2026-06-11 04:07:09 +01:00
go.sum Prepare for v1.0.0: version flag, tests, and pending features 2026-06-11 05:48:32 +01:00
Jenkinsfile v1.3.1: Restructure into internal/ packages — declutter root, extract crypto + eventlog 2026-06-16 04:31:22 +01:00
LICENSE Initial commit 2026-06-11 04:07:09 +01:00
main.go v1.3.1: Restructure into internal/ packages — declutter root, extract crypto + eventlog 2026-06-16 04:31:22 +01:00
Makefile v1.3.1: Restructure into internal/ packages — declutter root, extract crypto + eventlog 2026-06-16 04:31:22 +01:00
README.md v1.7.0: Tier Polish — theme builder, config backup, per-folder docs, troubleshooting 2026-06-16 05:04:29 +01:00

weremutt — NeoMutt, tamed · encrypted · themed · multi-account

NeoMutt, tamed.

A friendly TUI that generates and maintains your entire NeoMutt + mbsync + msmtp mail setup — accounts, contacts, encryption, themes — then drops you straight into your inbox.

Go 1.22+ macOS | Linux Bubble Tea 20 themes Native Go OAuth2 MIT

Quick start · Features · Contacts · Shortcuts · Developers


Terminal mail with NeoMutt is wonderful once it works — but the setup is spread across half a dozen fiddly files with cryptic option names. Weremutt owns the boring parts. It generates and maintains a clean, correct configuration so you spend your time reading mail, not debugging config.

  ┌ weremutt ──────────────────────────────────────────┐
  │  [logo]                                             │
  │                                                     │
  │   Open Mail          Unlock, sync, open NeoMutt    │
  │    Open Contacts      Browse your address book      │
  │    Mail Accounts      Add, edit, delete accounts    │
  │    Contact Accounts   Address-book sources          │
  │    Settings           General NeoMutt behaviour     │
  │    Themes             Colour scheme                 │
  │    Keybindings        Customise key bindings        │
  │    Encryption         Encrypt local mail at rest    │
  │    Sync Contacts      Sync CardDAV address books    │
  │    Sync All Mail      Run mbsync for everything     │
  │    Quit                                             │
  │                                                     │
  │  theme: Catppuccin Mocha · ↑↓ move · ⏎ select · q   │
  └─────────────────────────────────────────────────────┘

Features

📬 Multi-account IMAP, Microsoft 365 (OAuth2) and Google Workspace (OAuth2) — up to 12 accounts, switched with F1F12.
👥 Contacts that autocomplete Address-book sources — CardDAV (synced with vdirsyncer) or a local vCard directory — feed NeoMutt's recipient completion via khard. Global by default, or scoped to specific accounts.
🔐 Mail encrypted at rest Optional gocryptfs store: ciphertext on disk, plaintext mounted only while you read.
🔑 Secrets done right IMAP/CardDAV passwords and OAuth tokens are all GPG-encrypted. Nothing sensitive is ever written in plain text.
🎨 20 live-preview themes Catppuccin (×4), Dracula, Nord, Gruvbox, Tokyo Night, One Dark, Solarized, Monokai, Rosé Pine, Everforest, Kanagawa, Ayu Dark. Needs a truecolor terminal.
🧩 Plain-English settings Arcane NeoMutt options presented with human-readable names, one-line descriptions and sane defaults.
🌐 HTML mail that renders Generated mailcap + auto_view text/html so HTML displays as readable text inline; open images/PDFs with v.
🏢 Works around locked-down tenants Optional per-account Microsoft Graph send mode for M365 tenants that disable SMTP AUTH. OAuth is native Go — no Python.
🧹 Clean terminal Mount messages, sync progress and background-job notices are suppressed, so quitting leaves a tidy prompt.

🚀 Quick start

# 1. Install the tools (macOS shown; see Setup for apt/pacman)
brew install neomutt isync msmtp gnupg w3m

# 2. Build & install Weremutt
git clone https://forge.quinlan.cloud/joshquinlan/weremutt.git
cd weremutt && make install        # -> ~/.local/bin/weremutt

# 3. Run it
weremutt

Then: Mail Accounts add your account (IMAP passwords are GPG-encrypted on save; for MS365/GSuite press o to do the OAuth2 browser sign-in) → optionally Encryption Set Up EncryptionOpen Mail. Weremutt unlocks, syncs, opens NeoMutt, and re-locks on exit.

In the account form, typing a plain-IMAP email autodetects the IMAP/SMTP servers from Mozilla's provider database (so common providers "just work"), the Test button probes the IMAP server before you save, and save is blocked if no GPG key exists for the "encrypt to" recipient — so you never seal secrets to a key you can't decrypt with.

That's it. Everything below is reference.

Setup

Requirements

  • NeoMutt, isync (mbsync), msmtp, GnuPG
  • A terminal browser (w3m, lynx or elinks) to render HTML mail inline
  • khard + vdirsyncer (optional) for Contacts — recipient auto-completion from an address book
  • gocryptfs (optional) for mail-at-rest encryption; on macOS also macFUSE
  • Go 1.22+ to build
# macOS
brew install neomutt isync msmtp gnupg w3m khard vdirsyncer
brew install --cask macfuse
brew install gromgit/fuse/gocryptfs-mac
# approve macFUSE in System Settings  Privacy & Security and reboot

# Debian / Ubuntu
sudo apt install neomutt isync msmtp gnupg gocryptfs w3m khard vdirsyncer

# Arch
sudo pacman -S neomutt isync msmtp gnupg gocryptfs w3m khard vdirsyncer

khard and vdirsyncer are Python tools. They're the one place Weremutt leans on a Python runtime — invoked as external binaries (like mbsync/msmtp), never embedded. They're only needed if you use Contacts. OAuth stays native Go.

Install

git clone https://forge.quinlan.cloud/joshquinlan/weremutt.git
cd weremutt
make install        # builds and installs to ~/.local/bin/weremutt

Then follow the Quick start above.

Shortcuts

These are the key bindings you'll use in NeoMutt. All bindings are editable from the Keybindings screen in Weremutt (macros are shown there for reference).

Navigation

Key Action
j / k Next / previous message
= / * First / last message

Reading

Key Action
Enter Open message (index) / scroll down a line (pager)
Backspace Scroll up a line (pager)
Space Next page (pager)
- Previous page (pager)
h Show all headers — toggle (pager)
q Back / close (pager)

Mail actions

Key Action
n Compose new message
r Reply
g Reply all
f Forward
s Move folder
a Archive
d Delete
R Continue postponed draft
b Bounce (resend) message

Sending

When you've finished writing in the editor, the compose menu lets you review recipients and send. Shortcuts:

Key Action
y Send message
s Edit subject
t / c / b Edit To / Cc / Bcc
Tab Autocomplete the recipient from your contacts (in a To/Cc/Bcc field)
p / S Security — PGP / S/MIME (sign, encrypt)
a Attach file
P Postpone draft
i Check spelling
q Abort send
Key Action
/ Search
N Next search match

Folders & accounts

Key Action
Ctrl-n / Ctrl-p Next / previous folder in sidebar
Ctrl-o Open selected folder
B Show / hide sidebar
F1 F12 Switch account

Sync & read state

Key Action
,r Mark current folder read
,,r Mark whole account read
,s Sync current account
,S Sync all accounts

Contacts

Weremutt can feed NeoMutt's recipient auto-completion from an address book. Press Tab in a To/Cc/Bcc field while composing and NeoMutt offers matching contacts. Two external tools do the work, invoked like mbsync/msmtp:

  • vdirsyncer syncs each CardDAV collection down into a local directory of .vcf files.
  • khard reads those vCard directories and emits NeoMutt-format completion lines.

Both are Python tools — the one Python dependency in the stack. They're external system packages, not embedded; install them only if you want Contacts. (OAuth remains native Go.)

Manage everything from the Contact Accounts main-menu entry: add/edit/delete sources, choose the type, and set the optional per-account assignment. The screen also shows whether khard/vdirsyncer are installed. To browse your whole address book at any time, use Open Contacts from the main menu (it mounts the encrypted store if needed, then lists every contact with khard).

Source types

  • CardDAV (default). A generic CardDAV base URL + username + app-password. The password is GPG-encrypted on save (secrets/<slug>.carddav.gpg), exactly like an IMAP password. Weremutt uses vdirsyncer's collection discovery, so you only supply the base URL. Google works with an app password.
  • Local vCard. A directory of .vcf files with no sync — drop vCards into the source's contacts directory and they're queried directly.

Microsoft 365 has no CardDAV endpoint and is not supported as a contact source. (Mail still works over IMAP/Graph; only CardDAV contact sync is unavailable for M365.)

Scoping (global by default, optionally scoped per account)

The model is additive, keyed on each source's assignment — not on the active account:

  • A source with no account assignment is global: available to every account.
  • Assigning a source to one or more accounts makes it visible only to those accounts (it stops being global).
  • While account X is active, completion searches the union of (all global sources) (sources assigned to X).

To keep personal contacts out of a work account, assign the personal source(s) to your personal account(s) — they stop being global, so the work account no longer sees them. A source meant to be everywhere is simply left unassigned. Under the hood, Weremutt writes a base query_command (covering the global books) into neomuttrc and a per-account query_command override (covering global + that account's assigned books) into each account's muttrc, re-sourced by the folder-hook on switch.

Syncing

Contact sync runs only via a dedicated action — never coupled to mbsync / Open mail:

  • Sync Contacts in the main menu, or
  • weremutt sync-contacts on the command line, so it can be dropped into cron.

When mail-at-rest encryption is on, the local vCard store lives inside the gocryptfs store, so sync-contacts mounts the store, runs vdirsyncer, then unmounts — self-contained. A consequence: with encryption on, khard completion only works while mail is unlocked/mounted (the books are encrypted at rest alongside the caches).

Adding contacts. Add Contact in the main menu opens khard's new-contact editor (unlocking the store first if needed). vdirsyncer's sync is two-way, so a contact you create locally is pushed to the CardDAV server on the next Sync Contacts.

MS365 contacts. Microsoft 365 exposes contacts via Graph, not CardDAV, so they aren't synced today. Pulling them would need a Graph token with a Contacts scope — the current Graph tokens are scoped to Mail.Send only, so this needs a re-authorization flow and is intentionally left for a future release rather than silently broadening token scopes.

Cron note. For non-interactive runs the gocryptfs passphrase must be supplied via WEREMUTT_GCFS_PW, and gpg-agent must already hold the key that decrypts the CardDAV password (Weremutt primes the agent during interactive use, but cron has no such session):

# every 30 min — assumes gpg-agent is primed and (if encrypted) WEREMUTT_GCFS_PW is exported
*/30 * * * * weremutt sync-contacts >/dev/null 2>&1

Security note — WEREMUTT_GCFS_PW. While a process that reads this variable is running, the passphrase is visible to other processes owned by your user via /proc/<pid>/environ (Linux) or ps -E (some systems). This is an accepted trade-off for unattended cron sync. Treat it accordingly: keep it out of shell history, prefer setting it inside the crontab entry (not a world-readable file), and lock down your crontab perms (chmod 600). For interactive use you never need it — Weremutt prompts for the passphrase in-app.

Command line

Weremutt is an interactive TUI — run it with no arguments. It also exposes a few subcommands (run weremutt --help for the full list with environment overrides):

Command What it does
weremutt Start the interactive TUI
weremutt doctor Preflight checks — required tools, a usable GPG key, FUSE, and the deps for any features you've enabled — printed as a green/red checklist with the exact install command per platform. Also available as the Diagnostics menu item.
weremutt sync-contacts Sync CardDAV address books with vdirsyncer — self-contained and cron-friendly (mounts the encrypted store if needed, then unmounts)
weremutt reindex Update the notmuch index now (only when notmuch search is enabled in Settings)
weremutt export [file] Bundle the config — accounts, settings, themes, and the already-GPG-sealed secrets — into a tarball, so a new machine is a restore, not a re-setup
weremutt import <file> Restore a bundle made by weremutt export (needs your GPG private key to use the sealed secrets)
weremutt --version, -v Print version and exit
weremutt --help, -h Show full help

The remaining subcommands (oauth-token, oauth-authorize, graph-authorize, graph-send, refresh-contacts, resync-loop) are invoked automatically by mbsync / msmtp / NeoMutt and aren't meant to be run by hand.

Sync visibility. Background sync is quiet by design, but no longer silent about failure. Every bulk sync ("Open Mail") and every background resync records its outcome to $XDG_CACHE_HOME/weremutt/weremutt.log and a small sync.state file, and the main menu shows "last synced 14m ago" or, in red, "last sync failed — ". So a mailbox that has quietly stopped updating is now visible at a glance rather than a surprise.

Troubleshooting

Start with weremutt doctor. It checks the tools, GPG key, FUSE and feature dependencies for your setup and prints the exact fix command for your platform. Most of the problems below are caught there first.

Symptom Likely cause & fix
"Open Mail" fails to mount / hangs at unlock macFUSE isn't installed or wasn't approved. Install it (brew install --cask macfuse), then approve it in System Settings ▸ Privacy & Security and reboot. doctor reports this as a failed FUSE check.
OAuth sync fails on macOS (SASL ... no mechanism) The XOAUTH2 SASL plugin isn't built. Run the OAuth Plugin menu item (it appears once you have an MS365/Google account) to compile cyrus-sasl-xoauth2.
Sync silently stopped Check the menu's "last synced" line and $XDG_CACHE_HOME/weremutt/weremutt.log. A last sync failed line carries the tail of mbsync's error.
gpg: decryption failed / pinentry never appears gpg-agent doesn't have your key cached. Run Open Mail once (it primes the agent); for cron, the agent must already be primed and WEREMUTT_GCFS_PW exported.
HTML mail shows as "unsupported" No terminal browser. Install w3m (or lynx/elinks).
notmuch search does nothing It's opt-in (Settings ▸ notmuch search) and needs a notmuch-enabled NeoMutt build plus the notmuch binary (doctor checks both once enabled). Run weremutt reindex to (re)build the index.
Colours look wrong / "Direct colours support disabled" Weremutt's themes need a truecolor terminal (e.g. Ghostty, Kitty, modern iTerm).
A folder you don't want keeps syncing On the account list press f (Folders) and untick it — only ticked folders sync (INBOX is always kept).

Backing up / moving machines: weremutt export bundles your whole config (secrets stay GPG-sealed); weremutt import restores it. Bring your GPG private key across separately — without it the sealed secrets can't be decrypted.

Custom themes: in Themes, press n to clone the highlighted theme and tweak its palette by hex value with live preview; saved themes join the list and are written as a NeoMutt colour file like the built-ins.

For developers

Building and testing

make build      # builds ./weremutt
make test       # runs go test ./...
make install    # installs to ~/.local/bin/weremutt

OAuth2 (MS365 & Google Workspace)

Add the account, then press o on it in the Mail Accounts list to run the browser sign-in. OAuth token minting and refresh are handled natively by Weremutt itself — there is no external Python helper or other runtime dependency. Existing token files written by older (Python-based) versions keep working unchanged.

  • MS365 uses Thunderbird's public client ID by default (no app registration needed). If your organisation blocks third-party IDs, register your own app in the Entra portal (Mobile & desktop redirect http://localhost) and set WEREMUTT_MS_CLIENT_ID.
  • Google Workspace requires a project in the Google Cloud Console with the Gmail API enabled, an OAuth consent screen, and a Desktop app client ID. Weremutt prompts for your client ID/secret on first authorize.

Microsoft Graph send mode

Some Microsoft 365 tenants run Security Defaults, which disables SMTP AUTH for every mailbox. Sending then fails with 535 5.7.139 ... SmtpClientAuthentication is disabled for the Tenant, because msmtp can no longer authenticate over SMTP — even with a valid OAuth token.

Graph mode works around this by sending through the Microsoft Graph sendMail API (Mail.Send), which those tenants still permit. It is off by default and configured per account (different accounts can use different tenants). Receiving is unaffected — mail still syncs over IMAP XOAUTH2 as usual; only the send path changes.

When enabled, NeoMutt's sendmail is pointed at Weremutt's own graph-send translation layer instead of msmtp. It takes the composed message and POSTs it to Graph as raw MIME, so HTML, attachments and signatures are delivered exactly as composed. Because Graph's MIME send always stores a server-side copy in Sent Items, Weremutt unsets NeoMutt's local record for these accounts (mbsync pulls the single server copy back down — no duplicates).

Enable it in Mail Accounts edit on an MS365 account: turn graph send mode on and pick an auth flow.

  • App-only (client credentials). Register an app in the Entra portal, grant it the Mail.Send application permission, and grant admin consent. Enter the tenant ID, client ID and client secret in the form (the secret is stored GPG-encrypted, like other Weremutt secrets). Sends as /users/<email>/sendMail; no interactive sign-in. Press o on the account to run a live credential check.
  • Delegated (user sign-in). Register an app with the Mail.Send delegated permission and a Mobile & desktop redirect of http://localhost. Enter the tenant ID and client ID, then press o on the account to authorize — this signs in and mints a Graph-scoped token (separate from the IMAP/SMTP token, which has a different audience, so o runs both flows). Sends as /me/sendMail.

macOS only: Homebrew's mbsync is linked against Apple's system libsasl2, which breaks XOAUTH2 with a callback-18948 error. When you have an OAuth account, an OAuth plugin item appears in the main menu. Run it once to build and install the cyrus-sasl-xoauth2 plugin into ~/.config/weremutt/sasl2/. Requires Homebrew and Xcode command-line tools.

Encryption

Ciphertext lives in $XDG_DATA_HOME/weremutt/mail.enc; the plaintext view is FUSE-mounted at $XDG_DATA_HOME/weremutt/mail only while mail is open. Set Up Encryption initialises the store and moves any existing plaintext mail aside to mail.plain.bak.*. Open Mail prompts for your passphrase inside the app (never echoed, never written to disk), syncs, opens NeoMutt, and unmounts on exit. When encryption is on, the local vCard store (and the header/body caches) live inside the mountpoint too, so they're encrypted at rest and only readable while mail is unlocked.

Changing an account's PGP key

An account's stored IMAP password (secrets/<slug>.gpg), OAuth token (oauth2/<slug>.token) and any Graph credentials (oauth2/<slug>.graph-token, secrets/<slug>.graph-secret.gpg) are GPG-encrypted to the account's encrypt to recipient (the signing key field, or the account email when left blank). If you later rotate that PGP key — generate a new one, or point the account at a different key — those files are still sealed to the old key. Decryption then fails, and sending/syncing reports the misleading Difficulty decrypting token file / gpg: ... No secret key.

To rotate the key cleanly:

  1. Trust the new key as your own. GPG refuses to encrypt to an untrusted key (gpg: <id>: There is no assurance / Unusable public key). Since it's your key, mark it ultimately trusted once:

    gpg --edit-key <new-key-id>   # then: trust → 5 (ultimate) → quit
    # or non-interactively:
    echo "<FULL_FINGERPRINT>:6:" | gpg --import-ownertrust
    
  2. Edit the account in Weremutt (Mail Accounts → e) and set the new signing key / "encrypt to" recipient, then Save. On save, Weremutt re-seals the stored credential to the new key:

    • If the old key is still in your keyring, the secret is decrypted and re-encrypted to the new key automatically — nothing else to do.
    • If the old key is gone, the credential can't be recovered. An OAuth token is moved aside (<slug>.token.stale-*) so you can press o to re-authorize; an IMAP password must be re-entered (edit the account and type it in the password field).

    If you see "Open mail to prime gpg-agent, then re-save", your old key's passphrase wasn't cached — open mail once (or unlock) to prime gpg-agent, then re-save the account.

Files managed

The layout is XDG-compliant. Config lives under $XDG_CONFIG_HOME/weremutt (default ~/.config/weremutt):

File Ownership
accounts/<slug>.muttrc, <slug>.sig per-account identity, folders, signature
mbsyncrc, msmtprc fully generated from your accounts
neomuttrc only the # >>> weremutt managed >>> blocks (hooks, settings, theme)
macros.muttrc account-switch and action macros
bindings.muttrc the managed keybindings block
mailcap HTML-to-text viewer and image/PDF attachment openers
themes/<name>.muttrc the selected colour scheme
contacts/sources/<slug>.source contact-source definitions
contacts/khard/*.conf, contacts/vdirsyncer/config generated khard + vdirsyncer config
sasl2/xoauth2.so macOS XOAUTH2 SASL plugin (built via the OAuth plugin menu item)
secrets/<slug>.gpg, oauth2/<slug>.token encrypted credentials (git-ignored)
secrets/<slug>.carddav.gpg encrypted CardDAV password (git-ignored)
oauth2/<slug>.graph-token, secrets/<slug>.graph-secret.gpg encrypted Graph send-mode credentials (git-ignored)

App-managed data lives under $XDG_DATA_HOME/weremutt (default ~/.local/share/weremutt):

Path Contents
mail/ the plaintext Maildir view / gocryptfs mountpoint (was ~/Mail)
mail.enc/ gocryptfs ciphertext at rest (was ~/.mail.enc)
mail/.cache/ header + body caches (inside the encrypted store)
contacts/ local vCard store, when encryption is off (when on, it lives in mail/.contacts/)

Anything outside the managed marker blocks is left untouched, so your own neomuttrc tweaks survive. Moving from an older (~/.config/neomutt + ~/Mail + ~/.mail.enc) install? Run the maintainer migration script, then start weremutt once so the managed files regenerate at the new locations.

Environment overrides

Variable Purpose Default
WEREMUTT_ROOT config root $XDG_CONFIG_HOME/weremutt (~/.config/weremutt)
WEREMUTT_DATA app-managed data root $XDG_DATA_HOME/weremutt (~/.local/share/weremutt)
WEREMUTT_MAIL Maildir root / mountpoint <data root>/mail
WEREMUTT_CIPHER gocryptfs ciphertext store <data root>/mail.enc
WEREMUTT_CACHE header + body cache root <mail root>/.cache
WEREMUTT_CONTACTS plaintext vCard store (encryption off) <data root>/contacts
WEREMUTT_GCFS_PW gocryptfs passphrase for non-interactive sync-contacts (cron) unset
WEREMUTT_DEBUG show mount/sync/teardown output unset (quiet)
WEREMUTT_MS_CLIENT_ID Microsoft OAuth2 client ID Thunderbird's public ID
WEREMUTT_NO_IMAGE disable the Kitty inline logo unset

WEREMUTT_ROOT / WEREMUTT_DATA / WEREMUTT_MAIL / WEREMUTT_CIPHER take precedence over the XDG resolution.

Security notes

  • IMAP passwords are stored only as gpg-encrypted files; decryption happens in memory via gpg-agent at sync time.
  • OAuth tokens are GPG-encrypted.
  • CardDAV contact-source passwords are GPG-encrypted (secrets/<slug>.carddav.gpg); vdirsyncer decrypts them on the fly at sync time.
  • Graph send-mode credentials (delegated token and app-only client secret) are GPG-encrypted too.
  • msmtprc is written 0600.
  • secrets/ and oauth2/ are git-ignored.

Built with

Bubble Tea, Bubbles and Lip Gloss. OAuth2 (IMAP/SMTP XOAUTH2 and Microsoft Graph) is handled natively in Go — no external helper. Theme palettes are from their respective upstream projects.

License

MIT. See LICENSE.