Skip to content

Commit 693ca29

Browse files
authored
[miniflare] Add backend resources for email capture and storage (#15064)
1 parent f830836 commit 693ca29

53 files changed

Lines changed: 10316 additions & 1325 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Generate and use production-style Message-IDs for local email artifacts
6+
7+
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.
8+
9+
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`.
10+
11+
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.

.changeset/email-reply-builder.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Support `EmailReplyMessageBuilder` when replying from local email handlers
6+
7+
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.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
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.
7+
8+
```ts
9+
const result = await server.getWorker().email({
10+
from: "sender@example.com",
11+
to: "inbox@example.com",
12+
raw: [
13+
"From: Sender <sender@example.com>",
14+
"To: Inbox <inbox@example.com>",
15+
"Message-ID: <test@example.com>",
16+
"Subject: Test email",
17+
"",
18+
"Hello from the test harness",
19+
].join("\r\n"),
20+
});
21+
22+
expect(result.events).toEqual([
23+
{ type: "received", timestamp: expect.any(String) },
24+
{
25+
type: "forward",
26+
timestamp: expect.any(String),
27+
messageId: expect.any(String),
28+
},
29+
{
30+
type: "reply",
31+
timestamp: expect.any(String),
32+
messageId: expect.any(String),
33+
},
34+
]);
35+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Capture locally sent and received emails, along with forwarding and reply activity and metadata, for inspection through the Local Explorer email API.
6+
7+
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:
8+
9+
- `POST /local/email/routing/send?worker=<name>` sends a test email to a Worker's `email()` handler.
10+
- `GET /local/email/routing?worker=<name>` lists emails received by a Worker.
11+
- `GET /local/email/routing?email_id=<message-id>&worker=<name>` returns a received email and its handler activity.
12+
- `GET /local/email/sending?worker=<name>` lists emails sent through a Worker's `send_email` bindings.
13+
- `GET /local/email/sending?email_id=<message-id>&worker=<name>` returns a sent email.
14+
15+
For example, send and then inspect a test email against a Worker named `my-worker`:
16+
17+
```sh
18+
curl -X POST \
19+
"http://localhost:8787/cdn-cgi/local/explorer/api/local/email/routing/send?worker=my-worker" \
20+
-H "Content-Type: application/json" \
21+
--data '{
22+
"from": "sender@example.com",
23+
"to": ["inbox@example.com"],
24+
"subject": "Local test",
25+
"text": "Hello from Local Explorer"
26+
}'
27+
28+
curl \
29+
"http://localhost:8787/cdn-cgi/local/explorer/api/local/email/routing?worker=my-worker"
30+
```
31+
32+
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.
33+
34+
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:
35+
36+
```json
37+
{
38+
"messages": [
39+
{
40+
"code": 10604,
41+
"message": "Displayed received email content was truncated during local capture. The complete message was still delivered to the Worker."
42+
}
43+
]
44+
}
45+
```

packages/miniflare/openapi-ts.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default defineConfig({
44
// Keep these paths in sync with the prettier inputs in package.json (generate:types script)
55
input: "src/workers/local-explorer/openapi.local.json",
66
output: "src/workers/local-explorer/generated",
7-
plugins: ["@hey-api/typescript", "zod"],
7+
plugins: ["@hey-api/typescript", { name: "zod", compatibilityVersion: 4 }],
88
parser: {
99
patch: {
1010
schemas: {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { z } from "zod";
2+
import {
3+
zEmailAttachment,
4+
zEmailBase,
5+
zEmailHandlerEvent,
6+
zEmailHandlerForward,
7+
zEmailHandlerReplyApi,
8+
zEmailRoutingDetail,
9+
zEmailRoutingItem,
10+
zEmailSendingDetail,
11+
zEmailSendingItem,
12+
zEmailSendRequest,
13+
} from "../src/workers/email/contracts";
14+
15+
function toOpenApiSchema(schema: z.ZodType): Record<string, unknown> {
16+
const { $schema: _$schema, ...openApiSchema } = z.toJSONSchema(schema, {
17+
target: "openapi-3.0",
18+
unrepresentable: "any",
19+
});
20+
return openApiSchema;
21+
}
22+
23+
export const EMAIL_OPENAPI_SCHEMAS = {
24+
"email_handler-event": toOpenApiSchema(zEmailHandlerEvent),
25+
"email_handler-forward": toOpenApiSchema(zEmailHandlerForward),
26+
"email_handler-reply": toOpenApiSchema(zEmailHandlerReplyApi),
27+
email_base: toOpenApiSchema(zEmailBase),
28+
"email_routing-item": toOpenApiSchema(zEmailRoutingItem),
29+
"email_routing-detail": toOpenApiSchema(zEmailRoutingDetail),
30+
"email_send-request": toOpenApiSchema(zEmailSendRequest),
31+
email_attachment: toOpenApiSchema(zEmailAttachment),
32+
"email_sending-item": toOpenApiSchema(zEmailSendingItem),
33+
"email_sending-detail": toOpenApiSchema(zEmailSendingDetail),
34+
};

0 commit comments

Comments
 (0)