No description
  • Go 72.5%
  • HTML 12.6%
  • CSS 8.2%
  • JavaScript 5%
  • Shell 1.3%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-24 19:46:50 +02:00
cmd/mailfilter Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
deploy/tvbox Prompt for IMAP hostname during tvbox install 2026-09-24 19:46:50 +02:00
internal Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
third_party Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
.dockerignore Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
.gitignore Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
config.example.yaml Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
docker-compose.yml Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
Dockerfile Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
go.mod Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
go.sum Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00
README.md Initial Mailfilter implementation 2026-09-24 19:32:04 +02:00

Mailfilter

A self-hosted Go application that browses one IMAP account and moves incoming messages using this fixed order:

  1. The first matching YAML rule.
  2. An optional OpenAI-compatible classifier.
  3. The configured TODO folder when AI is disabled or fails.

It never automatically deletes email. IMAP servers must support the MOVE capability. This is deliberate: the IMAP library's generic move fallback uses COPY plus EXPUNGE, which could expunge unrelated messages flagged for deletion.

Quick start

Requires Go 1.26 or Docker, an IMAP account with IMAP over TLS on port 993, and an HTTPS reverse proxy for browser access.

mkdir -p data
cp config.example.yaml data/config.yaml
# Edit data/config.yaml: set imap.host and review folders, rules, AI and scheduling.
chmod 600 data/config.yaml
GOCACHE=/tmp/imapper-gocache go build -buildvcs=false -o mailfilter ./cmd/mailfilter
./mailfilter -config data/config.yaml -data data config validate
./mailfilter -config data/config.yaml -data data serve

The default paths are /data/config.yaml and /data. Set MAILFILTER_CONFIG and MAILFILTER_DATA or pass -config and -data before the command. The application creates mailfilter.db, process.lock, and the two encryption keys in data/secrets/ on first start. Keep the entire data directory writable by the service user, private, and persistent. The key files are created with mode 0600. Never put data/ into version control.

Open the HTTPS URL provided by your reverse proxy. Sign in with your IMAP username and password. A successful IMAP login synchronizes server folders into config.yaml, preserving existing descriptions and adding new folders with empty descriptions. It creates configured folders that are missing on the server, including TODO.

Docker Compose

mkdir -p data
cp config.example.yaml data/config.yaml
# Configure the file. The image runs as UID/GID 1000; make data/ writable by it.
docker compose up --build -d

The Compose example binds the app only to 127.0.0.1:8080. Put an HTTPS reverse proxy in front of it. Set server.trusted_proxies to the proxy's address as seen inside the application container; a host-based proxy usually appears as the Docker bridge gateway, not 127.0.0.1. For a proxy in another container, connect both containers to a private Docker network and allowlist that proxy's address. Only explicitly trusted proxies may supply X-Forwarded-Proto: https. The login page and authenticated routes reject plain HTTP.

Example Caddy configuration when Caddy runs on the same host:

mail.example.com {
    reverse_proxy 127.0.0.1:8080
}

Set the hostname and configure Caddy's TLS as appropriate. Do not expose the Go HTTP listener directly to the internet. See Caddy's reverse proxy documentation for proxy options.

Login and credential lifetime

The login form fetches the server's RSA-OAEP-4096 public key and encrypts the login JSON in the browser with SHA-256 and the imapper-login-v1 label. HTTPS is still required. The private key stays in data/secrets/login_private_key.

Only a successful IMAP authentication creates a session. A new login replaces the previously stored account and its sessions, since the application manages one account at a time. The browser receives a random opaque __Host-mailfilter_session cookie with Secure, HttpOnly, SameSite=Strict, Path=/, and a seven-day maximum age. SQLite holds only the SHA-256 hash of that token. The IMAP credentials and username are encrypted with AES-256-GCM using a separate data/secrets/credential_storage_key. Both the account and session expire after seven days; page views do not extend them. Startup and hourly cleanup delete expired credentials and sessions. Logout invalidates every session for the account and removes its encrypted credentials immediately. Mail history is retained.

Back up both mailfilter.db and secrets/ together. Without the storage key, saved credentials cannot be decrypted. Restoring an older backup can restore still-unexpired credentials, so protect backups and run mailfilter cleanup after restoring.

Folders and the TODO safety fallback

Exactly one configured folder must have todo: true. Existing IMAP folders are added to YAML after login with empty descriptions. Write folder descriptions yourself; they are provided to the AI model to explain destinations. The AI cannot select TODO or invent a new folder.

Create, rename, describe, mark as TODO, and delete folders from Settings. Deleting a folder moves its messages to TODO, verifies the source is empty, then deletes the IMAP folder. A failed move aborts deletion. The active TODO folder and INBOX cannot be deleted. Rules referring to a deleted folder must be disabled during deletion. Rename updates rule destinations and local history references.

The mailbox page lists the latest 100 messages per folder and supports IMAP text search across the folder, sorting the displayed results, multi-selection, moving, and read/unread changes. Message previews show plain text, sanitized HTML, and downloadable attachments. Remote images are blocked by default and can be loaded only by choosing Load remote images in HTML view.

YAML filter syntax

Rules are evaluated top to bottom; the first match wins and prevents any AI request. Matching is case-insensitive by default. Operators: equals, wildcard, contains, starts_with, ends_with. Each operator accepts one string or a list of strings. Set case_sensitive: true beside the operator when needed.

filters:
  - name: Supplier invoice
    enabled: true
    destination: Invoices
    match:
      all:
        - sender:
            wildcard: "*@supplier.example"
        - any:
            - subject: {contains: [invoice, receipt]}
            - headers:
                X-Billing-System: {equals: "yes"}

A condition has exactly one of sender, receiver, subject, body, headers, all, or any. all and any may nest. Unknown fields and operators produce errors with YAML line numbers. Invalid configuration stops automatic processing and leaves mail untouched. Settings provides visual rule forms with nested ALL/ANY groups, priority controls and drag reordering, plus a full YAML editor. Saves validate first, then write a temporary file, sync, and atomically rename it.

AI endpoint

The ai block accepts an OpenAI-compatible /v1 base URL, API key, model name, timeout, and concurrency. The application calls /chat/completions, passes from/to/CC/subject/body and eligible folder names plus descriptions, and requires a JSON answer such as {"folder":"Invoices"}. The folder name must match an eligible configured folder exactly. An HTTP error, timeout, malformed response, or unknown folder sends the message to TODO. Disable AI with ai.enabled: false; deterministic rules still work.

Do not expose a local AI service to the public internet. The AI service receives selected email content, so choose a backend consistent with your privacy needs. The Settings page includes an endpoint test.

Scheduling and CLI

Choose one processing mode in YAML:

processing:
  mode: interval  # or cron, or manual
  interval: "5m"
  cron: "*/5 * * * *"
  batch_size: 25
  ai_workers: 4

Interval uses Go durations such as 10s, 5m, or 1h. Cron uses standard five-field expressions in the server's local time zone. Docker Compose defaults to UTC; set TZ before starting Compose to choose another time zone. manual disables the internal scheduler. The Settings processing form or YAML editor can change the mode or frequency; saving refreshes the scheduler. A Check now button runs the same processing pipeline.

For external Linux cron:

*/5 * * * * /usr/local/bin/mailfilter -config /data/config.yaml -data /data process

For Docker cron:

*/5 * * * * docker exec mailfilter mailfilter process

The process command scans once and exits. A cross-process file lock prevents simultaneous runs from web, internal scheduler, and external cron. Processing tracks account, source folder, UIDVALIDITY and UID in SQLite to avoid classifying the same source message again. A pending database record is written before each IMAP move. mailfilter cleanup removes expired credentials and sessions; mailfilter config validate checks YAML without opening the database. SIGINT/SIGTERM stop the scheduler and gracefully shut down the HTTP server.

Operations

GET /healthz checks database availability. The dashboard shows scheduler state, today's counts and recent processing. The Diagnostics page tests IMAP and AI availability and shows the latest run and recent errors. The History page records destination, method, matching rule, status and time. Logs use Go structured logging and omit passwords, session tokens, API keys and message bodies. Changing server.listen requires a restart; other saved settings apply to subsequent requests and runs.

For backups, stop the container or use SQLite's online backup feature, then archive data/config.yaml, data/mailfilter.db, and data/secrets/. Preserve file ownership and permissions on restore. To upgrade, back up data/, rebuild the image or binary, and restart; schema setup runs on startup. Do not remove old encryption keys while sessions or stored credentials are active.

Troubleshooting:

  • HTTPS required: Check the reverse proxy's TLS and server.trusted_proxies CIDR.
  • IMAP authentication failed: Check the IMAP host, TLS port, username, password or app password, and account IMAP access.
  • Server lacks IMAP MOVE: Use an IMAP server with MOVE support. The application intentionally leaves messages untouched on servers without it.
  • Messages reach TODO: Check AI availability and model response in History, or add a deterministic rule.
  • Invalid YAML: Use mailfilter config validate or the Settings recovery editor. Processing stays off until fixed.
  • Expired credentials: Sign in again. The seven-day timer is fixed from the last successful login.

Tests

go test ./...

Tests cover strict nested YAML parsing, filter precedence and operators, session hashing and expiry, an in-process IMAP server, safe folder deletion ordering, AI response validation, storage tamper detection, processing overlap, and HTTP CSRF/logout behavior.