Your mailing lists,
actually manageable
Self-hosted SMTP receiver + AI triage + Matrix notifications. Urgent threads surface immediately; everything else lands in a daily digest.
Services
mail-receiver
Postfix accepts mail for one address, rejects everything else. Writes Maildir files.
digestor
Watches Maildir with inotify. Runs ollama triage, then Claude for urgent items. Stores everything in SQLite.
notifier
Matrix bot. Delivers urgent alerts immediately and daily digests on schedule. Accepts !cancel commands.
ollama
Runs gemma3:4b locally. Fast first-pass filter — only flagged messages go to Claude.
Quick start
Clone and copy templates
git clone https://github.com/jmq/mailing-list-digestor
cd mailing-list-digestor
cp .env.example .env
cp config/context.md.example config/context.md
cp config/lists.yaml.example config/lists.yaml
Fill in .env
# Required
SMTP_RECIPIENT=digest@yourdomain.example
SMTP_HOSTNAME=mail.yourdomain.example
ANTHROPIC_API_KEY=sk-ant-...
# Matrix
MATRIX_HOMESERVER=https://matrix.example.com
MATRIX_USERNAME=@digestbot:example.com
MATRIX_PASSWORD=your-bot-password
MATRIX_WHITELIST=@you:example.com
MATRIX_ROOM_ID=!roomid:example.com
Describe your interests in config/context.md
Plain markdown. Tell the digestor which working groups you follow, what counts as urgent, and what to ignore. Re-read on every processing cycle — edit any time without restarting.
Map lists to working groups in config/lists.yaml
working_groups:
QUIC:
- quic@ietf.org
TLS:
- tls@ietf.org
Daily digests are grouped by these names.
Start everything
podman-compose up -d
On first start, the ollama container pulls gemma3:4b — this takes a few minutes. After that, subscribe $SMTP_RECIPIENT to your mailing lists. Subscription confirmations arrive as Matrix notifications.
Configuration reference
Environment variables
| Variable | Default | Description |
|---|---|---|
| SMTP_RECIPIENT | — | Address to accept mail for |
| SMTP_HOSTNAME | mail-receiver | Postfix hostname / SMTP banner |
| SMTP_PORT | 25 | Host port mapped to container port 25 |
| ANTHROPIC_API_KEY | — | Anthropic API key |
| OLLAMA_MODEL | gemma3:4b | Local triage model name |
| DIGEST_TIME | 07:00 | Daily digest time (HH:MM, container TZ) |
| MATRIX_HOMESERVER | — | Matrix homeserver URL |
| MATRIX_USERNAME | — | Bot Matrix ID |
| MATRIX_PASSWORD | — | Bot password |
| MATRIX_WHITELIST | — | Comma-separated Matrix IDs for the bot |
| MATRIX_ROOM_ID | — | Room to post notifications to |
| POLL_INTERVAL_S | 30 | Notifier poll interval in seconds |
Matrix bot commands
| Command | Effect |
|---|---|
!track <interval> <url> [for <description>] | Track a URL; intervals: hourly, daily, weekly, Nh, Nd, Nw |
!untrack <id> | Stop tracking a URL by its ID (shown in !list) |
!list | List active trackings and pending notification count |
| !cancel <id> | Cancel a specific repeating notification |
| !cancel-all | Cancel all active repeating notifications |
| anything else | Stored as a reply; digestor updates context.md next cycle |
The tracker always renders pages with headless Chromium and only calls Claude when the text content changes.
Deployment
Requirements
A Linux host with:
- Podman 5+ (rootless) with cgroups v2 mounted
- podman-compose
- fuse-overlayfs (for overlay storage driver)
- Port 25 reachable from the internet, or an MX forwarder
sudo rc-update add cgroups default && sudo rc-service cgroups start once.
systemd user service
A unit file is included at ~/.config/systemd/user/mailing-list-digestor.service:
systemctl --user daemon-reload
systemctl --user enable --now mailing-list-digestor
loginctl enable-linger # start at boot without a session
Health check
podman exec $(podman ps -qf name=digestor) \
curl -s http://localhost:8080/healthz
Returns {"status": "ok", "db": "ok"}.
Smoke test
Tests the mail delivery path without any AI credentials:
SMTP_RECIPIENT=smoketest@mail-receiver.test ./smoke_test.sh