Local-first AI email triage for IMAP inboxes.
mAIl classifies incoming mail, routes it into operational folders, keeps an auditable workflow state in SQLite, and uses a local LLM only when deterministic rules are not enough. It is designed for private deployments where data residency, operational safety, and predictable routing matter more than raw demo speed.
Most inbox automation tools optimize for happy-path classification and ignore the hard parts:
- mailbox state drift
- IMAP safety during
copy -> delete - replay / deduplication
- partial failures after successful copy
- local-only processing and privacy
- auditability for every routing decision
mAIl was built to handle those constraints explicitly.
- reads mail from worker-owned IMAP source folders such as
INBOX.AI-Review - applies deterministic rules first for billing, complaints, system mail, spam, newsletters, offers, and known operational patterns
- sends only ambiguous messages to a local Ollama model
- converts rule or LLM output into deterministic workflow actions
- routes mail to target folders like
INBOX.Billing,Junk,INBOX.Newsletter,INBOX.Offer,INBOX.Other,INBOX.Appointments, orINBOX.System - persists mailbox-safe workflow state and leases in SQLite
- writes JSONL audit logs for review, reporting, and Grafana / Prometheus metrics
Current folder policy:
spam -> Junknewsletter -> INBOX.Newsletteroffer -> INBOX.Offerother -> INBOX.Otherparse_error -> INBOX.AI-Uncertain
Current confidence policy:
- high-signal categories use
MOVE_CONFIDENCE_THRESHOLD(default0.75) otheruses a lowerOTHER_MOVE_CONFIDENCE_THRESHOLD(default0.50)- this keeps genuinely low-signal mail out of
INBOX.AI-Uncertainwhile preserving stricter routing for business-sensitive categories
flowchart LR
A[IMAP Mailboxes] --> B[Worker Source Folder\nINBOX.AI-Review]
B --> C[Python Worker]
C --> D[Email Parser\nnormalize body, attachments, fingerprint]
D --> E[Rule Engine]
E -->|deterministic match| F[Decision Engine]
E -->|needs semantic classification| G[LLM Gateway\nOllama]
G --> F
F --> H[IMAP Router\ncopy -> delete_message]
F --> I[Draft Store]
C --> J[SQLite State\nleases, idempotency, cleanup state]
C --> K[JSONL Audit Log]
J --> L[Health / Status / Reports]
K --> L
L --> M[Prometheus Metrics]
M --> N[Grafana]
launchdstarts the worker on a schedule.- The worker loads global settings and the mailbox manifest.
- A global runtime lock is acquired.
- Messages are fetched from the configured source folder.
- Each message is parsed, normalized, and fingerprinted.
- SQLite leases ensure idempotent processing and safe retries.
- Deterministic rules handle obvious cases first.
- Only unresolved messages go to Ollama for semantic classification.
- The decision engine maps semantics to folders, flags, or drafts.
- The message is routed through IMAP and every state transition is audited.
The LLM runs through Ollama on the host machine. This keeps message content local and removes dependency on external inference APIs for day-to-day classification.
The system does not ask the model to solve everything. It protects common operational classes with deterministic rules first, which improves speed, cost, and predictability.
mAIl treats IMAP as a failure-prone integration point. It validates folder access, prefers UID EXPUNGE when possible, tracks UIDVALIDITY, and keeps explicit cleanup_pending state when copy succeeded but source deletion did not.
SQLite is used for:
- leases
- idempotency
- retry control
- cleanup recovery
- mailbox-scoped uniqueness
This avoids duplicate work and allows safe replay after partial failures.
Each routing action is written to JSONL audit logs, which then feed:
- health checks
- quality reports
- manual review reports
- Prometheus metrics
| Component | Responsibility |
|---|---|
config.py |
Settings, mailbox manifest loading, secret resolution |
email_parser.py |
RFC822 parsing, body normalization, attachment metadata, fingerprint inputs |
rule_engine.py |
Fast deterministic routing for known categories |
llm_gateway.py |
Local semantic classification through Ollama with schema validation |
decision_engine.py |
Final workflow action and folder mapping |
imap_client.py |
Safe IMAP fetch/copy/delete/retry behavior |
state_manager.py |
SQLite workflow state, leases, cleanup tracking |
audit_logger.py |
Append-only JSONL audit trail |
metrics_exporter.py / metrics_bridge.py |
Prometheus-compatible runtime and quality metrics |
historical_backfill_cli.py |
Safe backlog staging through the standard worker flow |
admin_mailbox_cli.py |
Narrow operational remediation for exact IMAP/admin actions |
quality_learning_cli.py |
Read-only quality-learning report with proposed rule_engine / prompt / parser changes |
The public repository is prepared for safe sharing:
- real
.envfiles are ignored - real mailbox manifests are ignored
- runtime data, logs, drafts, and generated output are ignored
- only example configuration is kept in git
Runtime security model:
- prefer
imap_pass_refover plaintextimap_pass - supported secret references:
env:VAR_NAMEkeychain:service/accountkeychain:service:account
- audit and state can redact direct PII fields
- runtime files are written with owner-only permissions where possible
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e .Create .env from .env.example and fill in:
IMAP_HOSTIMAP_USERIMAP_PASS
Run:
.venv/bin/python -m mail_ai_agent.cli --jsonStart from:
Prepare local files:
cp .env.multi.test.example .env.multi.test
cp config/mailboxes.example.json config/mailboxes.local.jsonThen point MAILBOXES_CONFIG_PATH to that manifest and run:
.venv/bin/python -m mail_ai_agent.cli --env-file .env.multi.test --jsonPreflight topology before enabling the worker:
.venv/bin/python -m mail_ai_agent.preflight_cli --env-file .env.multi.testProduction assumes:
INBOX.AI-Reviewis worker-owned- target folders already exist
Junk,INBOX.Newsletter, andINBOX.Offerare provisioned before enabling production routing- IMAP routing uses
copy -> delete_message - metrics are exposed to Prometheus and Grafana
- worker scheduling is handled by
launchd
Useful commands:
.venv/bin/python -m mail_ai_agent.status_cli --state-db data/state.sqlite --audit-log logs/audit.jsonl
.venv/bin/python -m mail_ai_agent.report_cli --state-db data/state.sqlite --audit-log logs/audit.jsonl
.venv/bin/python -m mail_ai_agent.preflight_cli --env-file .env.multi.test
.venv/bin/python -m mail_ai_agent.cleanup_cli --applySafe reprocess of current uncertain backlog without deleting originals:
.venv/bin/python -m mail_ai_agent.historical_backfill_cli \
--env-file .env.multi.prod \
--apply \
--keep-source \
--force-reprocess \
--folders INBOX.AI-UncertainRead-only quality-learning report:
.venv/bin/python -m mail_ai_agent.quality_learning_cli \
--state-db data/multi-prod-state.sqlite \
--audit-log logs/multi-prod-audit.jsonl \
--output-dir logs/quality-learningReview or apply a generated rule_engine.py patch:
.venv/bin/python -m mail_ai_agent.apply_quality_patch_cli \
--patch logs/quality-learning/quality-learning-YYYYMMDDTHHMMSSZ-proposal-1.patch \
--check.venv/bin/python -m mail_ai_agent.apply_quality_patch_cli \
--patch logs/quality-learning/quality-learning-YYYYMMDDTHHMMSSZ-proposal-1.patch \
--applyHistorical audit cleanup after category model changes:
.venv/bin/python -m mail_ai_agent.migrate_audit_log_cli \
--audit-log logs/multi-prod-audit.jsonlApply with automatic backup:
.venv/bin/python -m mail_ai_agent.migrate_audit_log_cli \
--audit-log logs/multi-prod-audit.jsonl \
--applyHistorical backlog staging:
.venv/bin/python -m mail_ai_agent.historical_backfill_cli \
--env-file .env.multi.prod \
--export-csv output/historical-backfill-plan.csvTargeted remediation:
.venv/bin/python -m mail_ai_agent.admin_mailbox_cli \
--env-file .env.multi.prod \
requeue-uncertainThe project exposes Prometheus-style metrics and is designed to integrate with Grafana.
Example checks:
bash scripts/prod_metrics.sh
curl -sS http://127.0.0.1:9177/metrics | sed -n '1,80p'
curl -sS 'http://127.0.0.1:9090/api/v1/query?query=mailai_health_ok'Active documentation:
- docs/architecture.md
- docs/launchd-setup.md
- docs/multi-mailbox-operations.md
- docs/recovery-runbook.md
- docs/quality-review-checklist.md
Historical reference:
Primary local test runner (recommended for this repo):
uv run python -m pytest tests/Equivalent shortcut:
make testRun the live Ollama integration test:
RUN_LIVE_OLLAMA_TESTS=1 .venv/bin/pytest tests/integration/test_ollama_live.py -qRun the live IMAP integration test:
RUN_LIVE_IMAP_TESTS=1 \
LIVE_IMAP_HOST=mail.example.com \
LIVE_IMAP_USER=user@example.com \
LIVE_IMAP_PASS=change-me \
LIVE_IMAP_SOURCE_FOLDER=INBOX.Test-AI-Review \
.venv/bin/pytest tests/integration/test_imap_live.py -qDo not point live IMAP tests at a production source folder.
Discovery/review report for AI-generated branches and open PR heads:
make ai-branch-sweepDelete only patch-duplicate AI branches that have no open PR:
make ai-branch-sweep-cleanThe runtime and operational hardening pass is complete. The next recommended stage is quality iteration:
- promote repeated safe LLM decisions into deterministic rules
- expand the golden set with anonymized production-like examples
- monitor category quality and model drift
- keep operational changes auditable and reversible