You want IMAP mail landing in suchi. This recipe stands up a small
Docker stack that:
- Pulls mail from your provider into a local Maildir (
mbsync).
- Fans every message into suchi’s ingest dir as
<sha>.eml.
- Suchi ingests it via fs-watch — parses the
.eml, fans out
attachments as child docs, populates correspondents from From:,
dedups by Message-Id.
Providers wired: Proton Mail (via Bridge, running as a sibling
container), Gmail, Fastmail, and a generic IMAPS/IMAP
escape hatch (iCloud, Yahoo, Zoho, Dovecot, etc.).
When to use this vs. built-in IMAP polling
Suchi already speaks IMAP directly via INGEST_IMAP_URL — see
config#email-ingest-imap-polling.
That’s fewer moving parts and enough for most operators.
Reach for the mbsync recipe when you want:
- A local Maildir mirror you can grep, back up, or hand off to
another tool independently of suchi.
- Proton Mail Bridge in the loop (suchi’s direct IMAP works against
Bridge too; the recipe just bundles Bridge into the same stack).
- Multi-folder / weird-provider support that suchi’s direct IMAP
doesn’t cover yet.
Boot it
The wizard asks for provider, credentials, folders, and port; it writes
config/.env (container-side vars) and .env (compose profiles + port
- your UID/GID) and points
templates/mbsyncrc.tmpl at the matching
provider template.
For Proton, do the interactive Bridge login first:
Then bring up the stack:
What each container does
- bridge (Proton only,
proton compose profile) — shenxn/protonmail-bridge
with libfido2 layered on for Bridge 3.25+. Listens on
protonmail-bridge:143 inside the compose network. State (encrypted
vault) lives in ./bridge-data/.
- mbsync — Debian slim + isync + gettext. Renders the mbsyncrc
template from env, polls every
SYNC_INTERVAL seconds (default 300),
writes Maildir to ./maildir/, then fans new files into ./ingest/
as <sha>.eml. Idempotent via a .fanned-marker timestamp file —
each cycle only touches Maildir entries newer than the last fan.
- suchi — built from the repo root Dockerfile (
slim target).
fs-watches /ingest, ingests every .eml, runs the full
post-ingest pipeline including attachment fanout.
Where things live on the host
Rotating credentials
Re-run ./setup.sh — it’s idempotent. Or edit config/.env directly
and docker compose restart mbsync.
Mail wizard (in-browser)
The server-rendered /admin/mail-setup wizard retired with the SPA
promotion; the equivalent in-app settings panel is tracked (task
#141 — needs GET/PUT /api/admin/settings/mail and a connection-
test endpoint). Until it lands, edit config/.env directly and
docker compose restart mbsync.
The compose recipe still ships the two mounts the future in-app
panel will use:
./config → /etc/suchi/mail-config (RW) so suchi can rewrite the
file. Directory mount, not a single-file bind — atomic rename
requires this.
/var/run/docker.sock so suchi can POST
/containers/suchi-mail-mbsync/restart to reload mbsync.
Mounting docker.sock grants suchi root-equivalent access to the host
Docker daemon. Suchi only uses it to restart the sibling mbsync
container, but the socket permits arbitrary daemon calls. Comment
out the mount + unset MAIL_SETUP_DOCKER_SOCK if you’d rather
docker compose restart mbsync yourself after a wizard save.
Smoke test
./smoke-test.sh drives the whole recipe end-to-end with the generic
provider + dummy creds and asserts fs-watch → post-ingest → attachment
fanout produces the expected doc counts. Run it before shipping any
change to the recipe. Needs Docker; takes ~90s; cleans up after itself.
Teardown
Gotchas
- First run pulls up to
MAIL_MAX_MESSAGES messages per folder.
Default is 200. Bump / lower in config/.env; mbsync keeps track
of what it’s pulled via .mbsyncstate files inside the Maildir,
so subsequent runs are incremental.
- Attachments over
MAIL_MAX_SIZE (default 25 MB) are silently
skipped by mbsync. If you’re waiting on a huge PDF that never
arrives, that’s the knob.
- Proton labels ↔ folders. Bridge maps Proton labels to IMAP
folders.
Patterns in the mbsyncrc is what you tell mbsync to
pull; add "Archive" or "Bills" etc. in MAIL_FOLDERS.
- Gmail’s
All Mail. Gmail exposes every message under
[Gmail]/All Mail. If you set MAIL_FOLDERS="*" you’ll get every
message twice (once from INBOX, once from All Mail). Prefer
MAIL_FOLDERS="INBOX \"[Gmail]/Sent Mail\"" — explicit is safer.