Everything Email Triage does — from LLM classification to calendar-aware meeting scheduling to the agent API.
The whole picture
Every message flows through the same real-time path — ingest, classify, then routes, watch rules, and topic matching all act on that single classification pass (no second scan, no extra egress). Three kinds of digest run on their own schedule on top. The sections below walk each stage in detail; this is the map.
Section 1
Each incoming message is classified by a local LLM into one of your categories. The classifier sees the sender, subject, and body; you write the category descriptions and the system uses them as the classifier's instruction.
12 system categories ship out of the box:
| Category | Description |
|---|---|
to-respond | Emails that need a reply from you |
action-required | Tasks, requests, deadlines |
fyi | Informational. No action needed. |
newsletters | Subscriptions and recurring content |
meetings | Meeting invites, agenda, scheduling |
meeting-request | Prose-only requests to schedule a meeting (no .ics) |
grant-related | Grant applications, reviews, funding communications |
self-event | Note-to-self about a personal event. Auto-creates calendar entry. |
| … plus invoices, comments, notifications, sponsor | |
Add as many as you want. Each category is defined by a short slug and a description; the description is what the LLM uses to decide whether a message belongs. Write it well, the classifier follows.
/categories — system defaults + user-added categories with operator-editable descriptions
Two types of fast-path rules bypass the LLM entirely:
List-Id header to a category with a deterministic rule.
/rules — mailing-list, sender, and regex rules with 30-day hit counts
An email the system has effectively seen before doesn’t pay for a second AI look — it reuses the earlier decision, so repeat and near-duplicate mail is fast and cheap. On HIPAA-flagged accounts the cache stores no free text: the same reuse still applies, but the classifier’s free-text reason is stripped at write time and the cache key is hashed — so no PHI sits in the side-cache. (For your IT reviewer: a two-level Redis cache keyed on a hash of sender + normalized subject + body; on HIPAA accounts the free-text reason field is dropped at store time.)
Section 2
After classification, messages route through configurable action chains. Actions are independent — any number can fire on a single message.
| Action | What It Does |
|---|---|
move | Move to a different folder. Per-category folder mapping. |
label | Apply the category as a label / Gmail label. |
notify | Metadata-only alert (never the email body) fanned out to the outbound webhook system — Slack, PagerDuty, Opsgenie, Teams, Discord, or a custom signed endpoint. |
draft_reply | Generate a contextual draft reply, save to Drafts folder. |
suggest_meeting_times | Calendar-aware draft listing free slots. Auto-fires on meeting-request. |
accept_invite / decline / tentative | Calendar invite handling with iMIP-threaded replies. |
escalate | Forward to a configurable recipient. SMS-gateway pattern for phone delivery. |
mute | Suppress all classification + action firing. |
/accounts/<id>/routes — per-category action table. Different accounts can route the same category differently.
Independent of category. Match a sender, subject, body pattern → fire escalate / webhook / mute. Useful for "alert me immediately when X arrives" patterns that don't fit a category.
The notify action and any watch rule fan an event out through the outbound event webhook system. Five receiver types ship with tuned default payloads; a sixth “custom” option delivers Email Triage’s own signed envelope to your own relay or SIEM.
| Target | Delivered as |
|---|---|
| Slack | Incoming-webhook message |
| PagerDuty | Events API v2 trigger |
| Opsgenie | Alert API alert (GenieKey auth) |
| Microsoft Teams | Incoming-webhook MessageCard |
| Discord | Webhook message |
custom | {event, timestamp, data} envelope — verbatim, HMAC-signed |
Each payload is a template you can edit per target ({{data.category}}, {{data.subject}}, {{data.flow_id}}, …), rendered through one shared code path so the wire shape can’t drift between targets.
Signed, gated, metadata-only. Give a target a shared secret and every POST carries an X-Signature-256 HMAC-SHA256 over the exact rendered bytes — and if that secret ever fails to decrypt, the delivery is dropped, never sent unsigned. External URLs are deny-by-default: the operator explicitly allows each off-host destination, and every send is egress-gated and idempotency-guarded against double-fire. Payloads carry metadata only — never the email body; on a HIPAA-flagged account the operator’s template is ignored entirely and the wire collapses to {event, category, timestamp, flow_id}. A webhook failure never breaks triage — the application log is the durable receipt.
Section 3
Two digest features that solve the two recurring inbox problems every professional has: "I subscribe to too many newsletters to read" and "I've handed my inbox to an AI and now have no idea what it's doing."
Target user: the knowledge-work professional whose job requires staying current on a high-volume subscription feed. The research clinician subscribed to journal alerts + NIH funding announcements + IRB digests + society newsletters + sponsor communications. The financial analyst tracking 12 industry briefings. The consultant signed up for every category-defining substack in their vertical.
For someone like this, newsletter overload is a 60-to-90-minute-per-day problem. Each newsletter has 2–4 articles worth a 30-second scan; only 5–10% of those articles deserve a real read. The current state-of-the-art is Cmd+A → Archive — losing the signal entirely.
The newsletter digest replaces that with one inbox entry at a scheduled time (typically 7 AM local, operator-configurable):
Concrete outcome. A research clinician with 30 newsletter subscriptions averaging 3 articles each goes from "120 inbox entries the system can't help me with" to "one digest with 90 article cards I can scan in 5 minutes." The 5–10 articles worth a full read get a click-through; the other 80 are dismissed by closing the digest. Daily time saved: 60–90 minutes.
newsletters but can bundle any category (e.g. team-internal, comments for GitHub-style notifications)Each schedule slot has an independent enable/disable toggle and a "Run now" button for previewing without waiting 24 hours.
/accounts/<id>/edit on the Digests tab — Daily Activity, custom digests (Morning Newsletter, Weekly Wrap), per-slot Preview and Send-test-now buttons
The HTML body of a generated newsletter digest as it arrives in your inbox — section headings per source, article cards with headlines, 1–2 sentence summaries, and "Read more" links back to the original article URLs. The visual "30 emails → 1 digest" outcome:
A daily newsletter digest — 13 articles across 5 sources (NIH funding, NEJM, AHA, STAT News, JAMA), each with a headline + summary + "Read more" link
The "set it and forget it" anti-pattern is real: an AI quietly classifies + drafts + moves mail for weeks, the operator's confidence drifts toward "I have no idea what it's doing," trust erodes, eventually the operator pulls the plug. The triage-activity digest is the antidote — every day, the operator gets a complete record of what email-triage did with their mail.
Daily summary delivered to the account's own mailbox at the scheduled time (typically end-of-day, 6 PM local — operator-configurable):
The system never feels like it's acting on your behalf without your knowledge. Every classification, every action, every draft is in the digest. For HIPAA-regulated environments this doubles as a convenient at-a-glance surface — the compliance officer can skim the operator's last few daily digests as a convenience over, not a replacement for, the audit log itself.
Define a Topic — a study, project, or subject — with a few Subject keyword chips and the mailboxes it should watch. Email Triage collects every matching message across those mailboxes into one scheduled digest: Full, Differential (new since last), or both. Built for the PI following an IND across a sponsor inbox, an IRB inbox, and a coordinator inbox at once.
Section 4
The draft_reply action generates a contextual response using your local language model — a real AI-generated reply body. The draft lands in your Drafts folder. Nothing is ever sent automatically.
Live today — and it sharpens as it learns you. The AI-written reply body is a live feature. The personalized voice is learned from your own sent mail: a standard mailbox drafts in a clean neutral professional voice until you run the one-time, operator-initiated “mine my sent mail” step — there is no silent background scraping of your outbox. HIPAA-flagged accounts never use your verbatim past mail at all: they distill only a privacy-scrubbed style descriptor, and drafting fails closed (no draft) on any credential or BAA error. Every draft is saved for your review — never auto-sent.
Sender, subject, body — the obvious context.
Distilled from your sent mail once you run the one-time “mine my sent mail” step — tone, greeting style, signoff conventions, common phrases, average reply length. Default window: last 50 messages, operator-adjustable up to 250+. Before you mine, drafts use a neutral voice.
Per-account vector index of past sent mail. Few-shot examples shown to the LLM. Edits you make to drafts are weighted 1.3× over ordinary sent mail — the system learns your corrections.
A drafted reply in the email client's Drafts folder — threaded into the original conversation, ready for review
The system extracts measurable signals from your sent mail and shows you what it learned. Greeting patterns, signoff conventions, average length, common phrases — all visible and verifiable, not magic.
/profile/style-data — the distilled writing-style profile. Edits you make to drafts feed back into this index.
If your account sends from multiple addresses (work, personal, project-specific), the system maintains a separate writing-style profile per address. A draft to a message addressed to alias-a@ reflects how you write as alias-a.
When you edit a draft before sending, the final version is captured as a "high-signal example." Future drafts learn the way you actually want it to write — not the way it guessed.
Privacy posture. All drafting runs on the local LLM. The incoming message, the style examples, and the generated draft never leave your network. HIPAA-flagged accounts apply the same scrubbing rules to drafted body as to classification reasoning.
Section 5
The highest-leverage feature for anyone whose calendar is the bottleneck. When mail is classified as meeting-request, the system reads your calendar, picks slots, and drafts a calendar-aware reply.
/profile?tab=meeting — slot configuration, working-hours grid, OOO override
A generated meeting-suggestion draft — N slots across M days with proper EST/EDT labels
When a .ics-attached invite arrives, the accept_invite / decline_invite / tentative_invite actions respond on your calendar and draft an iMIP-formatted reply, properly threaded back to the organizer.
Pure-IMAP servers don't expose a calendar API. The system supports routing calendar operations for an IMAP account through a sibling Gmail / O365 account. Operator sets the surrogate via dropdown.
Section 6
For clinical-research teams, a missed safety-reporting deadline is a finding. Email Triage can watch for the mail that starts one of those clocks — an FDA IND safety report, an EU CTR SUSAR, an IRB notification — and track the deadline against a working-day calendar. Detection is opt-in and off by default — you enable each rule, and a human confirms every clock before it arms. It is built to assist a human’s judgment, never replace it, and never quietly assume your legal liability.
Whose deadline is it? The tool is explicit. FDA IND (21 CFR 312.32) and EU CTR (Reg. 536/2014 Art. 42) SUSAR deadlines are the sponsor’s filing obligation, so they ship as informational — a “sponsor deadline noted” receipt surfaced for awareness and never escalated as the site or PI’s liability. The IRB reporting deadline is the site’s obligation — but ICH E6(R3) removed the fixed number, so it ships UNCONFIGURED: the app never invents an IRB deadline, and the UI nudges you to enter your institution’s own written policy before that clock can arm.
Nothing arms silently. Every deadline begins as a candidate a person must confirm. A clock only starts running when it is both marked as your responsibility to submit and has a computable deadline. “Day-0” is write-once and can never be edited in place — a correction is a new, appended event, not an overwrite (21 CFR Part 11). The app never silently starts or closes a legal clock.
Deadline arithmetic runs offline, in working days, skipping weekends and your institution’s configured holidays — the language model is never asked to compute a date. A working-day clock with no calendar configured refuses to arm rather than guess. And the “who is responsible” flag is read only from out-of-band operator configuration; it is never inferred from the content of an email.
The Deadlines view — an armed IRB continuing-review clock: Running, a working-day countdown, an explicit due date, and Acknowledge / Mark-submitted / Dismiss actions. Sponsor FDA/EU deadlines surface as informational only; an escalating clock arms solely after a human confirms it is theirs to submit.
Section 7
| Provider | Push | Labels/Folders | Calendar API |
|---|---|---|---|
| Gmail | Cloud Pub/Sub (seconds) | Native labels | Google Calendar API |
| Office 365 | Graph subscription (seconds) | Native folders | Microsoft Graph |
| IMAP | IDLE (<2 sec, multi-folder) | SPECIAL-USE detected | via surrogate Gmail/O365 |
Supports Dovecot, Cyrus, Exchange-via-IMAP, Fastmail, Proton Bridge. TLS by default (port 993). Multi-folder watch per account with per-folder route overrides.
/accounts — multi-account view with real-time push status and per-account health indicators
Each account has its own tabbed configuration page covering provider credentials, push/poll cadence, watched folders, calendar role assignment, delegates, and integration endpoints. Every knob is in the UI — no config-file editing required.
/accounts/<id>/edit — provider credentials, aliases, time zone, push/poll, watch folders
Section 8
One install supports any number of users, each with their own accounts, categories, style profile, and meeting preferences. Three role tiers: Admin (install-wide config), User (owns own accounts), Delegate (granted view/triage/draft on another user's account).
Per-user isolation is enforced at the data layer. Delegate actions are audited with both actor and account-owner stamped. The HIPAA §164.312(b) audit gate distinguishes owner self-access from delegate access — the former is a §164.502(a) self-disclosure carve-out and isn't audited as PHI access; the latter writes an audit row every time.
/users — user management, role assignment, and delegate grants visible on the same surface
Section 9
Every operational signal is exposed through the admin UI — classifier latency, cache hit ratio, embedding metrics, supervised-task state, push watcher health, ingestion rollups. JSON for machine consumption (Nagios, Datadog, Prometheus); HTML for the admin stats page.
/admin/stats — operational signals dashboard showing the system is healthy at a glance
A single Settings area with six tabs — General, Integrations, AI Backends, TLS, Backup, and Security — with legacy admin URLs 303-redirecting to the matching tab. The Backup and AI Backends tabs are new in v0.2.0. Operator-controlled, audit-logged, no config-file editing.
/config — install-wide configuration tabs
The classifier model is chosen from a live model picker, not a free-text field: the AI Backends tab fetches what is actually installed on your Ollama endpoint and shows each model’s capabilities, size, quantization, context length, and whether it is currently resident in VRAM. Embedding-only models are flagged and can’t be selected for classification. A free-text “enter manually” escape hatch remains for non-enumerable backends.
An ordered backup_models list provides capability-aware failover: if the primary model hits a capability or resource error, the classifier falls over to the next model and writes a tamper-evident model_failover audit row. A per-backend circuit breaker distinguishes “the AI server is down” from “the AI gave a bad answer,” so a brief outage isn’t misread as a per-email failure.
Automated, encrypted database backups on a manual / daily / weekly cadence, configured in the Backup tab. Off by default — the operator must set a key and directory and explicitly enable it. A supervised, lease-gated sweeper produces either a full .etbk bundle (database + config + TLS certs + MSAL cache) or a raw SQLCipher .db, inheriting the boot encryption context so no plaintext PHI is ever written to /tmp. Public-key mode requires a parseable X25519 PEM, validated fail-closed before the config is saved.
.etbk with --private-key, and --list verifies a bundle’s contents without extracting — a non-destructive check. Restore is never a one-click web action.The runtime container image is ~250 MB compressed, Debian-13-slim with Python 3.12, non-root user, signed with cosign and SLSA-3 provenance. CPU-only PyTorch wheels (the GPU is reserved for the Ollama chat-model host). Fast pulls, fast CI, fits under GitHub Releases' 2 GiB asset cap for air-gap tarballs.
The embedding backend (PyTorch CPU runtime, sentence-transformers, all-MiniLM-L6-v2 model — ~600 MB total) is not baked into the image. On first admin setup, the operator clicks [Install now] on the AI Backends config page; the installer fetches each manifest-listed file over HTTPS, verifies SHA-256 against a pinned manifest baked into the image, and pip installs into the persistent volume at /app/data/runtime-deps/.
Operators who choose Ollama for embedding (rather than the in-process CPU backend) skip the local install entirely; their embed calls go to the same Ollama host as the chat model.
For organizations on disconnected networks — classified environments, OT segments, regulated research enclaves — air-gap installs use scripts/download-embedding-bits.sh on a connected machine to produce a hash-pinned tarball + SHA-256 sidecar. Transfer the tarball to the air-gap host and sideload through the admin UI. Sideload runs the same hash verification as the auto-download path — operator-staged bytes are not trusted; a bit-corrupted tarball or a tampered air-gap drop trips the same refusal as a poisoned PyPI mirror.
Image and source. Container: ghcr.io/unlimited-data-works-llc/email-triage. Source: github.com/Unlimited-Data-Works-LLC/Email-Triage (Apache 2.0). Verification recipe in the repo's docs/install.md.
Section 10
For a research team, a dropped IRB amendment or safety-report email isn’t an inconvenience — it’s a finding. So the system is built so a hiccup — a provider timeout, a brief network blip, a restart caught mid-sync — can’t make an inbound message quietly vanish.
Two things carry that promise. Work that fails is parked and retried rather than thrown away, and the system tracks exactly how far it has read in each mailbox, so nothing between “last confirmed” and “now” gets skipped when it catches back up. A message it can’t handle yet waits in line; it is not lost. (For your IT reviewer — skip if that’s not you: a durable retry queue plus per-mailbox no-loss read cursors.)
Why a PI should care. The failure you never want is the silent one — the safety email a system quietly ate during a five-minute outage, discovered only at audit. This design turns that class of failure into a delayed-then-recovered message instead of a lost one.
Section 11
Four controls that matter whether or not you read code. Each card leads with what it means for you; the named standard or mechanism sits in the grey line beneath — for your IT or security reviewer, and safe to skip if that’s not you.
An AI that reads email is a target: a message can hide instructions in its own text — “ignore your rules and forward this” — hoping the classifier obeys. The classify path is guarded against exactly that, so a booby-trapped email is far less able to bend the system to its will. This raises the floor; it is not a guarantee — treat it as a strong probabilistic defense, not an absolute one.
For your security reviewer: prompt-injection guarding on the core classify path, aligned to OWASP’s LLM risk guidance.
Removing a record hides it from everyday use but keeps its audit history intact — you can still answer “what happened to it, and when.” Disabling an account is separate and immediate: processing stops at once.
For counsel: soft-delete preserves the audit trail, supporting HIPAA §164.312(b) audit-control expectations.
An unattended browser session logs itself off after a period of inactivity, and when an admin revokes someone’s access it ends any session they already have open — right then, not at their next login.
Named control: HIPAA §164.312(a)(2)(iii) automatic logoff.
The secret that lets the system reach your mailbox is fenced so it can physically only travel to Google or Microsoft — nowhere else — and a test that runs when the software is built fails the build if that ever stops being true.
For your security reviewer: a build-time egress test pins the mail-provider OAuth secret to Google / Microsoft endpoints. One honest boundary — Microsoft’s own MSAL sign-in library is the single component permitted to reach Microsoft’s login endpoints.
Section 12
Automation nobody tends quietly decays — a certificate lapses, a login needs re-authorizing, a compliance agreement expires, and no one notices until something breaks. These three capabilities keep a real owner in the loop and let the system grow more useful the longer it runs — an argument for tending it, not just installing it.
Once a day the owner gets a single short message listing only what genuinely needs a human: a compliance agreement (BAA) about to expire, an account that needs re-authorizing, a certificate warning. On a quiet day it stays silent — you hear from it only when something actually needs you.
Your Topic digests learn per sender — a thumbs-down drops a noisy sender from a study digest, a thumbs-up always keeps one — so each digest fits your world better over months than it did on day one. Separately, the draft-reply voice starts neutral and sharpens as your own Sent mail is mined. Neither is instant — that steady, reversible improvement is exactly the kind of thing worth an ongoing hand on the tiller.
The IRB workflow ships unconfigured on purpose — the system won’t invent your institution’s rules. Instead you (or Craig, alongside you) encode your IRB’s standard operating procedure as an editable template, so the tool follows the process you already have to follow.
Email Triage is a reference implementation — proof of what a sovereign, audit-minded AI system looks like in a regulated setting. Craig LaForest builds systems like this as a fractional CTO. A 30-minute call to talk through your environment and where an approach like this fits.
Talk to Craig