Gmail
June 2, 2026 · View on GitHub
Every drafted email that relates to a tracking issue should
attach to the original inbound Gmail thread — the thread whose
threadId was recorded when the tracker was imported. Gmail's
server-side threader attaches by message-ID-of-parent (passed as
replyToMessageId on the MCP, or by threadId on the OAuth path);
other mail clients (Thunderbird, Outlook, Apple Mail, the
recipient's own client) thread by the MIME In-Reply-To /
References headers and the subject line, which Gmail synthesises
from the parent message in either case.
Both supported drafting backends now provide thread-attachment — see
draft-backends.md:
| Backend | Thread attach | Mechanism |
|---|---|---|
claude_ai_mcp (default) | yes | replyToMessageId — the message ID of the chronologically-last message on the inbound thread |
oauth_curl (opt-in) | yes | threadId plus explicit In-Reply-To / References headers |
The two threading paths available to the skills, in preferred order:
- Thread-attached draft — the first-choice path. Both backends
support this. Resolve the inbound thread's latest message ID (via
get_thread) and pass it toreplyToMessageId, or pass thethreadIdtooauth-draft-create --thread-id. - Subject-matched draft — the pragmatic fallback. Use when the
thread cannot be resolved (archived, deleted, stale
threadId) or the inbound subject is unsafe to match on. Both backends fall back to this by simply omitting the thread-attachment parameter.
The call shape (signature, kwargs, no-send rule) per backend lives in
operations.md — Drafting backends;
the rules on which thread to use and what the other fields look
like live here.
The rule
- Same thread every time, by
threadIdwhen known. Whatever the recipient change — a reporter reply, an ASF-security relay request, a PMC credit question, a follow-up asking for a PoC — the draft should attach to the inbound tracker'sthreadId. A triager reading the Gmail conversation view should see every exchange on a single tracker in one place; if threading breaks, that history scatters across two conversations. - Subject is always
Re: <root subject>, never a fabricated new one. Gmail's own threading survives without matching subjects whenthreadIdis set, but other clients commonly fall back to subject-based threading. A drifted subject looks like a broken conversation on half the world's mail readers. The root subject is the subject of the first message on the inbound thread — not the last reply's subject line, which Gmail may have displayed with a prefix trim. To:may differ from the original correspondents. It is fine to address a draft to a specific person (an ASF security-team member who relayed the report, a named PMC member, an individual reporter) even if the original inbound root was addressed to a list. Threading does not require recipient overlap; it requiresthreadIdor (as the fallback) a matching subject plus the rightIn-Reply-To/Referencesheaders.
Selecting the inbound thread when multiple are recorded
A tracker's Security mailing list thread body field can hold more than one thread when:
- a second reporter independently filed the same root-cause bug
through a different channel and
security-issue-deduplicatemerged the two trackers (one line per reporter, per the dedupe skill's body-field shape); or - an external reporter's report reached the project's security
list via a separate forwarder thread — e.g. a huntr.com bounty
relayed by
@apache/security, a GHSA forward, a HackerOne forward — after the original direct report had already been imported, so the secondary thread was appended to record the duplicate-of provenance.
The rule: default drafts go to the primary reporter's thread,
never to a forwarder/relay thread. The forwarder/relay thread
is kept on the tracker for record-keeping and for back-channel
relay questions only (e.g. "please ask the external reporter to
confirm a credit form") — see asf-relay.md for
the relay-shape body language.
The primary reporter is the one whose name appears in Reporter credited as without a relay annotation, whose direct email started the security-list thread chronologically first, and whose line in Security mailing list thread does not carry any of the forwarder signals below.
Forwarder/relay signals — match case-insensitively in the line's
annotation text (everything around the threadId reference):
via huntr.com,via GHSA,via HackerOne,via bugcrowd,via <any bounty platform>ASF-relayed,ASF-security relay,ASF-security-relay,relayed by @apache/security,relayed byforwarder,forwarded by,relay,relayedhuntr.com bounty <id>-class duplicate,<provider>-class duplicate
If a line has any of these signals it is secondary; the line without any of these signals (or — for legacy trackers that predate the convention — the chronologically-first thread mentioned) is primary.
Worked example. The body field on a real tracker reads:
No public archive URL — tracked privately on Gmail thread `19dc8d4675dfc1f1`.
Aymane Maguiti (huntr.com bounty `abdbcf11-…`-class duplicate, ASF-relayed by @apache/security on 2026-05-04T09:22:25Z): Gmail thread `19def0954b27ac31`.
- Line 1 → primary (no relay signal). Use
19dc8d4675dfc1f1for every default reply: receipt-of-confirmation, credit-question, CVE-allocated status update, advisory-shipped follow-up. - Line 2 → secondary (matches
via huntr.com-class,ASF-relayed). Use19def0954b27ac31only when the project needs to relay a question back through huntr.com to the external reporter and the primary thread cannot deliver it.
Edge cases:
- Only one thread recorded, with relay signals. Classic
ASF-security-relay case. Follow
asf-relay.md; there is no primary thread to fall back to. - Only one thread recorded, no relay signals. Standard single-reporter case; the thread is the primary by default.
- Both lines carry relay signals. Rare — typically a third-party reporter relayed by two different channels. Surface to the user before drafting; do not pick a "least forwarded" line silently.
- Neither line carries a
threadId(PonyMail URL only, no Gmail identifier). The tracker pre-dates the Gmail-threadId convention; fall back to the rollup-commentthreadIdlookup per the per-skill recipes.
Surface the primary/secondary selection in the skill's proposal
so the user sees which thread the draft attaches to ("Drafting
on primary reporter thread 19dc8d4675dfc1f1 (Vincent55); the
secondary huntr.com-relay thread 19def0954b27ac31 was excluded
from default reply targets."). The user can override per draft
if a specific message genuinely needs to go through the relay
channel instead.
Fallback — subject-matched draft when replyToMessageId is unavailable
Thread attachment is the first-choice path, but the skills must also work in cases where the inbound thread cannot be resolved:
- The tracker's security-thread body field was never filled in
(see
../github/issue-template.mdfor the field role). - The
threadIdin that field is stale (the thread was deleted or archived out of the user's Gmail search index). get_threadreturns no messages (the thread exists but has been emptied), so there is noreplyToMessageIdto point at.- The draft is a brand-new outbound ask on a topic the inbound thread did not cover (e.g. a relay request to a PMC member who was not on the original thread), where re-threading on the original inbound is actually confusing.
- The Gmail backend returns an error when attaching the supplied message / thread (rare, but possible if the user has moved the thread between accounts).
- A draft already exists on the same thread and the pile-up
workaround in
draft-backends.mdapplies — drop thread attachment for the new draft so it surfaces in the global Drafts folder.
In these cases, create the draft with replyToMessageId omitted
(or --thread-id omitted on the OAuth path) but with the matching
subject line from the inbound message. Gmail will start a new
conversation on the sender's side, but most other clients (and
Gmail's own subject-fallback behaviour on the recipient's side) will
still thread the reply by subject. This is not as good as a
thread-attached draft, but it is substantially better than either
fabricating a new subject or not drafting at all.
Surface the degraded threading in the skill's proposal so the user knows which path the draft took:
- "Draft attached by
replyToMessageIdto message<msg-id-prefix>...on thread<thread-id-prefix>...." — the good case (defaultclaude_ai_mcpbackend). - "Draft attached by
threadId(viaoauth_curlbackend)." — the good case for the OAuth-opt-in user. - "Draft created by subject fallback (
<reason>). Gmail shows it as a new conversation server-side; the recipient's client will thread it via theRe: <subject>match."
When the fallback kicks in, record the reason on the tracker's status comment so the next sync run can see why a new Gmail thread appeared. Do not silently drop to fallback — it changes the shape of the conversation the reporter sees.
When fallback is not appropriate. Some cases genuinely warrant stopping rather than drafting on a mismatched subject. Examples:
- You have neither a thread to attach to nor a matching subject to use (typically when the tracker has never been linked to any inbound thread at all — a bug, usually a missed import step). Stop and surface it; drafting with no thread context at all is worse than no draft.
- The inbound subject itself is the reason you cannot thread (the reporter sent the report with an empty subject, a generic "Security", or a subject that collides with dozens of unrelated threads in the user's inbox). A same-subject draft would attach to the wrong conversation on the recipient's side. Stop and ask the user to confirm a good subject manually.
Special case — ASF-security relay
When the inbound report arrives via an ASF forwarder rather than
directly from the external reporter, the drafting shape changes
slightly (different To: / Cc:, relay-specific body language) but
the threading rules above are unchanged: resolve and attach to
the inbound thread (replyToMessageId on the default backend,
threadId on oauth_curl); fall back to the inbound subject when
the thread cannot be resolved. See asf-relay.md.