Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/email-artifact-message-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"miniflare": patch
---

Generate and use production-style Message-IDs for local email artifacts

Locally sent emails and replies now use generated Message-IDs - which are 36 alphanumeric characters - consistently in returned results, raw MIME headers, Local Explorer records, and stored artifact filenames. User-provided `Message-ID` headers are replaced by the generated ID.

For example, sending an email from `sender@example.com` may return `<AbCdEfGhIjKlMnOpQrStUvWxYz0123456789@example.com>`. The raw email uses that same value for its `Message-ID` header, the Local Explorer exposes the same ID, and the stored artifact is named `AbCdEfGhIjKlMnOpQrStUvWxYz0123456789@example.com.eml`.

Similarly, a reply containing `Message-ID: <custom@example.com>` is stored and returned with a newly generated ID instead. This mirrors production behavior and prevents the supplied ID from becoming the local artifact key.
7 changes: 7 additions & 0 deletions .changeset/email-reply-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": minor
---

Support `EmailReplyMessageBuilder` when replying from local email handlers

Builder replies now generate the recipient, threading headers, and a production-style Message-ID automatically. Raw `EmailMessage` replies also use a generated production-style Message-ID; user-provided Message-ID headers are rejected in favor of the generated ID.
35 changes: 35 additions & 0 deletions .changeset/email-test-harness-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"miniflare": minor
"wrangler": minor
---

Include a chronological list of handler events in email test harness results, so programmatic local email tests can assert the order in which messages are received, forwarded, replied to, or rejected.

```ts
const result = await server.getWorker().email({
from: "sender@example.com",
to: "inbox@example.com",
raw: [
"From: Sender <sender@example.com>",
"To: Inbox <inbox@example.com>",
"Message-ID: <test@example.com>",
"Subject: Test email",
"",
"Hello from the test harness",
].join("\r\n"),
});

expect(result.events).toEqual([
{ type: "received", timestamp: expect.any(String) },
{
type: "forward",
timestamp: expect.any(String),
messageId: expect.any(String),
},
{
type: "reply",
timestamp: expect.any(String),
messageId: expect.any(String),
},
]);
```
45 changes: 45 additions & 0 deletions .changeset/local-explorer-email-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
"miniflare": minor
---

Capture locally sent and received emails, along with forwarding and reply activity and metadata, for inspection through the Local Explorer email API.

Miniflare now captures locally sent and received emails, including forwarding, reply, rejection, and exception activity. The following endpoints are available below `/cdn-cgi/local/explorer/api` while `wrangler dev` is running:

- `POST /local/email/routing/send?worker=<name>` sends a test email to a Worker's `email()` handler.
- `GET /local/email/routing?worker=<name>` lists emails received by a Worker.
- `GET /local/email/routing?email_id=<message-id>&worker=<name>` returns a received email and its handler activity.
- `GET /local/email/sending?worker=<name>` lists emails sent through a Worker's `send_email` bindings.
- `GET /local/email/sending?email_id=<message-id>&worker=<name>` returns a sent email.

For example, send and then inspect a test email against a Worker named `my-worker`:

```sh
curl -X POST \
"http://localhost:8787/cdn-cgi/local/explorer/api/local/email/routing/send?worker=my-worker" \
-H "Content-Type: application/json" \
--data '{
"from": "sender@example.com",
"to": ["inbox@example.com"],
"subject": "Local test",
"text": "Hello from Local Explorer"
}'

curl \
"http://localhost:8787/cdn-cgi/local/explorer/api/local/email/routing?worker=my-worker"
```

List endpoints support `per_page` and opaque `cursor` query parameters. File paths logged by the `send_email` binding are asynchronous debugging artifacts and should not be used to synchronize after `send()` resolves. Email handler exceptions are logged when structured local delivery reports an exception outcome.

When email content exceeds the local storage row budget of approximately 2 MB, the email is delivered in full but the Local Explorer capture is truncated to fit. Detail responses identify each truncated sent email, received email, or reply in the top-level `messages` array with warning code `10604`; for example:

```json
{
"messages": [
{
"code": 10604,
"message": "Displayed received email content was truncated during local capture. The complete message was still delivered to the Worker."
}
]
}
```
2 changes: 1 addition & 1 deletion packages/miniflare/openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default defineConfig({
// Keep these paths in sync with the prettier inputs in package.json (generate:types script)
input: "src/workers/local-explorer/openapi.local.json",
output: "src/workers/local-explorer/generated",
plugins: ["@hey-api/typescript", "zod"],
plugins: ["@hey-api/typescript", { name: "zod", compatibilityVersion: 4 }],
parser: {
patch: {
schemas: {
Expand Down
34 changes: 34 additions & 0 deletions packages/miniflare/scripts/email-openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from "zod";
import {
zEmailAttachment,
zEmailBase,
zEmailHandlerEvent,
zEmailHandlerForward,
zEmailHandlerReplyApi,
zEmailRoutingDetail,
zEmailRoutingItem,
zEmailSendingDetail,
zEmailSendingItem,
zEmailSendRequest,
} from "../src/workers/email/contracts";

function toOpenApiSchema(schema: z.ZodType): Record<string, unknown> {
const { $schema: _$schema, ...openApiSchema } = z.toJSONSchema(schema, {
target: "openapi-3.0",
unrepresentable: "any",
});
return openApiSchema;
}

export const EMAIL_OPENAPI_SCHEMAS = {
"email_handler-event": toOpenApiSchema(zEmailHandlerEvent),
"email_handler-forward": toOpenApiSchema(zEmailHandlerForward),
"email_handler-reply": toOpenApiSchema(zEmailHandlerReplyApi),
email_base: toOpenApiSchema(zEmailBase),
"email_routing-item": toOpenApiSchema(zEmailRoutingItem),
"email_routing-detail": toOpenApiSchema(zEmailRoutingDetail),
"email_send-request": toOpenApiSchema(zEmailSendRequest),
email_attachment: toOpenApiSchema(zEmailAttachment),
"email_sending-item": toOpenApiSchema(zEmailSendingItem),
"email_sending-detail": toOpenApiSchema(zEmailSendingDetail),
};
Loading
Loading