Problem
msgvault already holds everything needed to send a correct reply, except the ability to send it.
For every synced account it has the credentials and a live connection, the full RFC822 source in message_raw, the thread structure in conversations and messages.reply_to_message_id, the recipient set in message_recipients, and the sending identity in account_identities.
Anything that wants to act on archived mail has to rebuild all of that somewhere else: a second credential store, a second IMAP/SMTP connection to the same servers, and its own re-derivation of In-Reply-To and References from raw source msgvault has already parsed. That duplication is the whole cost of every "email agent" integration built on top of an archive, and it is avoidable here.
Proposal
An outbound path that composes from the archive and emits through the account the message belongs to.
Endpoints (mirrored in the MCP server and the CLI):
POST /api/v1/messages/{id}/reply — build a reply to an archived message. Server sets In-Reply-To, the full References chain, Re: subject normalisation, and the recipient set for reply or reply-all. From resolves through account_identities for the source that message arrived on.
POST /api/v1/messages/{id}/forward — same, preserving attachments from attachments rather than re-uploading them.
POST /api/v1/send — fresh compose against a named source.
Two modes, draft first:
mode=draft (default) does an IMAP APPEND to the account's Drafts mailbox with \Draft, and returns the resulting message id. The draft syncs to every client on that account, so review happens wherever the user already reads mail.
mode=send performs SMTP submission, then APPENDs to Sent for providers that do not do it server-side.
Coherence: on success the outbound message is recorded in the store immediately, rather than waiting for the next sync poll to discover it. Without this, anything acting through the API sees its own writes disappear for one poll interval.
Why this belongs here rather than in a separate tool
internal/deletion/ already writes back to the source: executor.go deletes messages from the provider with manifests, batching and retry. So outbound is not a new class of behaviour for the project, and the safety machinery for "act on the real mailbox" already exists and has a shape worth reusing.
Everything else about a correct reply — identity selection, threading headers, attachment bytes, the connection itself — is already implemented for sync. Submission is the missing verb, not a missing subsystem.
Scope and safety
The README's pitch is a complete local copy where "everything runs offline" and "no network access required". Submission should not weaken that, so:
- Off by default, enabled per-source in config. An install that never turns it on behaves exactly as it does today.
mode=send gated separately from mode=draft, so an install can allow draft creation while making autonomous sending impossible rather than merely discouraged — the same posture the deletion path takes toward destructive action.
- No outbound network activity unless an endpoint is invoked.
Open questions
- SMTP configuration. An IMAP config does not imply an SMTP host, and autodiscovery is unreliable. Explicit per-source SMTP settings, or derive-then-confirm?
- Auth. Gmail is XOAUTH2 on an existing token. Generic IMAP is usually password. Gateway setups (DavMail and similar) present plain SMTP on localhost. Does submission reuse the source's stored credential, or take its own?
- Sent duplication. Gmail writes to Sent server-side; most IMAP providers do not. Per-provider flag, or detect after the fact?
- Surface. Does submission belong in
serve and MCP, or start as a CLI-only verb where the human is unambiguously in the loop?
Happy to prototype this if the direction is welcome — starting with mode=draft and APPEND only, which needs no new credentials and no SMTP, and covers the reply-from-archive case on its own.
Problem
msgvault already holds everything needed to send a correct reply, except the ability to send it.
For every synced account it has the credentials and a live connection, the full RFC822 source in
message_raw, the thread structure inconversationsandmessages.reply_to_message_id, the recipient set inmessage_recipients, and the sending identity inaccount_identities.Anything that wants to act on archived mail has to rebuild all of that somewhere else: a second credential store, a second IMAP/SMTP connection to the same servers, and its own re-derivation of
In-Reply-ToandReferencesfrom raw source msgvault has already parsed. That duplication is the whole cost of every "email agent" integration built on top of an archive, and it is avoidable here.Proposal
An outbound path that composes from the archive and emits through the account the message belongs to.
Endpoints (mirrored in the MCP server and the CLI):
POST /api/v1/messages/{id}/reply— build a reply to an archived message. Server setsIn-Reply-To, the fullReferenceschain,Re:subject normalisation, and the recipient set for reply or reply-all.Fromresolves throughaccount_identitiesfor the source that message arrived on.POST /api/v1/messages/{id}/forward— same, preserving attachments fromattachmentsrather than re-uploading them.POST /api/v1/send— fresh compose against a named source.Two modes, draft first:
mode=draft(default) does an IMAPAPPENDto the account's Drafts mailbox with\Draft, and returns the resulting message id. The draft syncs to every client on that account, so review happens wherever the user already reads mail.mode=sendperforms SMTP submission, thenAPPENDs to Sent for providers that do not do it server-side.Coherence: on success the outbound message is recorded in the store immediately, rather than waiting for the next sync poll to discover it. Without this, anything acting through the API sees its own writes disappear for one poll interval.
Why this belongs here rather than in a separate tool
internal/deletion/already writes back to the source:executor.godeletes messages from the provider with manifests, batching and retry. So outbound is not a new class of behaviour for the project, and the safety machinery for "act on the real mailbox" already exists and has a shape worth reusing.Everything else about a correct reply — identity selection, threading headers, attachment bytes, the connection itself — is already implemented for sync. Submission is the missing verb, not a missing subsystem.
Scope and safety
The README's pitch is a complete local copy where "everything runs offline" and "no network access required". Submission should not weaken that, so:
mode=sendgated separately frommode=draft, so an install can allow draft creation while making autonomous sending impossible rather than merely discouraged — the same posture the deletion path takes toward destructive action.Open questions
serveand MCP, or start as a CLI-only verb where the human is unambiguously in the loop?Happy to prototype this if the direction is welcome — starting with
mode=draftandAPPENDonly, which needs no new credentials and no SMTP, and covers the reply-from-archive case on its own.