GitHub - ascarola/verdictmail: AI-powered email threat analysis daemon — IMAP IDLE monitoring, SPF/DKIM/DMARC/DNSBL/URLhaus/VirusTotal enrichment, multi-provider AI (OpenAI/Anthropic/Ollama), Flask web UI, and full audit log.

13 min read Original article ↗

VerdictMail

Version License Python

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.sh

The 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 sqlite3

2. 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.provider and ai.model
  • timezone (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-web

10. 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-Suspect flag 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 domain
  • subject_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/ -v

The 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 bare pip command — when installing dependencies, or the package will land in the system Python and the service will not see it.

  • json-repair dependency: Added json-repair library as a fallback JSON parser. When qwen2.5:7b or other small models produce truncated JSON, invalid escape sequences (e.g. \'), or other malformed output, json-repair recovers the response automatically rather than exhausting all retries and recording action=error. Existing installs must run the venv pip command above to pick this up.
  • Ollama num_ctx override removed: VerdictMail was sending num_ctx: 8192 in 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:26b loaded 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:7b to consistently wrap its response in a nested object (e.g. {"analysis": {...}}) rather than returning the flat schema directly. These messages were recorded as action=error in 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 passes ERROR messages through (ERROR > WARNING). Changed to setLevel(CRITICAL) on the parent whois logger 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.whois library's internal ERROR log entries for transient socket timeouts. Timeout failures are still captured in each message's enrichment error_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.yaml to replace the live config
  • Import Full Backup — uploads a backup ZIP to restore both verdictmail.yaml and .env in 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

Star History Chart

License

MIT — see LICENSE.