Installation
Dependencies
- Go 1.26 or newer
- libnotmuch (the default build links the cgo binding against
libnotmuch.so- install thenotmuchruntime and dev package of your distribution) - a running notmuch setup (
notmuch newwith your maildirs indexed - notmutt is a front-end, notmuch owns the database)
No libnotmuch available, or you prefer not to link it? The CLI backend is one build tag away - the same binary code, the same interface:
make build TAGS="cli"
Build
git clone git@github.com:fishman/notmutt.git
cd notmutt
make build
The Makefile default carries the Lua runtime (R8); TAGS overrides (make build TAGS="cli" for the CLI backend, make build TAGS="" for a plain Go build). make test runs the suite under the same tags. The binary lands at ./notmutt. Run it from anywhere; your configuration is read from ~/.config/notmutt/config.toml.
Packages
The release workflow builds three packages from a vMAJOR.MINOR.PATCH tag (packaging/): a deb and an rpm via nfpm, and an Arch package via makepkg. All three wrap the same Go binary; they split only on the link model.
deb and rpm
The deb and rpm embed libnotmuch statically: the release workflow builds notmuch 0.40 and links its archives into the binary. Static is forced, not chosen - the cgo binding calls notmuch_threads_status, which needs libnotmuch >= 0.40, and the default distro libnotmuch is older (Ubuntu ships 0.37/0.38). Embedding 0.40 statically is the only way the binary’s own libnotmuch calls compile and run there.
The packages still Depends: notmuch, because the binary shells out to the notmuch CLI for database.path (see packaging/notmutt.yaml). That runtime notmuch comes from the distro - on stock Ubuntu still 0.37/0.38, older than the 0.40 the client targets.
The deb and rpm are untested. CI builds and packages them but never runs them on a real system, and no stock distro ships libnotmuch 0.40, so the runtime dependency chain cannot be satisfied there. They are buildable artifacts, not validated installs. Use the Arch package, or build from source against your distro’s libnotmuch, if you need something you can run.
Arch
The Arch package is the usable path. Its PKGBUILD builds against the distro’s current libnotmuch (go build -tags lua, shared linking) - Arch’s rolling notmuch is new enough to satisfy the 0.40 requirement - and the runtime notmuch CLI matches. The release workflow also pushes it to the AUR; see packaging/notmutt/PKGBUILD.
Configuration
The client ships with its defaults as data: src/config/base.toml is the reference - every default (keybindings, themes, views, tag groups, pager settings) is visible there. Your user file overlays it; nothing in your file needs to restate a default. Start with an empty file if you want to discover the defaults, or copy base.toml and edit.
Configuration is strict by design: unknown keys are load errors, not silently ignored typos.
Transport: mbsync and msmtp
notmutt is a front-end: it never talks IMAP or SMTP itself (see the non-goals). Delivery in is mbsync (or vdirsyncer), delivery out is msmtp (or any mutt-compatible sendmail). The client reads the maildirs mbsync writes and calls the send command with the message on stdin. The reference setup ships in this repository - references/.mbsyncrc and references/.msmtprc are the live working shape: copy them, edit, and use them as the template for every additional account.
Receive: mbsync
One account is three blocks: an IMAPStore (the remote side), a MaildirStore (the local maildir), and a Channel wiring them together:
IMAPStore your.email@gmail.com-remote
PipelineDepth 3
Host imap.gmail.com
Port 993
User your.email@gmail.com
PassCmd "oama access your.email@gmail.com"
AuthMechs XOAUTH2
TLSType IMAPS
CertificateFile /etc/ssl/certs/ca-certificates.crt
MaildirStore your.email@gmail.com-local
Subfolders Verbatim
Path /home/you/Mail/gmail/
Inbox /home/you/Mail/gmail/INBOX
Channel your.email@gmail.com
Expunge Both
Far :your.email@gmail.com-remote:
Near :your.email@gmail.com-local:
Patterns INBOX "[Gmail]/Drafts" "[Gmail]/Sent Mail" "[Gmail]/Spam" "[Gmail]/Trash" Archives Pending
Create Both
SyncState *
MaxMessages 0
ExpireUnread no
The points that matter to notmutt:
- The MaildirStore
Pathis the account root. notmutt’s setup detection walks these roots, so every account needs its own directory (~/Mail/<account>/) and its own store/channel block trio, named by the account. Subfolders Verbatimkeeps the provider’s real folder names ([Gmail]/Drafts, …) as subdirectories. Setup’s provider detection and the client’s folder rules match against exactly those names - do not rename them away.PassCmdruns a command that prints the credential on stdout; nothing secret sits in the config. The reference usesoama(an OAuth token helper, the successor of mailctl) withAuthMechs XOAUTH2- the Gmail shape. oama is not on Homebrew; install it from its GitHub releases (purpleidea/oama), the AUR (oama-bin), orgo install, then authorize your account once and oama renews the token in the background. App passwords work the same way:PassCmd "echo ..."is the mechanism, oama is just the reference’s command.Patternslists the folders to mirror. Everything the client’s tag pipeline classifies (see the mail concept) must be mirrored; the Gmail special folders are the bracketed names above.
Index: notmuch
notmuch is the single source of truth (R1): it owns the database, the client only reads and tags. Its config (~/.notmuch-config) tells it where the mail lives and how to treat new mail; the reference lives at references/muttrc/toolconfig/notmuch-config. The parts that matter to the chain:
[database]
path=Mail
[new]
tags=unread;inbox;
ignore=.mbsyncstate;.uidvalidity;.cache;
[maildir]
synchronize_flags=true
pathis the mail root, relative to$HOME- the directory that holds every account’s Maildir (~/Mailin this document’s example, matching the mbsync store paths above).[new] tagsis the initial setnotmuch newstamps on delivery: theunread;inbox;the client’s inbox view and the unread flag rely on. The client’s views are tag queries - change the stamp and the views move with it.ignoreis the mbsync glue: mbsync drops.mbsyncstate,.uidvalidityand its cache files into every maildir, and without the ignore list notmuch would index those dotfiles as mail.synchronize_flagskeeps the maildir flags and the notmuch tags in sync - the physical side of the tag model. The[user]identity block feeds notmuch’s own addressing (its reply tools); the client’s sender addresses come fromaccounts.toml.
Index once, before the client ever runs:
notmuch new
Send: msmtp
msmtp is a mutt-compatible sendmail: it reads the message on stdin, authenticates, and hands it to the server. The reference references/.msmtprc shows the shape:
defaults
auth on
tls on
logfile ~/.msmtp.log
account gmail
host smtp.gmail.com
from your.email@gmail.com
user your.email@gmail.com
port 587
auth oauthbearer
passwordeval "oama access your.email@gmail.com"
tls_trust_file /etc/ssl/certs/ca-certificates.crt
The contract with notmutt: the client runs the configured [send] command (default msmtp) with the envelope recipients as argv and the assembled message on stdin, and msmtp picks the account from the message’s From header (its --read-envelope-from default). That is per-account sending: choose the sender in the compose dialogue (A), the From header follows, msmtp routes by it. One account block per address, from and user matching the account’s From address.
Like mbsync, the credential is a command, never plaintext: passwordeval runs it and reads stdout (oama again for the Gmail OAuth case). tls_trust_file is your distribution’s CA bundle.
Accounts
Run notmutt setup once after indexing your mail. It walks the notmuch mail root, detects each account from its folder structure, and writes ~/.config/notmutt/accounts.toml (0600) with one [accounts.<name>] entry per match:
$ ./notmutt setup
setup: accounts: gmail (gmail), acme (outlook)
setup: no template match: atlas
setup: wrote /home/you/.config/notmutt/accounts.toml
Detection is template-driven. Each built-in provider shape (gmail, exchange, icloud, zoho, outlook) names the folders that must exist at the account root - gmail is gated by a top-level [Gmail], exchange by Sent Items, zoho by Snoozed - then maps the hard tags (inbox, sent, draft, spam, deleted, archive, pending) to that provider’s real folder names: [Gmail]/Drafts, Sent Items, Archives vs Archive. Candidates are priority-ordered, first existing folder wins, and a flat layout falls back to the provider names. Only directory names are read, never mail content.
setup is detection output, not a complete account: from and default_signature are yours to fill in. The [accounts.<name>] table then carries the full surface:
| key | meaning |
|---|---|
from | sender address used to prefill the compose dialogue |
default_signature | signature file name in the account’s signatures dir |
folder | the account’s folder prefix (the account tag); derived by setup |
folders | the hard-tag -> folder map; derived by setup |
no_fcc | skip the sent copy (the provider stores it server-side); setup sets it for gmail/zoho |
readonly | never classify, never move, never tag this account’s mail |
return_inbox | trash returns to inbox instead of staying deleted |
preset | provider preset name for the default move rules |
moves | per-tag move overrides (tag -> folder candidates) |
Accounts drive the compose dialogue: A picks the sender account, reply mode resolves it from the message’s account tag, and the Fcc path derives from the account’s sent-folder map at send time. The generated accounts.toml merges with the rest of your config automatically - edit it freely, re-run notmutt setup to regenerate (a from you typed by hand is overwritten only if you delete the file; regeneration writes the whole file).
Provider-specific setup notes and template shapes (Gmail, Outlook, iCloud, Zoho, Exchange, and the flat layouts) are welcome - if your provider’s folder names do not match, open an issue with its folder list, or contribute a template: drop a lua/templates/<provider>.lua in the config dir, enable it via [setup] templates, and send a PR mirroring it in src/setup/setup.go. A maildir fixture with the provider’s folder names (no mail inside) is all a template test needs.
Terminal requirements
- Truecolor (
COLORTERM=truecolor); the R11 baseline is truecolor, no 256-color mapping - Images: Kitty graphics is probed automatically at startup, with a 50 ms timeout. If the probe fails, negotiated sixel support is used (foot, mlterm, xterm with
-ti 340, or a sixel-enabled tmux build). - No image protocol setting is needed. Remove any existing
[pager] image-protocolentry; unsupported terminals remain text-only.