Mach

A fast Gmail and Google Calendar client for macOS and Linux.

I got tired of waiting on Gmail, so I wrote my own. All my accounts in one list, every key where Gmail put it. There's an agent in it too, and it can archive and reply rather than just tell me what's in there. Gmail and Google Calendar only — no IMAP, no other providers.

macOS and Linux · Build from source · MIT · github.com/bborn/mach

Five accounts in one inbox

Everything in date order. None of it's real mail, though. That's the test suite's fixture data, and the status bar says so.

The Mach mail window: Mail and Calendar in the title bar, an account rail on the left, a dense unified thread list in the middle, and an open conversation in the reading pane on the right.
What it does

Everything you look at comes out of a SQLite database on your own disk. Opening a thread doesn't ask Google for anything, so there's no spinner, and it works on a plane. Sync runs behind that and writes into the same database.

The long version
Local data
Twelve months backfilled per account, then incremental history sync from a stored watermark. The backfill runs in the background, so you can read mail while it's still filling in behind you — which is just as well, because Gmail's quota caps it at five messages a second and a big mailbox takes over an hour.
Search
Gmail's operator syntax, parsed as you type and compiled to SQL against an FTS5 index. from:, has:attachment, older_than:2m, quoted phrases, OR and - all work. Mine is around 61,000 messages and I've never waited on it.
Keyboard
Gmail's own, so there's nothing to learn twice. J K to move, E to archive, B to snooze, C to write, R to reply, G I for the inbox. ⌘K for search and commands, ? for the full list. ⌘Z undoes over a stack that does not expire.
Several accounts
One list, in date order, with a colour bar on each row telling you whose it is. Replies go out from the account the mail came in on, so you can't answer your mother from work by accident. A new message has a From row to pick with; it starts on whichever account the list is filtered to.
Calendar
Day, week and month, and you can drag to make an event or drag it somewhere else. Open one and you get the guests and who's actually coming, the recurrence rule, the reminders, and the Meet link.
Writing
Drafts save themselves and go up to Gmail, so one started here can be finished on your phone. Several can be open at once, as tabs along the bottom, and any of them can be popped out into its own window. The body is plain text with a bit of markdown in it, sent as HTML. A send sits in the outbox for ten seconds before it leaves — longer or shorter in preferences — and ⌃S schedules it for three hours' time, tomorrow morning or Monday instead.
Attachments
Drag a file onto a composer to send it, or press ⇧⌘A. Incoming ones are fetched, cached, opened and saved, with inline images drawn in the message. What kind of file it is comes from the bytes themselves, not from what the sender claimed, and executables don't open.
Notifications
New mail gets a notification and a Dock badge, but only if it has earned one: unread, in the inbox, not from you, and either Gmail filed it as personal or it's a thread you've already written into. Newsletters stay quiet.
The agent
Every action in Mach is a typed command in Rust, and the agent gets that same list and nothing else. There's no second path to Google. It runs on the Claude Code CLI if you have one, so it spends the subscription you're already paying for and never asks for an API key. Anything that actually leaves the building stops and waits for you to say yes. Everything else is undoable and shows up in the same log as the things you did yourself. ⌘K will also hand a thread off to an agent running somewhere else.
From a shell
mach is the same list of commands the agent gets, from a terminal. The verbs aren't written down anywhere in the CLI — it reads them off the app at runtime, so it can't fall behind what the app can do. mach search invoice and the other reads open the SQLite file directly and work whether or not Mach is running. Anything that writes goes over a loopback socket to the running app, which is the only process holding the tokens, the outbox and the undo stack; with the app shut, a write fails and says so rather than queueing.
Unsubscribe
⇧⌘U on a newsletter archives it and unsubscribes in the background. Most bulk senders support one-click now, so that's a single request and no page; the rest get the unsubscribe email sent for them. If all a sender offers is a link to a form, you get a button that opens it. Mach won't click through a page it hasn't read, and I'd rather hand you the link than send a request that only might have worked.

It doesn't offer on everything that carries an unsubscribe link, either. Unsubscribing tells the sender your address is live and somebody reads it. Fine for a newsletter you asked for. For spam it's the most useful thing you could hand them, so mail that looks like spam gets offered the spam report instead.
Copy it for a chatbot
⌥⌘C puts whatever's on screen on the clipboard as plain text — the whole thread, quotes stripped, ready to paste into whatever you're asking. Or the week, or a search and its results. ⇧⌥⌘C takes just the one message you're reading, which is usually what I actually wanted.
Plugins
Somebody else's code runs in a sandbox with no network, no files and none of your Google credentials. All a plugin can do is send the same commands your keyboard sends, and only the ones you let it, which is also why ⌘Z takes its work back.

Five calendars in one grid

Week view, with every event tinted by the calendar it came from and a red line on the current time. Fixture data again.

The Mach calendar in week view: five accounts' calendars overlaid in one grid, each event tinted by the calendar it belongs to, with a red line marking the current time.
The agent, and handoffs

Ask from ⌘K and the answer comes back in a drawer under the list, with every tool it called and what each one returned. I don't take its word for things either.

The agent shows its work

Here it drafted a reply, then stopped and waited for me to say send. The monospaced lines are the tools it called on the way.

The Mach window with the agent drawer open across the lower half: the question at its head, chips naming the conversation and mailbox it was given, four monospaced lines for the tools it ran, its answer, and a bar asking to approve sending the reply it drafted.

Or hand the thread somewhere else

The same conversation on its way to Claude Code in a terminal. The first time you send a thread somewhere new, you get to read the whole thing first: the command, the directory, and the text itself.

A panel over the dimmed Mach window headed 'Hand off to OfferLab': labelled rows for the command, the directory, the mode and the conversation, and under them the whole text that will be sent to the external agent.
What it doesn't do

I'm not planning to do any of it.

Install
  1. Prerequisites

    • macOS, Apple silicon or Intel — or Linux on Wayland, tested on Omarchy (Arch with Hyprland)
    • Rust stable. On macOS, the Xcode command line tools; on Linux, webkit2gtk-4.1, gtk3, libsoup3 and wl-clipboard
    • Bun
    • A Google account you can create a Cloud project with
    • For the agent, either the Claude Code CLI or an Anthropic API key. Mach uses the CLI when it finds one.
  2. Bring your own Google OAuth app

    This is the worst part, and there's no way around it: Mach ships with no credentials, so you register your own Google Cloud project and OAuth client. I'm not putting a personal mail client through Google's verification. Hand skills/mach-setup/SKILL.md to a coding agent that can drive a browser and it'll click through the console with you: the project, the two APIs, the consent screen, a Desktop app client, the seven scopes, publishing, and writing .env.local. It gives the keyboard back when it hits your password and the User Data Policy. If you'd rather do it by hand, the same file reads as a checklist.

    Google will warn you about the app. The first time you authorise an account you get "Google hasn't verified this app", which is true — it's your own unverified app. Click Advanced → Go to Mach (unsafe). Once per account.

    Set it to In production anyway, unverified. Leave it in Testing and Google throws away every refresh token after seven days, which means signing every account back in once a week, forever.

  3. Build and run

    git clone https://github.com/bborn/mach
    cd mach
    bun install
    bun run tauri dev

    Then ⌘KAdd account. If you got the credentials wrong it still boots and the window still paints — it just tells you it can't add an account.

    The command line is a second binary, mach-cli, since mach is the application. scripts/mach builds it if it's missing and runs it; symlink that onto your PATH as mach for the short name.

The state of it

This is my mail client. I built it for me and put it up here in case it's useful to anybody else. It isn't a product, there's no support, and there are no releases. I read my mail in it every day; the parts I don't use are probably broken.

Nothing is packaged, nothing updates itself, there's no onboarding past adding an account, and marketing mail still renders like a ransom note more often than I'd like. cargo test and bunx vitest run are the whole safety net — no end-to-end suite. ⌘KSend feedback grabs the window, lets you scribble on the screenshot, and drops the result in .feedback/inbox/ for a coding agent to pick up. That's how most of this got fixed.

I wrote down why it's built this way, at some length, in the design record and the README.