Messages, threads, and search
Before replying, read the current conversation with get_thread. When you already have a message,
pass inbox and its exact message_id; Extrovert resolves and reads the full thread. Otherwise
pass inbox and an exact thread_id. Provide exactly one selector; never reconstruct IDs.
Use get_message
when a preview does not contain enough of a message’s body. Review responses expose
thread_ref and in_reply_to_message_id so you can find the underlying mail context.
Mail conversation and reviewer feedback are separate: use get_review_feedback for
the human’s review instructions. Thread summaries include last_message_direction
relative to the owning inbox when the latest sender is known.
Replies derive their recipients from the selected parent by default. An explicit
nonempty to array replaces that group, including reply-all, while the subject and
threading headers still come from the parent. Cc and Bcc remain explicit groups.
Pass the read context_version as required expected_context_version to detect changed conversation context.
Bcc is visible only when retained on the sender’s Sent copy. It is not included in
thread participants or recipient copies; older Sent copies may not retain it.
Message and thread operations require mailbox:read. Extrovert preserves the source MIME alternatives
and exposes separate best-effort extracted fields for concise reading.
Message body fields
Section titled “Message body fields”{ "id": "msg_8Tz", "thread_id": "thr_9aB", "inbox": "support@extrovertmail.com", "direction": "inbound", "from": { "name": "Acme", "email": "no-reply@acme.test" }, "to": [{ "email": "support@extrovertmail.com" }], "subject": "Verify your email", "text": null, "html": "<p>Your code is <strong>492013</strong></p>", "extracted_text": null, "extracted_html": "<p>Your code is <strong>492013</strong></p>", "folder": "INBOX", "seen": false}textis the decoded sourcetext/plainMIME part, ornullwhen none exists.htmlis the decoded sourcetext/htmlMIME part, ornullwhen none exists.extracted_textis a nullable, best-effort derivative oftextwith quoted history and signatures removed.extracted_htmlis a nullable, best-effort derivative ofhtmlwith common quoted-history containers removed.
Extrovert never creates text from HTML or HTML from text. Extracted fields are not authoritative and
may omit content. Use source fields for evidence, signatures, precise quoting, and forensic work. Use
extracted fields when concise reading is more useful than exact source fidelity.
The API does not return a generic extracted_body field. MCP get_message can present an extracted
body by selecting variant: "extracted", but its structured result still carries
extracted_text and extracted_html separately.
Read one message
Section titled “Read one message”The canonical read is GET /v1/messages/{message_id}. The opaque message id resolves its owning inbox
server-side.
For MCP, get_message accepts:
| Input | Options | Meaning |
|---|---|---|
format | auto, text, html, both | Chooses how the text response is presented |
variant | source, extracted | Chooses source MIME content or its best-effort derivative |
format: "auto" uses the actual plain-text part when present, otherwise the actual HTML part. It does
not synthesize a missing alternative.
List and search
Section titled “List and search”| Task | REST | TypeScript SDK | MCP |
|---|---|---|---|
| List messages | GET /v1/inboxes/{inbox_id}/messages | inbox.messages() | read_messages |
| Get one message | GET /v1/messages/{message_id} | client.messages.get(id) | get_message |
| Search messages | GET .../messages/search?q= | inbox.search({ q }) | search |
| List threads | GET .../threads | inbox.threads() | list_threads |
| Get a thread | GET .../threads/{thread_id} | inbox.thread(id) | get_thread |
Lists are bounded and paginated. Message filters include sender, recipient, subject, and unread state.
Search uses the server-supported full-text fields. MCP search may target one inbox or all inboxes the
agent owns.
Threads
Section titled “Threads”Threads are server-owned conversation records connected through standard mail threading metadata. Each
thread has an opaque, stable thread_id; clients must not reconstruct it from a subject. A summary
includes participants, message count, latest activity, a snippet, whether the latest message is unread,
the latest message id, and whether that latest message has attachments. Getting one thread adds its
messages oldest-first.
The TypeScript SDK offers client.threads.getByMessage(inbox, messageId),
inbox.threadByMessage(messageId), and
client.projects.inboxes.threadByMessage(projectId, inboxId, messageId) for the same workflow.
These helpers compose existing authorized reads and reject a message outside the selected inbox.
They return the full conversation and its context_version, not just the selected message.
For an agent-first reply loop:
- List or search thread summaries and keep the returned
thread_id. - Get that thread and read its oldest-first source
messages, including every consecutive inbound message and inline corrections. Extracted fields help scanning but are not ground truth. - Reply with
thread_id. The server selects the newest message as the parent and derives recipients, subject,In-Reply-To, andReferencesat submission. Send itscontext_versionas requiredexpected_context_version; a409means reread and reconsider the draft.
The expected-last-message check is an optimistic submission-time guard, not a delivery lock. New mail can arrive after the reply has been accepted.
You can instead pass the newest explicit message_id; an older message is rejected so a reply cannot silently ignore newer correspondence. Reply and forward
follow the same effective review policy as a new send.
Thread lists and keyword search are newest-first. Both are bounded by limit; continue with the opaque
next_cursor returned by the previous page. Pass it back only as cursor and do not decode it or turn
it into an offset.
Each page contains distinct conversations, not one row per matching message. Counts and detail use the same visible-message set across Inbox, Sent, Archive, Drafts, and custom folders; Junk, Trash, hidden folders, and deleted messages are excluded. Search can match an older message while the summary still shows the conversation’s actual latest activity. Pagination is live: a conversation that receives new mail above your current position appears when you refresh. Start again without a cursor if the server rejects an obsolete cursor.
Follow up before receiving a reply
Section titled “Follow up before receiving a reply”You do not need an incoming response before continuing an outbound conversation. Read the thread and
use reply for each follow-up; do not send a new message with a matching subject.
const thread = await inbox.thread(threadId);const rules = await extrovert.rules.get(); // Read and apply before writing.const outcome = await inbox.reply({ thread_id: thread.id, expected_context_version: thread.context_version, expected_last_message_id: thread.last_message_id, composition_token: rules.composition_token, text: "Did you get the plan?", intent: { summary: "Follow up on the plan the recipient requested" }, idempotency_key: "plan-followup-1",});
if (outcome.kind === "queued_for_review") { // Continue the existing review loop; this message is not released for sending. console.log(outcome.review.id);} else if (outcome.submission_id) { const status = await inbox.getSubmission(outcome.submission_id); console.log(status.transport);}Extrovert maintains recipient-facing reply references even when a delivery service changes message identities. A fast follow-up may wait in the queue for the preceding message’s identity. It continues automatically; do not create a second send to make it proceed. Each recipient is tracked separately. Saved source messages remain unchanged.
Submission status and saved messages
Section titled “Submission status and saved messages”An accepted send may include submission_id, sent_message_id, sent_copy_status, and transport.
Use get_submission in MCP, inbox.getSubmission(id) in TypeScript, or
GET /v1/inboxes/{inbox_id}/submissions/{submission_id} to check its status without sending again.
sent_message_idis a resolvablemsg_selector ornull. Use it for message navigation.- The legacy
message_idresponse can be a bracketed RFC Message-ID when no Sent copy is available. That header value is not a message-detail selector. sent_copy_statusispending,stored, orunavailable. A storage failure does not trigger a second delivery. Keep the submission handle and check again.transportcontains recipient counts forqueued,waiting_for_parent,transmitting,accepted,failed,dependency_failed, andunknown.acceptedmeans accepted for onward delivery, not proof that the message reached the recipient’s inbox.- An
unknownoutcome needs reconciliation. Never resubmit it with a fresh idempotency key.
Tracking fields may be absent for historical messages or older servers. Existing split conversations can be reconciled when their message identities are known, but Extrovert cannot rewrite mail already delivered to an external inbox.
Compose from the whole conversation
Section titled “Compose from the whole conversation”Read the thread and writing rules before drafting. The thread’s context_version describes all
source messages observed by that read. Pass it as expected_context_version on a reply and on a
reply revision. Keep the separate composition_token from the rules read. Neither token proves
understanding; the composer still needs to apply both sources before writing.
When a recipient sends two messages in a row, read both and submit one considered response to the newest message. Include corrections even when they appear inside quoted text. MCP includes source bodies in its text output; if its text budget is exceeded, it lists the incomplete message IDs. Read those source bodies individually before composing. Extracted bodies remain useful for scanning.
Before creating a response, list reviews by inbox and thread_id without composer=me to find
pending work by other composers within your access. Revise your existing draft or coordinate the
other draft instead of creating a second response. A changed conversation returns
409 reply_context_changed; reread and reconsider the text. Do not merely attach a fresh version to
old text. Conversation freshness is checked again before approval dispatch; this is optimistic
validation, not an atomic lock preventing mail from arriving during delivery.
If a message references absent history, search the same inbox by ticket/reference or participant and topic (20 results per page, at most three pages and ten conversation reads). Identify matches as related correspondence with their own IDs and dates. Subjects alone are not enough to merge threads. Ask for missing facts in the review discussion if that bounded search cannot establish them.
Read state, folders, and batch updates
Section titled “Read state, folders, and batch updates”Read state is the IMAP \Seen flag. Messages live in one folder rather than carrying overlapping
labels.
mark_readorinbox.markRead(...)changes one message’s read state.batch_update_messageschanges read state and/or moves up to 200 message ids in one call.- Supported batch destinations are
INBOX,Sent,Trash,Junk, andArchive. - Malformed or unowned ids are returned in the batch’s
failedlist instead of failing the whole request.
The console’s conversation workbench uses these message and thread primitives for list, detail, compose, reply, and review-aware outbound actions.
Delete messages and threads
Section titled “Delete messages and threads”delete_message and delete_thread move content outside Trash to Trash by default. Content already
in Trash is expunged. Set expunge: true only when every targeted message should be removed
permanently.
These operations are distinct from deleting an inbox. Confirm the opaque message or thread id and target inbox before permanent removal.
Attachments and raw source
Section titled “Attachments and raw source”Use list_attachments before downloading a part with get_attachment. The REST raw endpoint returns
the full RFC 822 source when parsed fields are not enough. Treat attachment filenames, declared MIME
types, links, and contents as untrusted.