Skip to content
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

Postmaster MCP follows Semantic Versioning for stable releases. Every stable release should update `VERSION`, this changelog, and publish an immutable Git tag/release named `vX.Y.Z`.

## 9.4.2 - 2026-08-20

### Added
- Explicit outbound follow-up tools `follow_up_email` and `create_follow_up_draft`, mirroring the existing reply APIs while keeping inbound replies and outbound follow-ups as separate safety semantics.
- Shared thread-recipient resolution for reply/follow-up mode. Inbound replies prefer a valid `Reply-To` and otherwise use `From`; outbound follow-ups reuse the original visible `To` and preserve the original visible `Cc` by default.
- Direction guards: `reply_email` rejects messages clearly sent by the selected sender account and tells callers to use `follow_up_email`; `follow_up_email` rejects inbound messages and points callers to `reply_email`.
- Regression coverage for recipient direction, sender/identity filtering, case-insensitive deduplication, Bcc non-disclosure, zero-recipient failures, threading headers, subject normalization, recipient authorization, drafts, tracked visible headers, clean Sent copies and attachment-byte identity.

### Changed
- Sender-owned identities are filtered from resolved `To`/`Cc` before authorization or delivery. The primary sender plus account-configured email identities are compared case-insensitively, and duplicate external recipients are removed while preserving a stable order.
- Thread subjects now normalize repeated leading `Re:` prefixes to one `Re:`. `In-Reply-To` targets the selected message's `Message-ID`, while `References` are preserved and extended without duplicating that selected ID.
- Follow-ups use the same existing outbound path as replies/sends. Tracked follow-ups therefore retain v9.4 individualized recipient MIME, visible `To`/`Cc`, clean archived Sent MIME, original URLs and identical attachment bytes without introducing a second tracking pipeline.

### Fixed
- Calling `reply_email` on an outbound/Sent message can no longer select the sender's own `From` address and create a self-reply.
- Outbound follow-ups no longer authorize or validate the sender address in place of the original external recipients.
- Original Bcc recipients are never rediscovered, inferred or re-exposed by follow-up resolution.
- A follow-up with no external recipient left after sender/identity filtering fails before any SMTP delivery.

### Compatibility / deployment
- Existing `reply_email` and `create_reply_draft` signatures remain compatible; their safe direction semantics are now explicit.
- No recipient-policy rule, tracking schema, environment variable, volume, port or public callback path changes are required.
- `postmaster-mcp.yml` remains unchanged. Deployments using `POSTMASTER_VERSION=latest` with update checks enabled can select v9.4.2 on the normal restart/redeploy after the stable release is published.
- No Cloudflare Access change is required for v9.4.2.

## 9.4.1 - 2026-08-20

### Added
Expand Down Expand Up @@ -114,4 +139,4 @@ Postmaster MCP follows Semantic Versioning for stable releases. Every stable rel
- CI coverage for runtime import, bootstrap, model provisioning, MIME regressions and knowledge operations.

### Changed
- Public project naming and configuration became provider-agnostic.
- Public project naming and configuration became provider-agnostic.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Postmaster MCP
+-- IMAP / SMTP mail operations
+-- encrypted multi-account storage
+-- recipient safety policy
+-- drafts, replies and attachments
+-- drafts, replies, follow-ups and attachments
+-- open analytics / AMP support
+-- persistent task registry
+-- memories / skills / project context
Expand Down Expand Up @@ -344,7 +344,7 @@ See `docs/context-model.md` for details.

# Email and MIME handling

Postmaster MCP supports plain text, HTML, attachments, drafts, replies and forwarded messages.
Postmaster MCP supports plain text, HTML, attachments, drafts, replies, follow-ups and forwarded messages.

A normal multipart message may contain:

Expand Down Expand Up @@ -386,6 +386,25 @@ Credentials remain server-side and are not returned through MCP tools.

---

# Reply vs follow-up (v9.4.2)

Threaded mail actions deliberately separate inbound replies from outbound follow-ups:

```text
reply_email -> reply to an inbound message
create_reply_draft -> draft a reply to an inbound message
follow_up_email -> follow up an outbound/Sent message
create_follow_up_draft -> draft a follow-up to an outbound/Sent message
```

For inbound messages, `reply_email` prefers a valid `Reply-To` and otherwise uses `From`. Calling it on a message clearly sent by the selected account is rejected with guidance to use `follow_up_email`, preventing self-replies.

For outbound/Sent messages, `follow_up_email` reuses the original visible `To` and preserves the original visible `Cc` by default. The sender account and its configured email identities are removed case-insensitively, duplicates are removed while preserving order, and at least one external `To` recipient must remain. Original Bcc recipients are never rediscovered, inferred or exposed. Calling follow-up on an inbound message is rejected.

Both modes preserve normal threading: one normalized `Re:` prefix, `In-Reply-To` pointing to the selected message's `Message-ID`, and `References` preserved/extended. Follow-up sending uses the same recipient-authorization, tracking, individualized-delivery and clean-Sent pipeline as existing sends/replies; it does not introduce a parallel tracking implementation.

---

# Recipient safety

Sending is protected by an authorization policy.
Expand Down Expand Up @@ -436,15 +455,15 @@ The server persists the task state; the AI client performs the reasoning and exp

# Open tracking and AMP

Open tracking can be configured per account and overridden for individual sends/replies.
Open tracking can be configured per account and overridden for individual sends/replies/follow-ups.

```text
track_opens: null -> account default
track_opens: true -> enable for this message
track_opens: false -> disable for this message
```

Tracked multi-recipient delivery uses a distinct token per recipient while preserving visible `To` / `Cc` headers. `Bcc` remains hidden. Replies preserve normal threading headers.
Tracked multi-recipient delivery uses a distinct token per recipient while preserving visible `To` / `Cc` headers. `Bcc` remains hidden. Replies and follow-ups preserve normal threading headers.

Open events are telemetry, not proof that a human read a message. Mail scanners, proxies, prefetching and image blocking can affect observations.

Expand Down Expand Up @@ -685,3 +704,9 @@ Cloudflare Access is external to the container. Keep the existing public bypasse
Do not expose `/mcp`, dashboard/admin/private APIs, mail/task/memory/skill/file-management endpoints or tracking analytics. The pre-existing v9.3 signed `/files/*` handoff remains a separate deployment-policy concern and is not added automatically as part of v9.4.

See `docs/LINK_TRACKING.md` for architecture, schema, Sent-clean behavior, analytics and the live Cloudflare preflight.

# Explicit reply/follow-up semantics (v9.4.2)

v9.4.2 prevents outbound messages from accidentally being replied back to the sender account. `reply_email` / `create_reply_draft` are inbound-only semantics, while `follow_up_email` / `create_follow_up_draft` operate on outbound/Sent messages and reuse the original visible recipients after sender-identity filtering. Source Bcc is never recovered.

Tracked follow-ups reuse the v9.4 dual-MIME pipeline: recipient copies may contain the configured open/link instrumentation, while archived Sent copies keep original URLs and omit active recipient pixel, click-tracking URLs and recipient AMP callbacks. Visible `To` / `Cc`, threading headers and attachment bytes remain consistent. No new environment variables, ports, volumes, callback paths or Portainer YAML changes are required.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9.4.1
9.4.2
59 changes: 59 additions & 0 deletions src/postmaster/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
from html import escape
from typing import Any

import uvicorn
from mcp.types import CallToolResult
Expand Down Expand Up @@ -38,13 +39,71 @@ def build_status():
status["link_tracking"] = True
status["sent_copy_tracking_sanitized"] = True
status["provider_qualitative_classification"] = True
status["explicit_reply_follow_up_modes"] = True
status["follow_up_email"] = True
status["follow_up_draft"] = True
return status

mcp.remove_tool("build_status")
mcp.add_tool(build_status, name="build_status")
_base.build_status = build_status


@mcp.tool()
def follow_up_email(
mailbox: str,
uid: str,
body: str = "",
cc: list[str] | None = None,
bcc: list[str] | None = None,
body_html: str | None = None,
attachments: list[dict[str, Any]] | None = None,
track_opens: bool | None = None,
campaign_id: str | None = None,
account_id: str | None = None,
):
"""
WRITE ACTION. Threaded follow-up to an outbound/Sent message from the selected account.

The original visible To/Cc recipients are reused after removing the sender account and
configured account identities. Original Bcc is never rediscovered or exposed. Inbound
messages are rejected and should use reply_email instead.

Tracking follows the same account-default/explicit override semantics and the same v9.4
recipient/Sent-clean pipeline as send_email and reply_email.
"""
return _base._safe_call(
mail_client(account_id).follow_up_email,
mailbox=mailbox, uid=uid, body=body, cc=cc, bcc=bcc,
body_html=body_html, attachments=attachments,
track_opens=track_opens, campaign_id=campaign_id,
)


@mcp.tool()
def create_follow_up_draft(
mailbox: str,
uid: str,
body: str = "",
cc: list[str] | None = None,
bcc: list[str] | None = None,
body_html: str | None = None,
attachments: list[dict[str, Any]] | None = None,
account_id: str | None = None,
):
"""
WRITE ACTION. Save a threaded follow-up draft for an outbound/Sent message.

The draft reuses the original visible To/Cc after sender/alias filtering, never recovers
original Bcc, and rejects inbound messages so reply/follow-up semantics stay explicit.
"""
return _base._safe_call(
mail_client(account_id).create_follow_up_draft,
mailbox=mailbox, uid=uid, body=body, cc=cc, bcc=bcc,
body_html=body_html, attachments=attachments,
)


@mcp.tool()
def get_stored_file_resource(file_id: str, transport: str = "auto") -> CallToolResult:
return stored_file_resource_result(_base.file_store(), file_id, transport)
Expand Down
Loading
Loading