A minimal terminal email client for people who write in Markdown and live in Neovim.
Neomd is my way of implementing an email TUI based on my experience with Neomutt, focusing on Neovim (input) and reading/writing in Markdown and navigating with Vim Motions with the GTD workflow and HEY-Screener.
Warning
neomd moves, deletes, and modifies emails directly on your IMAP server. These operations affect your mailbox across all devices. Back up important emails before first use. neomd is experimental software and can't take responsibility for lost or misplaced emails. Consider testing with a secondary email account first.
The key here is speed in which you can navigate, read, and process your email. Everything is just a shortcut away, and instantly (ms not seconds). It's similar to the foundations that Superhuman was built on: it runs on Gmail and makes it fast with vim commands.
With the HEY-Screener, you get only emails in your inbox that you screened in, no spam or sales pitch before you added them. Or don't like them, just screen them out, and they get automatically moved to the "ScreenedOut" folder.
With the GTD approach, using folders such as next (inbox), waiting, someday, scheduled, or archive, you can move them with one shortcut. This allows you quickly to move emails you need to wait for, or deal with later, in the right category. Processing your email only once.
With the additional Feed and Papertrail, two additional features from HEY, you can read newsletters (just hit F) on them automatically in their separate tab, or move all your receipts into the Papertrail. Once you mark them as feed or papertrail, they will moved there automatically going forward. So you decide whether to read emails or news by jumping to different tabs.
Note
neomd's speed depends entirely on your IMAP provider. On Hostpoint (the provider I use), a folder switch takes ~33ms which feels instant. On Gmail, the same operation takes ~570ms which is noticeably slow. See Benchmark for full details and how to test your provider.
Feed view with all Newsletters - also workflow with differnt tabs and unread counter only for certain tabs (not all):

Reading an email with Markdown 💙:
This is the markdown sent:
# [neomd: to: email@domain.com]
# [neomd: subject: this is an email from neomd!]
This email is from Neomd. Great I can add links such as [this](https://ssp.sh) with plain Markdown.
E.g. **bold** or _italic_.
## Does headers work too?
this is a text before a h3.
### H3 header
how does that look in an email?
Best regardsCompose emails in your editor, read them rendered with glamour, and manage your inbox with a HEY-style screener — all from the terminal.
Which looks like this:
YouTube rundown of most features:

- Write in Markdown, send beautifully — compose in
$EDITOR(defaults tonvim), send asmultipart/alternative: raw Markdown as plain text + goldmark-rendered HTML so recipients get clickable links, bold, headers, inline code, and code blocks - Pre-send review — after closing the editor, review To/Subject/body before sending; attach files, save to Drafts, or re-open the editor — no accidental sends
- Attachments — attach files from the pre-send screen via yazi (
a); images appear inline in the email body, other files as attachments; also attach from within neovim via<leader>a; the reader lists all attachments (including inline images) and1–9downloads and opens them - Link opener — links in emails are numbered
[1]-[0]in the reader header; pressspace+digitto open in$BROWSER - CC, BCC, Reply-all — optional Cc/Bcc fields (toggle with
ctrl+b);Rin the reader replies to sender + all CC recipients - Drafts —
din pre-send saves to Drafts (IMAP APPEND);Ein the reader re-opens a draft as an editable compose; compose sessions are auto-backed up to~/.cache/neomd/drafts/so you never lose an unsent email (:recoverto reopen) - HTML signatures — configure separate text and HTML signatures; text signature appears in editor and plain text part, HTML signature in HTML part only; use
[html-signature]placeholder to control inclusion per-email - Multiple From addresses — define SMTP-only
[[senders]]aliases (e.g.s@ssp.shthrough an existing account); cycle withctrl+fin compose and pre-send; sent copies always land in the Sent folder - Undo —
ureverses the last move or delete (x,A,M*) using the UIDPLUS destination UID - Search —
/filters loaded emails in-memory;space /or:searchruns IMAP SEARCH across all folders (only fetching header capped at 100 per folder) with results in a temporary "Search" tab; supportsfrom:,subject:,to:prefixes - Address autocomplete — To/Cc/Bcc fields autocomplete from screener lists; navigate with
ctrl+n/ctrl+p, accept withtab - Everything view —
geor:everythingshows the 50 most recent emails across all folders; find emails that were screened out, moved to spam, or otherwise hard to locate - Threaded inbox — related emails are grouped together in the inbox list with a vertical connector line (
│/╰), Twitter-style; threads are detected viaIn-Reply-To/Message-IDheaders with a subject+participant fallback; newest reply on top, root at bottom;·reply indicator shows which emails you've answered - Conversation view —
Tor:threadshows the full conversation across folders (Inbox, Sent, Archive, etc.) in a temporary tab with[Folder]prefix; see your replies alongside received emails - Glamour reading — incoming emails rendered as styled Markdown in the terminal
- HEY-style screener — unknown senders land in
ToScreen; pressI/O/F/Pto approve, block, mark as Feed, or mark as PaperTrail; reuses your existingscreened_in.txtlists from neomutt - Folder tabs — Inbox, ToScreen, Feed, PaperTrail, Archive, Waiting, Someday, Scheduled, Sent, Trash, ScreenedOut
- Multi-select —
mmarks emails, then batch-delete, move, or screen them all at once - Auto-screen on load — screener runs automatically every time the Inbox loads (startup,
R); keeps your inbox clean without pressingS(configurable, on by default) - Background sync — while neomd is open, inbox is fetched and screened every 5 minutes in the background; interval configurable, set to
0to disable - Kanagawa theme — colors from the kanagawa.nvim palette
- IMAP + SMTP — direct connection via RFC 6851 MOVE, no local sync daemon required and keeps it in sync if you use it on mobile or different device
Prerequisites: Go 1.22+ and make.
Note
Optional attachment helpers:
yazienables the built-in file picker used by pre-senda- custom Neovim integration in
custom.luaenables inline<leader>aattachment insertion insideneomd-*.mdbuffers - without these, neomd still works; the inline Neovim attachment workflow just won't be available
git clone https://github.com/ssp-data/neomd
cd neomd
make install # installs to ~/.local/bin/neomdOr just build locally:
make build
./neomdOr if on Arch Linux (AUR), you can use my neomd-bin via:
yay -S neomd-binOn first run, neomd:
- Creates
~/.config/neomd/config.tomlwith placeholders — fill in your IMAP/SMTP credentials- Important: Make sure that the Capitalization and naming of folder in
config.tomlis accroding to webmail IMAP, e.g. Gmails usessent = "[Gmail]/Sent Mail"and notsentetc.
- Important: Make sure that the Capitalization and naming of folder in
- Creates
~/.config/neomd/lists/for screener allowlists (or uses your custom paths from config) - Creates any missing IMAP folders (ToScreen, Feed, PaperTrail, etc.) automatically
Neomd also runs on Android (more for fun) — see docs/android.md.
On first run, neomd creates ~/.config/neomd/config.toml with placeholders:
[[accounts]]
name = "Personal"
imap = "imap.example.com:993" # :993 = TLS, :143 = STARTTLS
smtp = "smtp.example.com:587"
user = "me@example.com"
password = "app-password"
from = "Me <me@example.com>"
starttls = false
tls_cert_file = "" # optional PEM cert/CA for self-signed local bridges
# Root-level settings
store_sent_drafts_in_sending_account = false # default: Sent/Drafts use first account; true = follow sending account
[screener]
screened_in = "~/.config/neomd/lists/screened_in.txt"
screened_out = "~/.config/neomd/lists/screened_out.txt"
feed = "~/.config/neomd/lists/feed.txt"
papertrail = "~/.config/neomd/lists/papertrail.txt"
spam = "~/.config/neomd/lists/spam.txt"Use an app-specific password (Gmail, Fastmail, Hostpoint, etc.) rather than your main account password. The password and user fields support environment variable expansion ($VAR or ${VAR}) so you can avoid storing secrets in the config file.
For the full configuration reference including multiple accounts, OAuth2 authentication, [[senders]] aliases, folder customization, signatures, and UI options, see docs/configuration.md.
Provider-specific guides:
- Gmail: docs/gmail.md — folder name mapping and OAuth2 setup
- Proton Mail Bridge: docs/proton-bridge.md — non-standard port configuration
On first launch, auto-screening is paused because your screener lists are empty — neomd won't move anything until you've classified your first sender. Your Inbox loads normally so you can explore.
By default, neomd loads and auto-screens only the newest 200 Inbox emails ([ui].inbox_count). This keeps startup predictable. If you want to re-screen the entire Inbox on the IMAP server, run :screen-all inside neomd; that scans every Inbox email, not just the loaded subset, and can take a while on large mailboxes.
Getting started with the screener:
- From your Inbox, pick an email and press
I(screen in) to approve the sender, orO(screen out) to block them. This creates your first screener list entry. - Once you've classified at least one sender, auto-screening activates on every Inbox load — new emails from known senders are sorted automatically.
- Unknown senders land in the
ToScreentab. Jump there withgk(orTab, useLor click the tab) and classify them:Iscreen in — sender stays in Inbox foreverOscreen out — sender never reaches Inbox againFfeed — newsletters go to the Feed tabPpapertrail — receipts go to the PaperTrail tab
- Use
mto mark multiple emails, thenIto batch-approve them all at once. From theToScreenfolder, approving/blocking a single unmarked message now applies to all currently queued mail from that sender.
The best part: all classifications are saved permanently in your screener lists (screened_in.txt, screened_out.txt, etc.). An email address screened in will automatically go to your Inbox, and any email screened out will never be in your Inbox again.
You choose who can land in your Inbox. Bye-bye spam. This is the beauty of HEY-Screener, and neomd implements the same concept.
Tip
To disable auto-screening entirely, set auto_screen_on_load = false in [ui] config. Run :debug inside neomd if something isn't working.
Warning
:screen-all operates on the full Inbox mailbox on the server, not just the emails currently loaded in the UI. Use it when you intentionally want a mailbox-wide reclassification pass.
Find full Screener Workflow at docs/screener.md, classification tables, and bulk re-classification instructions.
Press ? inside neomd to open the interactive help overlay. Start typing to filter shortcuts.
See the full keybindings reference (auto-generated from internal/ui/keys.go via make docs).
Compose in Markdown, send as multipart/alternative (plain text + HTML). Attachments, CC/BCC, multiple From addresses, drafts, and pre-send review are all supported.
Discarding unsent mail now asks for confirmation in compose/pre-send, and :recover reopens the latest backup if you want to resume after an abort.
- See docs/sending.md for details on MIME structure, attachments, pre-send review, and drafts.
- See docs/reading.md for the reader: images, inline links, attachments, and navigation.
make build compile ./neomd
make run build and run
make install install to ~/.local/bin
make test run tests
make vet go vet
make fmt gofmt -w .
make tidy go mod tidy
make clean remove compiled binary
make help print this list
- Bubble Tea — TUI framework
- Bubbles — list, viewport, textinput components
- Glamour — Markdown → terminal rendering
- Lipgloss — styling
- go-imap/v2 — IMAP client (RFC 6851 MOVE)
- go-message — MIME parsing
- goldmark — Markdown → HTML for sending
- BurntSushi/toml — config parsing
See CHANGELOG.md for what's new.
neomd's responsiveness depends entirely on your IMAP server. Every folder switch, email open, and move requires IMAP round-trips (SELECT + UID SEARCH + FETCH). Here are real measurements from the same machine, same network:
Hostpoint (dedicated email provider) — folder switch: ~33ms total
| Operation | Time |
|---|---|
| SELECT | 12ms |
| UID SEARCH | 10ms |
| FETCH (200 emails) | 76ms |
| MOVE (1 email) | 46ms |
Gmail — folder switch: ~570ms total (17x slower than Hostpoint)
| Operation | Time |
|---|---|
| SELECT | 200ms |
| UID SEARCH | 180ms |
| FETCH (2 emails) | 190ms |
| MOVE (1 email) | 339ms |
Outlook/Office365 (with OAuth2 authentication and different network - not really comparable, but gives a indication) — folder switch: ~269ms total (8x slower than Hostpoint)
| Operation | Time |
|---|---|
| SELECT | 45ms |
| UID SEARCH | 22ms |
| FETCH (10 emails) | 180ms |
| MOVE (1 email) | 21ms |
Interestingly, Gmail benchmarks fast on a fresh single connection (scripts/imap-benchmark.sh shows ~70ms total, same as Hostpoint). But on a sustained session with sequential commands — which is how neomd actually uses IMAP — Gmail adds ~180ms latency per command. This is likely Gmail's internal label-to-folder translation and session management overhead. The result: every action in neomd feels much slower on Gmail, while Hostpoint stays instant.
Note
Gmail is not recommended. If you're on Gmail, consider a dedicated email provider (Hostpoint, Fastmail, HEY, Migadu, etc.) for the best neomd experience. Or use Gmail just for fun :). See docs/gmail.md for Gmail-specific folder configuration.
Test your own provider:
# With password
IMAP_HOST=imap.example.com IMAP_USER=me@example.com IMAP_PASS=secret ./scripts/imap-benchmark.sh
# With OAuth2 (reads token from neomd config)
CONFIG=~/.config/neomd/config.toml IMAP_USER=me@gmail.com ./scripts/imap-benchmark.shSee SECURITY.md for how credentials, screener lists, temp files, and network connections are handled — with links to the relevant source files.
- Neomutt — the gold standard terminal email client; neomd reuses its screener list format and borrows keybindings (though most are custom made and what I use). I implemented the HEY screener for Neomutt, see note for more information.
- HEY — the Screener concept: unknown senders wait for a decision before reaching your inbox
- hey-cli — a Go CLI for HEY; provided the bubbletea patterns used here
- newsboat — RSS reader whose
Oopen-in-browser binding and vim navigation feel inspired neomd's reader view - emailmd.dev — the idea that email should be written in Markdown when seen on HN
- charmbracelet/pop — minimal Go email sender from Charm
- charmbracelet/glamour — Markdown rendering in the terminal
- kanagawa.nvim — the color palette used for the inbox
- msgvault — Go IMAP archiver; the IMAP client code in neomd is adapted from it
This TUI is mostly vibe-coded in the sense that all code is written with Claude Code, but guided by very detailed instructions to make the workflow as I use it and like it to be.
I used my experience with Neomutt, TUIs, and the GTD workflow for handling emails with HEY Screener, and added some (hopefully) taste using my favorite tools and aesthetics. Find the full history at Twitter - inspired by seeing Email.md on HackerNews.
If you rather read the prompt, check out my initial prompt and its generated plan - which I have iterated and added features by the 100s since then.
See at my second brain at Roadmap.


