fix: fallback uses mbsync --pull to avoid re-uploading mbrts-delivered mail
Also documents the flags/deletions-only scheduled-mbsync approach using command-line operators instead of a restricted Sync directive.
This commit is contained in:
parent
c7c341264f
commit
f11a0926e5
2 changed files with 76 additions and 58 deletions
128
README.md
128
README.md
|
|
@ -1,51 +1,16 @@
|
||||||
# mbrts
|
# mbrts (mailbox real-time sync)
|
||||||
|
|
||||||
Real-time mail delivery for maildirs via Fastmail's JMAP push API.
|
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
|
## Setup
|
||||||
|
|
||||||
Create a Fastmail API token at **Settings → Security → API Tokens**.
|
### Nix / home-manager
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
Add mbrts as a flake input:
|
Add mbrts as a flake input:
|
||||||
|
|
||||||
|
|
@ -56,21 +21,11 @@ inputs.mbrts = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
The flake exposes:
|
Add the overlay and module, then configure the service:
|
||||||
|
|
||||||
| 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:
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
# in your flake outputs
|
# in your flake overlays
|
||||||
overlays = [ inputs.mbrts.overlays.default ];
|
inputs.mbrts.overlays.default
|
||||||
|
|
||||||
# in your home-manager module list
|
# in your home-manager module list
|
||||||
inputs.mbrts.homeManagerModules.default
|
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 |
|
| Option | Type | Default | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `enable` | bool | — | Enable the service |
|
| `enable` | bool | — | Enable the service |
|
||||||
| `package` | package | `pkgs.mbrts` | Package to use |
|
| `package` | package | `pkgs.mbrts` | Package to use |
|
||||||
| `logLevel` | enum | `"info"` | `debug`, `info`, `warn`, or `error` |
|
| `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 .
|
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
|
## aerc
|
||||||
|
|
||||||
Configure the account source as `maildir://` so aerc picks up delivered messages immediately via inotify:
|
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
|
```ini
|
||||||
source = maildir:///home/you/.mail/Personal
|
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.
|
||||||
|
|
|
||||||
6
jmap.go
6
jmap.go
|
|
@ -411,10 +411,14 @@ func buildAuth(a AccountConfig) (string, error) {
|
||||||
return "Bearer " + token, nil
|
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) {
|
func runMbsync(a AccountConfig) {
|
||||||
ch := a.mbsyncChannel()
|
ch := a.mbsyncChannel()
|
||||||
slog.Info("running mbsync", "account", a.Name, "channel", ch)
|
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))
|
slog.Error("mbsync failed", "channel", ch, "err", err, "output", string(out))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue