AI-powered email threat analysis daemon. Monitors your inbox via IMAP IDLE and runs every incoming message through a multi-stage enrichment and AI analysis pipeline — automatically passing, flagging, or moving suspicious mail to Junk.
Features
- Real-time monitoring via IMAP IDLE (push, no polling)
- Multi-stage pipeline: parse → whitelist check → enrich → AI → decide → act → audit
- Enrichment signals: SPF, DKIM, DMARC, DKIM alignment (cousin-domain
d=mismatch detection), DNSBL reputation, WHOIS domain age, display-name spoofing, passive URL expansion (shorteners only — no beaconing), URLhaus malware URL reputation, and VirusTotal URL & IP reputation (90+ vendors) - AI providers: OpenAI, Anthropic, or a local Ollama instance
- Three actions: pass (no change), flag (IMAP keyword), move to configured junk folder (default:
[Gmail]/Spam) - Aggressiveness presets: one-click sensitivity tuning (Conservative / Default / Aggressive / Very Aggressive) with fine-grained YAML override
- Whitelist: exempt trusted senders from analysis by email, domain, or subject pattern
- Web UI: Flask admin interface — dashboard, audit log, configuration, whitelist, credentials, manual test, documentation
- Audit log: full SQLite record of every decision including enrichment results (SPF, DKIM, DMARC, DNSBL, URLhaus, VirusTotal), AI signals, reasoning, and processing time — viewable per-message from the Audit Log page
- Backup & restore: export configuration (YAML only) or a full backup ZIP (YAML + credentials) from the web UI; restore via ZIP upload with a single click
Screenshots
Architecture
IMAP IDLE (main thread)
│
└─▶ ThreadPoolExecutor (worker threads)
│
├── message_parser — RFC 822 parsing, URL extraction
├── whitelist — bypass enrichment/AI for trusted senders
├── enrichment — SPF/DMARC/DKIM/DKIM alignment/DNSBL/WHOIS/URL expansion/URLhaus/VirusTotal
├── ai_analyzer — OpenAI / Anthropic / Ollama via httpx
├── decision_engine — threshold logic → PASS / FLAG / MOVE_TO_JUNK
├── imap_actions — set $VerdictMail-Suspect keyword or copy+delete
└── audit_logger — SQLite + rotating log file
Requirements
- Ubuntu 24.04 LTS (recommended) or 22.04 LTS, running as root for installation
- Python 3.11+ — Ubuntu 24.04 includes this by default; on 22.04 you may need to install it manually (
apt-get install python3.11 python3.11-venv) - An IMAP email account with IMAP access enabled. For Gmail: generate a Gmail App Password (Gmail Settings → See all settings → Forwarding and POP/IMAP → Enable IMAP, then Google Account → Security → App passwords). For other providers, see Other IMAP providers below.
- Port 80 free on the host (used by the web UI)
- One of:
- An OpenAI API key
- An Anthropic API key
- A running Ollama instance (local or remote) with a model pulled. 20B+ parameter models are recommended for reliable JSON schema adherence (e.g.
ollama pull gemma4:26b). Smaller models work but may occasionally produce malformed responses. Models with thinking/reasoning mode (Qwen3, DeepSeek-R1, etc.) are fully supported — thinking is automatically disabled for latency-sensitive pipeline use.
Installation
Quick install (recommended)
Download and review the install script, then run it as root:
curl -sSL https://raw.githubusercontent.com/ascarola/verdictmail/main/install.sh -o install.sh
less install.sh # review before running
sudo bash install.shThe script handles all steps below automatically and prompts interactively for credentials, AI provider, model, and timezone. It is safe to re-run if something goes wrong partway through.
Note: Requires Ubuntu 22.04 LTS or 24.04 LTS and Python 3.11+. Run as root or with
sudo.
Manual installation
If you prefer to install step by step, follow the instructions below.
1. Install system dependencies
apt-get update
apt-get install -y git python3 python3-venv python3-dev python3-pip \
build-essential libssl-dev sqlite32. Create the service user and directories
useradd -r -s /bin/false -M -d /opt/verdictmail verdictmail mkdir -p /opt/verdictmail /var/log/verdictmail chown verdictmail:verdictmail /opt/verdictmail /var/log/verdictmail
3. Clone the repository
git clone https://github.com/ascarola/verdictmail.git /opt/verdictmail chown -R verdictmail:verdictmail /opt/verdictmail
4. Create the virtual environment and install dependencies
python3 -m venv /opt/verdictmail/venv /opt/verdictmail/venv/bin/pip install --upgrade pip /opt/verdictmail/venv/bin/pip install -r /opt/verdictmail/requirements.txt chown -R verdictmail:verdictmail /opt/verdictmail/venv
5. Configure credentials
cp /opt/verdictmail/.env.example /opt/verdictmail/.env chown verdictmail:verdictmail /opt/verdictmail/.env chmod 600 /opt/verdictmail/.env
Edit /opt/verdictmail/.env and fill in your IMAP credentials and AI provider API key. Two optional threat intelligence keys can also be added: URLHAUS_API_KEY (free from abuse.ch) for malware URL lookups, and VIRUSTOTAL_API_KEY (free from virustotal.com) for URL and IP reputation checks against 90+ security vendors. Both are silently skipped if not set.
6. Configure the application
cp /opt/verdictmail/config/verdictmail.yaml.example /opt/verdictmail/config/verdictmail.yaml chown verdictmail:verdictmail /opt/verdictmail/config/verdictmail.yaml
Edit /opt/verdictmail/config/verdictmail.yaml and set at minimum:
ai.providerandai.modeltimezone(IANA name, e.g.America/New_York)
7. Install systemd units
cp /opt/verdictmail/systemd/verdictmail.service /etc/systemd/system/ cp /opt/verdictmail/systemd/verdictmail-web.service /etc/systemd/system/ systemctl daemon-reload
8. Install the sudoers rule (allows the web UI to restart the daemon)
cp /opt/verdictmail/systemd/verdictmail-sudoers /etc/sudoers.d/verdictmail chmod 440 /etc/sudoers.d/verdictmail
9. Enable and start
systemctl enable --now verdictmail verdictmail-web
systemctl status verdictmail verdictmail-web10. Verify installation via the web UI
Open a browser and navigate to:
On first visit, VerdictMail will prompt you to set a web UI password. This password protects all admin pages. The scrypt hash is stored in verdictmail.yaml — the plaintext is never saved.
Once logged in, verify the daemon is running on the Dashboard and use the Manual Test page to confirm the full pipeline is working before relying on it for live mail.
Configuration
All non-secret settings are in /opt/verdictmail/config/verdictmail.yaml.
See config/verdictmail.yaml.example for a fully-annotated template.
Changes require a daemon restart: systemctl restart verdictmail.
| Key | Default | Description |
|---|---|---|
ai.provider |
openai |
AI backend: openai, anthropic, or ollama |
ai.model |
gpt-4o-mini |
Model name passed to the provider |
ai.timeout_seconds |
120 |
Per-request AI timeout |
ai.ollama_base_url |
http://localhost:11434 |
Ollama base URL (ollama provider only) |
thresholds.flag |
0.55 |
Minimum confidence to flag medium/high threat. Set via the Aggressiveness preset or directly. |
thresholds.junk |
0.80 |
Minimum confidence to move high threat to Junk. Set via the Aggressiveness preset or directly. |
imap.host |
imap.gmail.com |
IMAP server |
imap.port |
993 |
IMAP SSL port |
imap.folder |
INBOX |
Folder to monitor |
imap.junk_folder |
[Gmail]/Spam |
Destination folder for MOVE_TO_JUNK actions (e.g. Junk on Fastmail/Outlook) |
worker_threads |
4 |
Concurrent message processors |
startup_scan_limit |
20 |
Max unread messages to process on startup |
whitelist.enabled |
true |
Master on/off for whitelist |
whitelist.rules |
[] |
List of whitelist rule objects |
timezone |
UTC |
IANA timezone for dashboard and audit log |
Other IMAP providers
VerdictMail is developed and tested against Gmail, but the underlying IMAP code uses only standard RFC-compliant operations (IMAP IDLE, COPY, DELETE, EXPUNGE) and should work with any IMAP server that supports IDLE.
Set IMAP_USERNAME and IMAP_PASSWORD in .env to your account credentials, then update the IMAP settings in verdictmail.yaml:
| Provider | imap.host |
imap.port |
imap.junk_folder |
|---|---|---|---|
| Gmail | imap.gmail.com |
993 |
[Gmail]/Spam |
| Fastmail | imap.fastmail.com |
993 |
Junk |
| Outlook / Hotmail | outlook.office365.com |
993 |
Junk |
| Apple iCloud | imap.mail.me.com |
993 |
Junk |
Note: Non-Gmail providers are not officially tested. If your provider requires an app-specific password or has two-factor authentication, generate a dedicated app password following your provider's documentation.
Actions
| Action | When | Effect |
|---|---|---|
pass |
Clean mail, low threat, or whitelisted | No IMAP changes |
flag |
Medium/high threat at sufficient confidence | Sets $VerdictMail-Suspect IMAP keyword; message stays in inbox |
move_to_junk |
High/critical threat at high confidence | Copies to the configured junk folder (imap.junk_folder, default [Gmail]/Spam), deletes original |
Note (Gmail): Gmail's web UI does not display custom IMAP keywords. The
$VerdictMail-Suspectflag is visible to standard IMAP clients and is always recorded in the audit log.
Whitelist
The whitelist bypasses enrichment and AI analysis for trusted senders. Rules are evaluated in order; the first match wins.
Each rule matches on one or more of:
sender— exact email address (case-insensitive)sender_domain— all addresses at a domainsubject_contains— case-insensitive substring of Subject
Multiple fields in one rule require all to match (AND logic). Manage rules via the web UI or by editing verdictmail.yaml directly (restart required).
Web UI
The Flask admin interface runs on port 80 alongside the daemon.
| Page | Path | Description |
|---|---|---|
| Dashboard | / |
Stats, threat chart, recent emails, service status; start/stop/restart daemon |
| Audit Log | /audit |
Paginated, searchable table with full-detail modal |
| Configuration | /config |
In-browser YAML editor + AI provider quick-config |
| Whitelist | /whitelist |
Add, edit, and delete whitelist rules |
| Credentials | /credentials |
IMAP credentials and API key management |
| Manual Test | /test |
Dry-run pipeline on a submitted email |
| Documentation | /docs |
In-app reference manual |
| Configuration (Backup) | /config/export |
Download verdictmail.yaml |
| Configuration (Full Backup) | /config/export/full |
Download ZIP of verdictmail.yaml + .env |
| About | /about |
Version and tech stack info |
A web UI password is set on first visit. The password hash is stored in verdictmail.yaml; the plaintext password is never stored.
Security note: The web UI runs on plain HTTP (port 80) with no TLS. Credentials and session cookies are transmitted in cleartext. This is acceptable on a trusted home or private LAN, but you should not expose port 80 directly to the internet. If remote access is needed, place the UI behind a reverse proxy with TLS (e.g. nginx + Let's Encrypt) or access it over a VPN.
Verification
Start / stop / restart the daemon from the web UI
The Dashboard provides Stop, Start/Resume, and Restart buttons. VerdictMail auto-detects its environment and chooses the appropriate control strategy:
| Environment | Strategy | Stop behaviour |
|---|---|---|
| Bare metal / privileged container | sudo systemctl |
Daemon fully stopped; unit marked inactive |
| Unprivileged LXC (e.g. Proxmox) | Signal + pause flag | Daemon stays running but skips incoming messages; emails remain UNSEEN until resumed |
Check Ollama connectivity (if using Ollama provider)
curl http://localhost:11434/api/tags
Test IMAP connectivity
python3 -c " import imapclient c = imapclient.IMAPClient('imap.example.com', ssl=True) # replace with your IMAP host c.login('YOUR_IMAP_USERNAME', 'YOUR_IMAP_PASSWORD') print(c.list_folders()) c.logout() "
Run the unit tests
# Install dev dependencies (includes pytest)
/opt/verdictmail/venv/bin/pip install -r /opt/verdictmail/requirements-dev.txt
PYTHONPATH=src /opt/verdictmail/venv/bin/python -m pytest tests/ -vThe test suite covers:
| File | What it tests |
|---|---|
test_message_parser.py |
RFC 2822 parsing, header extraction, URL extraction, DKIM signature parsing |
test_decision_engine.py |
All 7 decision rules, boundary conditions at both thresholds, threshold configuration variants, case insensitivity |
test_ai_analyzer.py |
JSON extraction from plain/fenced/embedded text, response schema validation, all _build_user_prompt sections and conditional blocks (URLhaus, VirusTotal, DKIM alignment, PBL note, body truncation) |
Watch live logs
journalctl -u verdictmail -f tail -f /var/log/verdictmail/verdictmail.log
Inspect the audit database
sqlite3 /var/log/verdictmail/verdictmail.db \ "SELECT id, subject, threat_level, printf('%.0f%%', confidence*100), action_taken, reasoning FROM audit_log ORDER BY id DESC LIMIT 10;"
Troubleshooting
| Symptom | Check |
|---|---|
| Service won't start | journalctl -u verdictmail -n 50 — look for config or credential errors |
| AI timeouts | Verify provider connectivity and ai.timeout_seconds |
| IMAP auth failure | Confirm credentials are correct (App Password for Gmail; provider-specific password for others) and that IMAP is enabled in your provider's settings |
| No messages processed | The daemon processes new/unseen messages; use the Manual Test page to verify the pipeline works |
| DNSBL slow | DNS resolution timeouts are 3 s per list; check network connectivity |
| URLhaus test times out | Verify outbound HTTPS to urlhaus-api.abuse.ch is allowed by your firewall. URLhaus lookups are silently skipped if the key is absent, so the daemon will still work without them. |
Upgrading
v0.3.6 — JSON robustness and Ollama multi-model safety
Non-breaking fix. No action required other than pulling, installing dependencies, and restarting:
git -C /opt/verdictmail pull /opt/verdictmail/venv/bin/pip install -r /opt/verdictmail/requirements.txt systemctl restart verdictmail verdictmail-web
Note: VerdictMail runs inside a Python virtual environment at
/opt/verdictmail/venv. Always use/opt/verdictmail/venv/bin/pip— not a barepipcommand — when installing dependencies, or the package will land in the system Python and the service will not see it.
json-repairdependency: Addedjson-repairlibrary as a fallback JSON parser. Whenqwen2.5:7bor other small models produce truncated JSON, invalid escape sequences (e.g.\'), or other malformed output,json-repairrecovers the response automatically rather than exhausting all retries and recordingaction=error. Existing installs must run the venv pip command above to pick this up.- Ollama
num_ctxoverride removed: VerdictMail was sendingnum_ctx: 8192in every Ollama request. If this value differs from the context window the model was loaded with, Ollama reloads the model — briefly evicting it from VRAM and disrupting other users sharing the same Ollama instance. VerdictMail now uses whatever context window the model was already loaded with. For reference,gemma4:26bloaded at 32,768 tokens comfortably covers the observed maximum prompt size of ~8,300 tokens.
Recommended model: gemma4:26b
If you have a capable Ollama server, switching to gemma4:26b (or another 20B+ model) is strongly recommended over qwen2.5:7b. The JSON schema compliance issues that drove the v0.3.3–v0.3.6 fixes are rooted in small model unreliability. Larger models follow the required output schema consistently. Update ai.model in your config/verdictmail.yaml:
v0.3.5 — AI response resilience fixes
Non-breaking fix. No action required other than pulling and restarting:
git -C /opt/verdictmail pull systemctl restart verdictmail verdictmail-web
- Schema unwrapping: Some emails caused
qwen2.5:7bto consistently wrap its response in a nested object (e.g.{"analysis": {...}}) rather than returning the flat schema directly. These messages were recorded asaction=errorin the audit log — completely unanalyzed. The JSON extractor now detects and unwraps one level of nesting automatically. - Failure diagnostics: When AI response validation fails, the first 300 characters of the model's actual output are now logged to the journal, making future schema mismatches diagnosable without database archaeology.
- WHOIS log suppression corrected: The v0.3.4 fix used
setLevel(WARNING)which still passesERRORmessages through (ERROR > WARNING). Changed tosetLevel(CRITICAL)on the parentwhoislogger to fully silence the noise.
v0.3.4 — Ollama reliability and log hygiene
Non-breaking fix. No action required other than pulling and restarting:
git -C /opt/verdictmail pull systemctl restart verdictmail verdictmail-web
- Ollama JSON mode: Added
"format": "json"to all Ollama API requests. Ollama's grammar-constrained sampling now enforces syntactically valid JSON at the token level, eliminating the intermittent parse-error retries that added 10–30 s of latency per affected message. Retry logic is retained for connection errors. - WHOIS log noise: Suppressed the
whois.whoislibrary's internalERRORlog entries for transient socket timeouts. Timeout failures are still captured in each message's enrichmenterror_notes; they were already handled gracefully — only the false-alarm[ERROR]journal noise is removed.
v0.3.3 — Enrichment data in audit log
Non-breaking feature addition. The database schema is migrated automatically on daemon startup — no manual steps required.
git -C /opt/verdictmail pull systemctl restart verdictmail verdictmail-web
The audit log Detail modal now shows a full Enrichment panel for every processed message: SPF/DKIM/DMARC pass/fail badges, display-name spoofing detection, domain age, DNSBL classification (including PBL-only distinction), URLhaus and VirusTotal results, DKIM alignment, and expanded URLs. A new enrichment column is added to the SQLite audit_log table. Records written before v0.3.3 show "Not available — recorded before v0.3.3."
v0.3.2 — Fast shutdown fix
Non-breaking bug fix. No action required other than pulling and restarting:
git -C /opt/verdictmail pull systemctl restart verdictmail
Fixes a 150-second forced shutdown delay that occurred when the daemon was restarted while in exponential backoff (e.g. after importing a backup ZIP on a fresh install with dummy credentials). Shutdown is now instant.
v0.3.1 — Backup & restore
Non-breaking feature addition. No action required when upgrading from v0.3.0.
A Backup & Restore card is now available on the Configuration page:
- Export Config — downloads
verdictmail.yaml(no credentials) - Export Full Backup — downloads a dated ZIP of
verdictmail.yaml+.env(contains credentials — store securely) - Import Config — uploads a
verdictmail.yamlto replace the live config - Import Full Backup — uploads a backup ZIP to restore both
verdictmail.yamland.envin one step
v0.3.0 — IMAP credential variable rename
The environment variables have been renamed for provider-agnostic clarity:
| Old (v0.2.x) | New (v0.3.0+) |
|---|---|
GMAIL_USERNAME |
IMAP_USERNAME |
GMAIL_APP_PASSWORD |
IMAP_PASSWORD |
Action required: Edit /opt/verdictmail/.env and rename the two variables.
The old names still work in v0.3.0 (a deprecation warning will appear in the log)
but will be removed in v0.4.0.
Log rotation
The rotating file handler caps each log file at 10 MB with 5 backups retained.
System-level rotation with logrotate is not required but can be added at /etc/logrotate.d/verdictmail.
Development Notes
VerdictMail was designed and architected by A. Scarola. The implementation was developed with substantial assistance from Claude Code (Anthropic's AI coding assistant), which wrote the majority of the code based on detailed specifications, requirements, and iterative direction from the author. All design decisions, security architecture, feature choices, and testing were directed and validated by the author.
Star History
License
MIT — see LICENSE.
