Add get_email_metadata — headers-only Email/get for least-privilege flows#68
Merged
Merged
Conversation
…lows
The existing get_email tool returns the full Email object — textBody,
htmlBody, bodyValues, attachments — which is great for "show me this
email" workflows but a hammer-too-large for skills that just need to
classify or route based on headers.
This shows up most acutely in privacy-sensitive flows: a skill working
through a customer-mail mailbox might be specifically forbidden from
ingesting message bodies (GDPR / least-privilege rules), but still
needs the basic envelope data — sender, subject, date, mailbox
membership — to do its job. With only get_email available, the skill
has to either (a) take the full payload and remember to discard the
body in its prompt logic, or (b) work around using advanced_search
filters. Both approaches put the privacy guarantee in the wrong place:
in the caller's discipline rather than the tool surface.
get_email_metadata is a tool-level guarantee. It calls Email/get with
a strict properties allowlist:
id, threadId, mailboxIds, keywords, receivedAt, sentAt, subject,
from, to, cc, bcc, replyTo, messageId, inReplyTo, references,
size, hasAttachment
There's no bodyProperties, no fetchTextBodyValues, no
fetchHTMLBodyValues — the JMAP server simply never sends body content
in this tool's path. hasAttachment is a Boolean (yes/no); filenames
and content types deliberately aren't surfaced because filenames
sometimes carry PII ("Smith Invoice June 2026.pdf"-style leakage). If
a caller does need attachment metadata or to download attachments,
get_email_attachments and download_attachment cover that explicitly.
Useful side benefit: this tool returns mailboxIds, which the existing
get_email omits. That makes get_email_metadata the right call for
"verify this email landed in the expected folder after I moved it"
checks — handy in tests and skill self-assessment paths.
Smoke-tested: response carries the 17 expected keys and none of
textBody / htmlBody / bodyValues / body / preview / attachments. The
existing get_email on the same email returns attachments, bodyValues,
htmlBody, textBody — confirming the contrast. All 176 existing unit
tests still pass.
MadLlama25
pushed a commit
that referenced
this pull request
Jul 6, 2026
…gs for advanced_search, search_emails, list_emails, and get_thread PR #68 added get_email_metadata as a header-only sibling of get_email, so callers in privacy-sensitive flows (customer-mail least-privilege scans, GDPR-bound workflows, anything that mustn't ingest message bodies) can pick up envelope data without any body content riding along. The tool surface itself does the enforcing — the JMAP server is simply never asked for body fields, so it never sends any. That's a much stronger guarantee than asking the caller to remember to discard body content after the fact. The other search and thread tools weren't part of that pass and still include 'preview' in their JMAP property request. 'preview' is a ~200-character body excerpt, so every result of every search/list/ thread call quietly surfaces body content to the caller. That gap showed up while wiring a customer-mail scan against the new tooling: the skill had to add a "discard preview before reasoning" guard at the top of every code path, which works but puts the privacy guarantee back into the caller's discipline rather than the tool. This PR closes that gap by adding four parallel tools alongside the existing ones (additive — nothing renamed, no breaking changes): advanced_search_metadata — same filters as advanced_search search_emails_metadata — same query as search_emails list_emails_metadata — same listing as list_emails get_thread_metadata — same thread enumeration as get_thread Each one mirrors its sibling's input schema and behaviour exactly, including get_thread's accept-an-email-id-and-resolve-the-thread flexibility. The only difference is the JMAP property allowlist, which is reduced to a fixed set of header-only fields: id, threadId, subject, from, to, [cc,] replyTo, receivedAt, hasAttachment, keywords (cc is included where the existing siblings included it.) No preview, no textBody, no htmlBody, no bodyValues. Anyone using the existing tools keeps working unchanged; anyone wanting the privacy-safe path reaches for the *_metadata variant by name. Tool descriptions: each new tool's description names its sibling explicitly, lists the property allowlist, and says when to pick it. The goal is that someone scanning the tool list sees not just "another search" but "the privacy-safe version of advanced_search" without having to read the source. Tests: five new tests in jmap-client-extra.test.ts pin the no-body-content invariant for each *_metadata method, plus one that confirms advancedSearchMetadata preserves the AND-conjunction filter logic when both isUnread and isPinned are set (the most intricate piece of advancedSearch's behaviour). All 181 tests pass — 176 pre-existing, 5 new. Bumped to 1.10.0 across package.json, manifest.json, and the Server constructor in src/index.ts (minor — additive feature).
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The existing
get_emailtool returns the full Email object —textBody,htmlBody,bodyValues,attachments— which is great for "show me this email" workflows but a hammer-too-large for skills that just need to classify or route based on headers.This shows up most acutely in privacy-sensitive flows: a skill working through a customer-mail mailbox might be specifically forbidden from ingesting message bodies (GDPR / least-privilege rules), but still needs the basic envelope data — sender, subject, date, mailbox membership — to do its job. With only
get_emailavailable, the skill has to either (a) take the full payload and remember to discard the body in its prompt logic, or (b) work around usingadvanced_searchfilters. Both approaches put the privacy guarantee in the wrong place: in the caller's discipline rather than the tool surface.get_email_metadatais a tool-level guarantee. It callsEmail/getwith a strictpropertiesallowlist:There's no
bodyProperties, nofetchTextBodyValues, nofetchHTMLBodyValues— the JMAP server simply never sends body content in this tool's path.hasAttachmentis a Boolean (yes/no); filenames and content types deliberately aren't surfaced because filenames sometimes carry PII (Smith Invoice June 2026.pdf-style leakage). If a caller does need attachment metadata or to download attachments,get_email_attachmentsanddownload_attachmentcover that explicitly.Useful side benefit: this tool returns
mailboxIds, which the existingget_emailomits. That makesget_email_metadatathe right call for "verify this email landed in the expected folder after I moved it" checks — handy in tests and skill self-assessment paths.Smoke-tested: response carries the 17 expected keys and none of
textBody/htmlBody/bodyValues/body/preview/attachments. The existingget_emailon the same email returnsattachments,bodyValues,htmlBody,textBody— confirming the contrast. All 176 existing unit tests still pass.This PR was drafted with Claude Opus 4.7; I reviewed and tested each commit before opening.