diff --git a/README.md b/README.md index 2cf96d4..3dacba7 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,16 @@ -# mbrts +# 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. +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. -## How it works +## Prerequisites -On first run, or when the saved JMAP state is stale, mbrts falls back to `mbsync` for a full sync, then takes over from that point. +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 -Create a Fastmail API token at **Settings → Security → API Tokens**. - -### Config file - -By default mbrts reads `$XDG_CONFIG_HOME/mbrts/config.yaml` (e.g. `~/.config/mbrts/config.yaml`): - -```yaml -accounts: - - name: Personal - maildir_path: /home/you/.mail/Personal - password_command: "pass show fastmail/api-token" -``` - -Each account requires exactly one token source: - -| Field | Description | -|---|---| -| `password_command` | Shell command that prints the token to stdout | -| `token_file` | Path to a file containing the token | -| `token_env` | Name of an environment variable containing the token | - -Optional fields: `mbsync_account` (mbsync channel/group name; defaults to `name`), `log_level` (top-level: `debug`, `info`, `warn`, `error`). - -### CLI flags - -Every config file option has an equivalent flag. Flags override the config file. A single account can be configured entirely on the command line without a config file: - -``` -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. - -### Nix +### Nix / home-manager Add mbrts as a flake input: @@ -56,21 +21,11 @@ inputs.mbrts = { }; ``` -The flake exposes: - -| Output | Description | -|---|---| -| `packages.*.default` | The `mbrts` binary | -| `overlays.default` | Adds `pkgs.mbrts` | -| `homeManagerModules.default` | The `services.mbrts` home-manager module | - -### home-manager module - -Add the overlay and module to your home-manager configuration, then configure the service: +Add the overlay and module, then configure the service: ```nix -# in your flake outputs -overlays = [ inputs.mbrts.overlays.default ]; +# in your flake overlays +inputs.mbrts.overlays.default # in your home-manager module list inputs.mbrts.homeManagerModules.default @@ -88,23 +43,55 @@ services.mbrts = { }; ``` -The module generates a config file and runs mbrts as a `systemd` user service. Logs are available via `journalctl --user -u mbrts -f`. +The module generates a config file and runs mbrts as a systemd user service. Logs: `journalctl --user -u mbrts -f`. -Module options: +**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 config file fields above) | +| `accounts` | list | `[]` | List of account submodules (see fields below) | -## Build +**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`): + +```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: @@ -112,3 +99,30 @@ Configure the account source as `maildir://` so aerc picks up delivered messages ```ini 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: + +```nix +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. diff --git a/jmap.go b/jmap.go index e458c0a..deb2b88 100644 --- a/jmap.go +++ b/jmap.go @@ -411,10 +411,14 @@ func buildAuth(a AccountConfig) (string, error) { return "Bearer " + token, nil } +// runMbsync backfills messages that arrived while mbrts was not running. It +// pulls only: mbrts delivers to the Maildir out of band, so a full sync would +// see those messages as new local mail and push them back up to the server as +// duplicates. --pull propagates new messages far-to-near without any push. func runMbsync(a AccountConfig) { ch := a.mbsyncChannel() slog.Info("running mbsync", "account", a.Name, "channel", ch) - if out, err := exec.Command("mbsync", ch).CombinedOutput(); err != nil { + if out, err := exec.Command("mbsync", "--pull", ch).CombinedOutput(); err != nil { slog.Error("mbsync failed", "channel", ch, "err", err, "output", string(out)) } }