Skip to content

[miniflare] Add backend resources for email capture and storage - #15064

Open
tpmmorris wants to merge 13 commits into
cloudflare:mainfrom
tpmmorris:main
Open

[miniflare] Add backend resources for email capture and storage#15064
tpmmorris wants to merge 13 commits into
cloudflare:mainfrom
tpmmorris:main

Conversation

@tpmmorris

@tpmmorris tpmmorris commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #13648
Add backend resources for Email interaction within Local Explorer

Adds storage and capture methods for emails sent from/received by a worker using durable objects for storage (in line with the new 'Observability' tab), and cdn-cgi endpoints to mimic the sending of an email to a worker. Also records actions taken by the email() handler (received, forwarded, replied, rejected, unhandled), so that they can be mapped and displayed in local explorer in a similar manner as the dash. The Email result interface has been updated to also include a list of events.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: no changes are user-facing.

Open in Devin Review

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e5d3aaf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod
workers-devprod requested review from a team and petebacondarwin and removed request for a team August 6, 2026 17:21
@workers-devprod

workers-devprod commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/local-email-capture.md: [@cloudflare/wrangler]
  • packages/miniflare/openapi-ts.config.ts: [@cloudflare/wrangler]
  • packages/miniflare/scripts/openapi-filter-config.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/temp-file.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/types.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/artifacts.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/capture.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/message-id.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/send_email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/storage.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/validate.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/explorer.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/types.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/openapi.local.json: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/resources/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/route-names.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/email/artifacts.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/email/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/email.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/index.spec.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/createTestHarness.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/get-platform-proxy.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/multiworker-dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/test-harness.ts: [@cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +323 to +331
// Decode MIME "encoded-word" headers (e.g. `=?utf-8?B?...?=`) in each reply's
// display text so the explorer shows readable subjects.
const decoded = {
...email,
replies: email.replies.map((reply) => ({
...reply,
raw: decodeWords(reply.raw),
})),
};

@devin-ai-integration devin-ai-integration Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Reply message shown in the email inspector can be altered from what was actually sent

The full raw text of each reply is run through a header-decoding step (decodeWords(reply.raw) at packages/miniflare/src/workers/local-explorer/resources/email.ts:328) before being returned, so the reply a developer inspects can differ from the message the worker actually produced.

Impact: A reply whose body happens to contain encoded-word-looking text is displayed altered, and the accompanying base64 copy of the same reply no longer matches what is shown.

Why decoding the whole MIME blob is wrong here

decodeWords from postal-mime decodes RFC 2047 =?charset?enc?...?= sequences anywhere in the string it is given. Here it is applied to reply.raw, which is the complete MIME message (headers and body), not just a header value. Any literal encoded-word sequence appearing in the body — e.g. a quoted example, a forwarded header, or test fixture text — will be rewritten.

The API contract also becomes self-inconsistent: email_handler-reply.raw is documented as "Raw MIME content of the reply" and rawBase64 as the "Lossless base64 representation of the reply MIME" (see packages/miniflare/src/workers/local-explorer/openapi.local.json), but only raw is transformed here — rawBase64 is passed through untouched from the stored record, so the two fields can disagree for the same reply.

If the goal is readable subjects in the UI, the decoding should be applied to individual parsed header values for display, leaving raw byte-faithful.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/miniflare/src/workers/core/email.ts
rawBase64: bytesToBase64(rawEmailBuffer),
});

this.ctx.waitUntil(

@devin-ai-integration devin-ai-integration Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Emails sent just before the dev session shuts down are never written to disk or logged

The on-disk copy of a sent email and its log line are queued to run in the background (this.ctx.waitUntil(...) at packages/miniflare/src/workers/email/send_email.worker.ts:370 and :451) instead of being finished before the send call returns, so a script that sends an email and then immediately shuts the local dev session down loses the saved message entirely.

Impact: Short-lived usages (for example sending through getPlatformProxy() and then disposing) no longer reliably produce the .eml/text/HTML/attachment files or the "send_email binding called..." log they used to.

Why the deferred work can be dropped

Before this change send() awaited every storeTempFile() call and logged before resolving, so by the time the caller's await env.SEND_EMAIL.send(...) returned the files existed. Now both branches resolve immediately after the in-workerd capture, deferring the loopback /core/store-temp-file writes and logging to ctx.waitUntil.

Miniflare#dispose() aborts, stops the loopback server and tears down workerd before drainEmailArtifactManager() runs (packages/miniflare/src/index.ts:3490-3496); drain() only awaits operations that already reached the Node side (packages/miniflare/src/plugins/email/artifacts.ts:86-89), so waitUntil work that has not yet issued its loopback request is simply discarded.

The test updates in this PR reflect the new asynchrony (the miniflare email specs now poll with vi.waitFor, and the get-platform-proxy e2e no longer asserts on the file), but callers that dispose right after sending have no way to wait.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/miniflare/src/plugins/email/artifacts.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15064

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15064

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15064

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15064

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15064

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15064

miniflare

npm i https://pkg.pr.new/miniflare@15064

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15064

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15064

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15064

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15064

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@15064

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15064

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15064

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15064

wrangler

npm i https://pkg.pr.new/wrangler@15064

commit: e5d3aaf

devin-ai-integration[bot]

This comment was marked as resolved.

@tpmmorris
tpmmorris force-pushed the main branch 2 times, most recently from 7fccbb7 to 494e214 Compare August 7, 2026 12:57
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

throw new TypeError("Sent email record does not match its table");
}
const sentEmail = email;
const artifacts: EmailArtifact[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Saved copies of large outgoing emails are never cleaned up and unrelated files are deleted instead

When an old sent email is dropped from the local history, the wrong on-disk copies are identified for deletion (sentEmail.raw !== undefined at packages/miniflare/src/workers/email/email-store.ts:241) for any message that was recorded in chunks, so its saved .eml copy is left behind forever.

Impact: Temporary email files for larger outgoing messages accumulate in the project/system temp folders for the whole dev session instead of being cleaned up.

Why chunked sent records never match the raw-file branch

reportSentEmail in packages/miniflare/src/workers/email/send_email.worker.ts:163-181 streams any message whose base64 body exceeds 64 KB (i.e. raw body > ~48 KB). That path destructures raw and rawBase64 out of the record before calling store.beginSent(metadata), and finishSent re-adds only rawBase64 (packages/miniflare/src/workers/email/email-store.ts:485-495). The persisted record therefore has raw === undefined.

On eviction, #insert re-parses the evicted row and calls getArtifacts("sent", ...). Because raw is missing, it takes the else branch and reports email-text/email-html/email-attachment artifacts (paths that were never written for an EmailMessage send) instead of the email/<id>.eml artifact that send() actually wrote via storeTempFile(..., "email", id, id) (packages/miniflare/src/workers/email/send_email.worker.ts:430-438). The unlink of the non-existent paths is ignored, and the real .eml is leaked.

Suggested change
const artifacts: EmailArtifact[] = [];
if (sentEmail.raw !== undefined || sentEmail.rawBase64 !== undefined) {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +198 to +200
const headers: string[] = [`From: ${body.from}`, `To: ${body.to.join(", ")}`];
if (body.cc?.length) {
headers.push(`Cc: ${body.cc.join(", ")}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Blind-copy recipients on a test email are silently dropped

Blind-copy addresses supplied when sending a test email are never written into the composed message (only Cc is added at packages/miniflare/src/workers/local-explorer/resources/email.ts:198-200), so they silently disappear even though the API documents that they will appear.

Impact: Developers testing an email handler that inspects blind-copy recipients see none, with no error or warning.

Contract mismatch between the documented endpoint and the composed MIME

The /email/routing/send OpenAPI description states: "Only the first to address is used as the envelope recipient; any other to/cc/bcc addresses appear only in the composed MIME headers" (packages/miniflare/src/workers/local-explorer/openapi.local.json, email-send-routing). buildMimeMessage emits From, To, Cc, Reply-To, Subject, Message-ID, Date and custom headers, but never a Bcc header. body.bcc is only read for control-character validation (packages/miniflare/src/workers/local-explorer/resources/email.ts:168).

Suggested change
const headers: string[] = [`From: ${body.from}`, `To: ${body.to.join(", ")}`];
if (body.cc?.length) {
headers.push(`Cc: ${body.cc.join(", ")}`);
if (body.cc?.length) {
headers.push(`Cc: ${body.cc.join(", ")}`);
}
if (body.bcc?.length) {
headers.push(`Bcc: ${body.bcc.join(", ")}`);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants