Skip to content

Commit 930ebb2

Browse files
penalosaCarmenPopoviciupetebacondarwin
authored
Email Workers local dev and send_email binding (#8375)
* feat: Add support for Email Workers in local dev * Update fixtures/email-worker/src/index.ts * Update fixtures/email-worker/src/index.ts * Update fixtures/email-worker/src/index.ts * Update fixtures/email-worker/src/index.ts * Update packages/miniflare/src/plugins/core/index.ts * Cleanup & address comments * fix Miniflare tests * more tests * fixups * fix logging * fix lint * fix e2e * make e2e tests a bit more reliable * Add comment about logRequests * Cleanup fixture * Add namespacing * Undo weird typing stuff * cleanup * add deprecation warning for mf/scheduled * remove test * more comments * build * fix lockfile * tweak test for Windows * update workers-types dependency in email-worker fixture * fixups --------- Co-authored-by: Carmen Popoviciu <cpopoviciu@cloudflare.com> Co-authored-by: Peter Bacon Darwin <pbacondarwin@cloudflare.com>
1 parent f566680 commit 930ebb2

32 files changed

+2285
-77
lines changed

.changeset/neat-planes-call.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
Add support for email local dev and send_email binding

fixtures/email-worker/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "email-worker",
3+
"private": true,
4+
"scripts": {
5+
"deploy": "wrangler deploy",
6+
"start": "wrangler dev"
7+
},
8+
"devDependencies": {
9+
"@cloudflare/workers-types": "^4.20250317.0",
10+
"@types/mimetext": "^2.0.4",
11+
"mimetext": "^3.0.27",
12+
"wrangler": "workspace:*"
13+
},
14+
"volta": {
15+
"extends": "../../package.json"
16+
}
17+
}

fixtures/email-worker/src/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { EmailMessage } from "cloudflare:email";
2+
import { env, WorkerEntrypoint } from "cloudflare:workers";
3+
import { createMimeMessage } from "mimetext";
4+
5+
export default class extends WorkerEntrypoint<Env> {
6+
async fetch(request: Request): Promise<Response> {
7+
const url = new URL(request.url);
8+
if (url.pathname === "/error") throw new Error("Hello Error");
9+
10+
if (url.pathname === "/send") {
11+
const msg = createMimeMessage();
12+
msg.setSender({ name: "GPT-4", addr: "sender@penalosa.cloud" });
13+
msg.setRecipient("else@example.com");
14+
msg.setSubject("An email generated in a Worker");
15+
msg.addMessage({
16+
contentType: "text/plain",
17+
data: "Congratulations, you just sent an email from a Worker.",
18+
});
19+
const m = new EmailMessage(
20+
"sender@penalosa.cloud",
21+
"else@example.com",
22+
msg.asRaw()
23+
);
24+
await this.env.LIST_SEND.send(m);
25+
}
26+
27+
return new Response("Hello World!");
28+
}
29+
async email(message: ForwardableEmailMessage) {
30+
console.log("hello");
31+
const msg = createMimeMessage();
32+
msg.setHeader("In-Reply-To", message.headers.get("Message-ID")!);
33+
msg.setSender(message.to);
34+
msg.setRecipient(message.from);
35+
msg.setSubject("An email generated in a Worker");
36+
msg.addMessage({
37+
contentType: "text/plain",
38+
data: `Congratulations, you just sent an email from a Worker.`,
39+
});
40+
41+
const m = new EmailMessage(message.to, message.from, msg.asRaw());
42+
await message.forward(
43+
"samuel@macleod.space",
44+
new Headers({ hello: "world" })
45+
);
46+
await message.reply(m);
47+
}
48+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2021",
4+
"lib": ["es2021"],
5+
"module": "es2022",
6+
"moduleResolution": "bundler",
7+
"types": ["@cloudflare/workers-types"],
8+
"noEmit": true,
9+
"isolatedModules": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"skipLibCheck": true
13+
}
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Generated by Wrangler by running `wrangler types --x-include-runtime`
2+
3+
interface Env extends Env {
4+
UNBOUND_SEND: SendEmail;
5+
SPECIFIC_SEND: SendEmail;
6+
LIST_SEND: SendEmail;
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name = "email-to-rss"
2+
main = "src/index.ts"
3+
compatibility_date = "2025-02-14"
4+
5+
compatibility_flags = [ "nodejs_compat" ]
6+
7+
send_email = [
8+
{name = "UNBOUND_SEND"},
9+
{name = "SPECIFIC_SEND", destination_address = "something@example.com"},
10+
{name = "LIST_SEND", allowed_destination_addresses = ["something@example.com", "else@example.com"]},
11+
]

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"@cloudflare/component-listbox@1.10.6": "patches/@cloudflare__component-listbox@1.10.6.patch",
8282
"pg@8.11.3": "patches/pg@8.11.3.patch",
8383
"toucan-js@3.3.1": "patches/toucan-js@3.3.1.patch",
84-
"toucan-js@4.0.0": "patches/toucan-js@4.0.0.patch"
84+
"toucan-js@4.0.0": "patches/toucan-js@4.0.0.patch",
85+
"postal-mime": "patches/postal-mime.patch"
8586
}
8687
}
8788
}

packages/miniflare/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@
8888
"http-cache-semantics": "^4.1.0",
8989
"kleur": "^4.1.5",
9090
"mime": "^3.0.0",
91+
"postal-mime": "^2.4.3",
9192
"pretty-bytes": "^6.0.0",
9293
"rimraf": "catalog:default",
9394
"source-map": "^0.6.1",
95+
"ts-dedent": "^2.2.0",
9496
"typescript": "catalog:default",
9597
"which": "^2.0.2"
9698
},

packages/miniflare/src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from "assert";
22
import crypto from "crypto";
33
import { Abortable } from "events";
44
import fs from "fs";
5+
import { mkdir, writeFile } from "fs/promises";
56
import http from "http";
67
import net from "net";
78
import os from "os";
@@ -982,6 +983,17 @@ export class Miniflare {
982983
if (!colors$.enabled) message = stripAnsi(message);
983984
this.#log.logWithLevel(logLevel, message);
984985
response = new Response(null, { status: 204 });
986+
} else if (url.pathname === "/core/store-temp-file") {
987+
const prefix = url.searchParams.get("prefix");
988+
const folder = prefix ? `files/${prefix}` : "files";
989+
await mkdir(path.join(this.#tmpPath, folder), { recursive: true });
990+
const filePath = path.join(
991+
this.#tmpPath,
992+
folder,
993+
`${crypto.randomUUID()}.${url.searchParams.get("extension") ?? "txt"}`
994+
);
995+
await writeFile(filePath, await request.text());
996+
response = new Response(filePath, { status: 200 });
985997
}
986998
} catch (e: any) {
987999
this.#log.error(e);
@@ -1547,11 +1559,13 @@ export class Miniflare {
15471559
const ready = initial ? "Ready" : "Updated and ready";
15481560

15491561
const urlSafeHost = getURLSafeHost(configuredHost);
1550-
this.#log.info(
1551-
`${ready} on ${secure ? "https" : "http"}://${urlSafeHost}:${entryPort}`
1552-
);
1562+
if (this.#sharedOpts.core.logRequests) {
1563+
this.#log.info(
1564+
`${ready} on ${secure ? "https" : "http"}://${urlSafeHost}:${entryPort}`
1565+
);
1566+
}
15531567

1554-
if (initial) {
1568+
if (initial && this.#sharedOpts.core.logRequests) {
15551569
const hosts: string[] = [];
15561570
if (configuredHost === "::" || configuredHost === "*") {
15571571
hosts.push("localhost");

packages/miniflare/src/plugins/core/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ export const CoreSharedOptionsSchema = z.object({
210210
unsafeModuleFallbackService: ServiceFetchSchema.optional(),
211211
// Keep blobs when deleting/overwriting keys, required for stacked storage
212212
unsafeStickyBlobs: z.boolean().optional(),
213+
// Enable directly triggering user Worker handlers with paths like `/cdn-cgi/handler/scheduled`
214+
unsafeTriggerHandlers: z.boolean().optional(),
215+
// Enable logging requests
216+
logRequests: z.boolean().default(true),
213217
});
214218

215219
export const CORE_PLUGIN_NAME = "core";
@@ -746,6 +750,14 @@ export function getGlobalServices({
746750
const serviceEntryBindings: Worker_Binding[] = [
747751
WORKER_BINDING_SERVICE_LOOPBACK, // For converting stack-traces to pretty-error pages
748752
{ name: CoreBindings.JSON_ROUTES, json: JSON.stringify(routes) },
753+
{
754+
name: CoreBindings.TRIGGER_HANDLERS,
755+
json: JSON.stringify(!!sharedOptions.unsafeTriggerHandlers),
756+
},
757+
{
758+
name: CoreBindings.LOG_REQUESTS,
759+
json: JSON.stringify(!!sharedOptions.logRequests),
760+
},
749761
{ name: CoreBindings.JSON_CF_BLOB, json: JSON.stringify(sharedOptions.cf) },
750762
{ name: CoreBindings.JSON_LOG_LEVEL, json: JSON.stringify(log.level) },
751763
{
@@ -795,13 +807,8 @@ export function getGlobalServices({
795807
name: SERVICE_ENTRY,
796808
worker: {
797809
modules: [{ name: "entry.worker.js", esModule: SCRIPT_ENTRY() }],
798-
compatibilityDate: "2023-04-04",
799-
compatibilityFlags: [
800-
"nodejs_compat",
801-
"service_binding_extra_handlers",
802-
"brotli_content_encoding",
803-
"rpc",
804-
],
810+
compatibilityDate: "2025-03-17",
811+
compatibilityFlags: ["nodejs_compat", "service_binding_extra_handlers"],
805812
bindings: serviceEntryBindings,
806813
durableObjectNamespaces: [
807814
{

0 commit comments

Comments
 (0)