Also documents the flags/deletions-only scheduled-mbsync approach using command-line operators instead of a restricted Sync directive.
4.9 KiB
mbrts (mailbox real-time sync)
Real-time mail delivery for maildirs via Fastmail's JMAP push API.
Connects to Fastmail's JMAP EventSource stream and delivers new messages directly to a local Maildir the moment they arrive without polling. On first run, or when saved JMAP state is stale, mbrts falls back to mbsync --pull to backfill anything missed, then takes over from that point.
Prerequisites
Create a Fastmail API token at Settings → Security → API Tokens. This is distinct from an IMAP app password — mbrts requires a proper JMAP bearer token.
Setup
Nix / home-manager
Add mbrts as a flake input:
inputs.mbrts = {
url = "git+https://code.adriano.fyi/me/mbrts";
inputs.nixpkgs.follows = "nixpkgs";
};
Add the overlay and module, then configure the service:
# in your flake overlays
inputs.mbrts.overlays.default
# in your home-manager module list
inputs.mbrts.homeManagerModules.default
# in your home-manager config
services.mbrts = {
enable = true;
accounts = [
{
name = "Personal";
maildirPath = "/home/you/.mail/Personal";
passwordCommand = "pass show fastmail/api-token";
}
];
};
The module generates a config file and runs mbrts as a systemd user service. Logs: journalctl --user -u mbrts -f.
Module options:
| Option | Type | Default | Description |
|---|---|---|---|
enable |
bool | — | Enable the service |
package |
package | pkgs.mbrts |
Package to use |
logLevel |
enum | "info" |
debug, info, warn, or error |
accounts |
list | [] |
List of account submodules (see fields below) |
Account fields (each account requires exactly one token source):
| Field | Description |
|---|---|
name |
Account name; used for state file and default mbsync channel |
maildirPath |
Path to the local Maildir root |
passwordCommand |
Shell command that prints the bearer token to stdout |
tokenFile |
Path to a file containing the bearer token |
tokenEnv |
Environment variable containing the bearer token |
mbsyncAccount |
mbsync channel/group name for fallback (defaults to name) |
Standalone
Build the binary:
CGO_ENABLED=0 go build -o mbrts .
By default mbrts reads $XDG_CONFIG_HOME/mbrts/config.yaml (e.g. ~/.config/mbrts/config.yaml):
accounts:
- name: Personal
maildir_path: /home/you/.mail/Personal
password_command: "pass show fastmail/api-token"
Every config file option has an equivalent CLI flag, and flags override the config file. A single account can be configured entirely on the command line:
mbrts --name Personal \
--maildir-path ~/.mail/Personal \
--password-command "pass show fastmail/api-token"
Run mbrts --help for the full flag list, or mbrts --version to print the version.
aerc
Configure the account source as maildir:// so aerc picks up delivered messages immediately via inotify:
source = maildir:///home/you/.mail/Personal
Using mbrts alongside mbsync
mbrts handles inbound delivery; mbsync is still useful for pushing local changes (read flags, deletions, moves) back to Fastmail. They can coexist, but require careful configuration to avoid duplicate messages.
The problem is that mbsync tracks synced messages by IMAP UID in its own state database. It knows nothing about messages mbrts delivered directly to the Maildir. So a scheduled mbsync run that pulls new messages re-downloads mail mbrts already wrote (local duplicates), and a run that pushes new messages re-uploads that same mail to the server (remote duplicates).
The solution is to keep new-message propagation out of the scheduled run entirely, and constrain it to flag changes and deletions in both directions. mbsync's command-line operators override the channel's Sync directive per-invocation, so no separate config file is needed — run the scheduled sync as:
mbsync --gone --flags --all
--gone propagates deletions and --flags propagates flag changes (read/unread, etc.), each in both directions, with no New in either direction. Local reads and deletions flow up to Fastmail; webmail reads and deletions flow down to the Maildir; new mail is left entirely to mbrts.
mbrts's own fallback (mbsync --pull) covers the inbound side: on first run, or when its JMAP state is stale, it pulls any messages it missed. Point mbrts at the channel via mbsync_account (defaults to the account name); no manual initial sync is required.
With home-manager
The generated systemd service runs mbsync --all by default, which does a full sync. Override its ExecStart to use the flags/deletions-only operators:
systemd.user.services.mbsync.Service.ExecStart =
lib.mkForce "${pkgs.isync}/bin/mbsync --gone --flags --all --verbose";
Leave the channel Sync directive at its default (Full) — both the scheduled service and mbrts's fallback override it on the command line, so it is never used as configured.