Mail source

May 25, 2026 · View on GitHub

security-issue-import (and the mail-touching paths in security-issue-sync, security-cve-allocate, security-issue-triage) scan an inbound <security-list> for security reports, read threads, and draft replies. The skills treat every supported source — Gmail, PonyMail, IMAP, a static mbox snapshot, the next one we plug in — the same way: through the abstract operations defined here. The adopting project's <project-config>/project.md → Mail sources section declares which backends are configured, what role each plays, and which (if any) are mandatory.

This file is the single source of truth for what a backend is expected to do and how the skills choose between configured backends. Backend-specific docs — tools/gmail/tool.md, tools/ponymail/tool.md, tools/mail-source/imap/README.md, and tools/mail-source/mbox/README.md — implement this contract.

Abstract operations

A mail-source backend exposes some subset of these operations. Some subset matters — Ponymail is read-only, an mbox snapshot is read-only-and-offline, and a corporate IMAP may or may not allow drafts depending on policy. The skills check capability before dispatch; backends that don't support an op are skipped in the resolution chain for that op.

OperationWhat it doesWhy the skills need it
list_recent_threads(list, since)Return threads on <list> newer than since (typically 14 / 30 / 90 days)security-issue-import Step 1 — find candidate reports that have arrived since the last sweep
read_thread(thread_id)Return full message history of a thread by stable identifierSteps 2–4 of -import; -sync reads tracker-linked threads for credit / CVE-reviewer / status signals
list_drafts(thread_id)Return draft replies already attached to a threadIdempotency — never propose a fresh draft when one is already pending
list_sent_since(thread_id, since)Return outbound replies sent on a thread within a windowDetect "we already replied; the ball is in the reporter's court"
create_draft(thread_id, body, …)Compose an un-sent reply attached to the thread (subject inherits, In-Reply-To set per the threading rule)Every reporter-facing reply the skills propose. Drafts only — never sends per the framework rule
thread_url(thread_id)Return a human-clickable URL to the thread (for tracker body fields, sync rollups, etc.)The Security mailing list thread tracker field
thread_id_kind (attribute)The shape of identifiers this backend emits (e.g. Gmail UUID, PonyMail hash, RFC-5322 Message-ID)Lets the skills tag stored IDs with their source so a future sync from a different backend doesn't dedupe across kinds

A backend's capability set is its supported subset of the operations above. The capability matrix below summarises the in-tree adapters.

Capability matrix

Backendlist_recent_threadsread_threadlist_draftslist_sent_sincecreate_draftthread_urlNotes
gmailFull read + write; primary backend in the reference adopter
ponymailRead-only public/private archive viewer; auth via ASF LDAP
imapdependsdependsStub adapter; create_draft / list_drafts depend on whether the IMAP server exposes the Drafts folder writably to the agent
mbox✓ (offline)✗ (or file://)Static archive snapshot; forensics / late triage only

Backends added by adopters extend the matrix in their own adapter README. The skill never assumes a backend has an op without consulting the matrix first.

Adopter declaration in <project-config>/project.md

The adopter declares the configured backends under a Mail sources section, one entry per backend:

## Mail sources

| Backend | Role | Mandatory | Notes |
|---|---|---|---|
| `gmail`    | primary  | yes | Triager Gmail account is subscribed to `<security-list>` and `<private-list>` |
| `ponymail` | fallback | no  | Read-only archive backstop when Gmail history is incomplete |

Role values

  • primary — the default backend for every operation it supports. Exactly one backend may carry this role.
  • preferred for <op> — overrides primary for one specific operation. Multiple backends may carry this role, each for a different op. Example: a project that runs IMAP for inbound mail but uses Gmail only for drafting would declare imap: preferred for read_thread, list_recent_threads and gmail: preferred for create_draft, list_drafts.
  • fallback — try after primary / preferred for any op the primary doesn't support, in the order the backends are listed.
  • optional — available for ad-hoc use but never in the resolution chain. Useful for adapter docs that are present but not wired in yet.

Mandatory flag

yes means the skill refuses to run when the backend is unavailable (auth missing, MCP server down, archive directory not mounted). The skill surfaces a clear "mandatory backend <name> unavailable: <reason>; run aborted" and exits without proposing anything.

no means the skill continues with the remaining backends. If a specific op then has no backend to dispatch to, the skill skips that operation's proposal (e.g. "no draft backend available — Step 7 proposal omitted, please draft the receipt-of-confirmation reply by hand") and keeps going.

Resolution rule — which backend runs an operation?

For each operation <op> the skill needs to dispatch:

  1. If a backend is marked preferred for <op> and the matrix shows it supports <op> and it is available, use it. Stop.
  2. Else if the primary backend supports <op> and is available, use it. Stop.
  3. Else walk the fallback backends in the declared order; use the first one that supports <op> and is available. Stop.
  4. Else: if any mandatory backend was unavailable, abort the run per the mandatory rule above. Otherwise skip this operation's proposal with a one-line "no backend available for <op>" note and continue.

The resolution result is logged in every proposal recap so the user can see which backend served which op.

Backend implementation contract

A new backend (tools/mail-source/<name>/) ships a README.md that:

  1. States which operations it supports (its column in the matrix above).
  2. Describes how each supported op is invoked (CLI command, MCP tool name, API call) and what identifier shape the backend emits for thread_id.
  3. Documents auth + setup: what credentials / sessions / subscriptions the operator needs in place before the skill can dispatch.
  4. Notes any fast-path / slow-path differences (e.g. PonyMail's private-list reads require an active ASF-LDAP session) so the skill can route accordingly.

The contract is intentionally minimal — backends are expected to gracefully decline ops they don't support (NotSupported / explicit error) rather than fake-implement them. Faking causes silent data loss (a draft that goes nowhere, a thread read that returns empty); declining lets the resolution rule fall through.

What this contract does NOT cover

  • Send semantics. The framework rule is draft, never send; every backend that exposes create_draft is expected to honour it. No backend exposes a send op through this contract.
  • PII / privacy filtering. Backends return raw thread content; the privacy-LLM filtering layer ( tools/privacy-llm/) sits between the backend and any LLM consumer. The contract is at the wire-format level, not the content-policy level.
  • Tracker reconciliation. Backends emit thread IDs and URLs; the tracker's Security mailing list thread field is owned by the tracker, not by any backend. Skills decide what to write there based on the resolution result.