-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Email Workers local development #21399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
e0b5df5
Email Workers local development
celso 40d3dd8
Update src/content/docs/email-routing/email-workers/local-development…
celso ce3c1a2
Update src/content/docs/email-routing/email-workers/local-development…
celso 2347699
Update src/content/docs/email-routing/email-workers/local-development…
celso 160128a
Update src/content/docs/email-routing/email-workers/local-development…
celso f3184c6
Update src/content/docs/email-routing/email-workers/local-development…
celso 1235570
preview link no longer required
celso 8441c2d
changelog entry
celso 114241b
changes required
celso 9a154dd
typo
celso 966a3ba
closes code section
celso fbc3ab6
Update src/content/docs/email-routing/email-workers/local-development…
celso 154ebf1
Update src/content/docs/email-routing/email-workers/local-development…
celso 7409620
Update src/content/docs/email-routing/email-workers/local-development…
celso a238063
Update src/content/docs/email-routing/email-workers/local-development…
celso c0f84a4
Update src/content/docs/email-routing/email-workers/local-development…
celso 3538283
Update src/content/docs/email-routing/email-workers/local-development…
celso 75de507
Update src/content/docs/email-routing/email-workers/local-development…
celso 32662f3
Update src/content/docs/email-routing/email-workers/local-development…
celso eb91077
Update src/content/docs/email-routing/email-workers/local-development…
celso 14621f6
Update src/content/docs/email-routing/email-workers/local-development…
celso c3bc5cc
update changelog
celso 3fb0a90
Merge branch 'celso/email-localdev' of github.com:cloudflare/cloudfla…
celso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
77 changes: 77 additions & 0 deletions
77
src/content/changelog/email-routing/2025-04-08-local-development.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| --- | ||
| title: Local development support for Email Workers | ||
| description: Developers can now use wrangler to do local development for Email Workers. | ||
| date: 2025-04-08T15:00:00Z | ||
| --- | ||
|
|
||
| Email Workers enables developers to programmatically take action on anything that hits their email inbox. If you're building with Email Workers, you can now test the behavior of an Email Worker script, receiving, replying and sending emails in your local environment using `wrangler dev`. | ||
|
|
||
| Below is an example that shows you how you can receive messages using the `email()` handler and parse them using [postal-mime](https://www.npmjs.com/package/postal-mime): | ||
|
|
||
| ```ts | ||
| import * as PostalMime from 'postal-mime'; | ||
|
|
||
| export default { | ||
| async email(message, env, ctx) { | ||
| const parser = new PostalMime.default(); | ||
| const rawEmail = new Response(message.raw); | ||
| const email = await parser.parse(await rawEmail.arrayBuffer()); | ||
| console.log(email); | ||
| }, | ||
| }; | ||
|
|
||
| Now when you run `npx wrangler dev`, wrangler will expose a local `/cdn-cgi/handler/email` endpoint that you can `POST` email messages to and trigger your Worker's `email()` handler: | ||
|
|
||
| ```bash | ||
| curl -X POST 'http://localhost:8787/cdn-cgi/handler/email' \ | ||
| --url-query '[email protected]' \ | ||
| --url-query '[email protected]' \ | ||
| --header 'Content-Type: application/json' \ | ||
| --data-raw 'Received: from smtp.example.com (127.0.0.1) | ||
| by cloudflare-email.com (unknown) id 4fwwffRXOpyR | ||
| for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000 | ||
| From: "John" <[email protected]> | ||
| Reply-To: [email protected] | ||
| To: [email protected] | ||
| Subject: Testing Email Workers Local Dev | ||
| Content-Type: text/html; charset="windows-1252" | ||
| X-Mailer: Curl | ||
| Date: Tue, 27 Aug 2024 08:49:44 -0700 | ||
| Message-ID: <6114391943504294873000@ZSH-GHOSTTY> | ||
|
|
||
| Hi there' | ||
| ``` | ||
|
|
||
| This is what you get in the console: | ||
|
|
||
| ```json | ||
| { | ||
| headers: [ | ||
| { | ||
| key: 'received', | ||
| value: 'from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000' | ||
| }, | ||
| { key: 'from', value: '"John" <[email protected]>' }, | ||
| { key: 'reply-to', value: '[email protected]' }, | ||
| { key: 'to', value: '[email protected]' }, | ||
| { key: 'subject', value: 'Testing Email Workers Local Dev' }, | ||
| { key: 'content-type', value: 'text/html; charset="windows-1252"' }, | ||
| { key: 'x-mailer', value: 'Curl' }, | ||
| { key: 'date', value: 'Tue, 27 Aug 2024 08:49:44 -0700' }, | ||
| { | ||
| key: 'message-id', | ||
| value: '<6114391943504294873000@ZSH-GHOSTTY>' | ||
| } | ||
| ], | ||
| from: { address: '[email protected]', name: 'John' }, | ||
| to: [ { address: '[email protected]', name: '' } ], | ||
| replyTo: [ { address: '[email protected]', name: '' } ], | ||
| subject: 'Testing Email Workers Local Dev', | ||
| messageId: '<6114391943504294873000@ZSH-GHOSTTY>', | ||
| date: '2024-08-27T15:49:44.000Z', | ||
| html: 'Hi there\n', | ||
| attachments: [] | ||
| } | ||
| ``` | ||
|
|
||
| Local development is a critical part of the development flow, and also works for sending, replying and forwarding emails. See [our documentation](/email-routing/email-workers/local-development/) for more information. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
src/content/docs/email-routing/email-workers/local-development.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| --- | ||
| title: Local Development | ||
| sidebar: | ||
| order: 6 | ||
| badge: Beta | ||
|
|
||
| --- | ||
|
|
||
| import { Render, Type, MetaInfo, WranglerConfig } from "~/components"; | ||
|
|
||
| You can test the behavior of an Email Worker script in local development using [wrangler dev](/workers/wrangler/commands/#dev). | ||
|
|
||
| This is the minimal wrangler configuration required to run an Email Worker locally: | ||
|
|
||
| <WranglerConfig> | ||
|
|
||
| ```jsonc | ||
| { | ||
| "send_email": [ | ||
| { | ||
| "name": "EMAIL" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| </WranglerConfig> | ||
|
|
||
| :::note | ||
|
|
||
| If you want to deploy your script you need to [enable Email Routing](/email-routing/get-started/enable-email-routing/) and have at least one verified [destination address](/email-routing/setup/email-routing-addresses/#destination-addresses). | ||
|
|
||
| ::: | ||
|
|
||
| You can now test receiving, replying, and sending emails in your local environment. | ||
|
|
||
| ## Receive an email | ||
|
|
||
| Consider this example Email Worker script that uses the open source [`postal-mime`](https://www.npmjs.com/package/postal-mime) email parser: | ||
|
|
||
| ```ts | ||
| import * as PostalMime from 'postal-mime'; | ||
|
|
||
| export default { | ||
| async email(message, env, ctx) { | ||
| const parser = new PostalMime.default(); | ||
| const rawEmail = new Response(message.raw); | ||
| const email = await parser.parse(await rawEmail.arrayBuffer()); | ||
| console.log(email); | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| Now when you run `npx wrangler dev`, wrangler will expose a local `/cdn-cgi/handler/email` endpoint that you can `POST` email messages to and trigger your Worker's `email()` handler: | ||
|
|
||
| ```bash | ||
| curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \ | ||
| --url-query '[email protected]' \ | ||
| --url-query '[email protected]' \ | ||
| --header 'Content-Type: application/json' \ | ||
| --data-raw 'Received: from smtp.example.com (127.0.0.1) | ||
| by cloudflare-email.com (unknown) id 4fwwffRXOpyR | ||
| for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000 | ||
| From: "John" <[email protected]> | ||
| Reply-To: [email protected] | ||
| To: [email protected] | ||
| Subject: Testing Email Workers Local Dev | ||
| Content-Type: text/html; charset="windows-1252" | ||
| X-Mailer: Curl | ||
| Date: Tue, 27 Aug 2024 08:49:44 -0700 | ||
| Message-ID: <6114391943504294873000@ZSH-GHOSTTY> | ||
|
|
||
| Hi there' | ||
| ``` | ||
|
|
||
| This is what you get in the console: | ||
|
|
||
| ```json | ||
| { | ||
| headers: [ | ||
| { | ||
| key: 'received', | ||
| value: 'from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000' | ||
| }, | ||
| { key: 'from', value: '"John" <[email protected]>' }, | ||
| { key: 'reply-to', value: '[email protected]' }, | ||
| { key: 'to', value: '[email protected]' }, | ||
| { key: 'subject', value: 'Testing Email Workers Local Dev' }, | ||
| { key: 'content-type', value: 'text/html; charset="windows-1252"' }, | ||
| { key: 'x-mailer', value: 'Curl' }, | ||
| { key: 'date', value: 'Tue, 27 Aug 2024 08:49:44 -0700' }, | ||
| { | ||
| key: 'message-id', | ||
| value: '<6114391943504294873000@ZSH-GHOSTTY>' | ||
| } | ||
| ], | ||
| from: { address: '[email protected]', name: 'John' }, | ||
| to: [ { address: '[email protected]', name: '' } ], | ||
| replyTo: [ { address: '[email protected]', name: '' } ], | ||
| subject: 'Testing Email Workers Local Dev', | ||
| messageId: '<6114391943504294873000@ZSH-GHOSTTY>', | ||
| date: '2024-08-27T15:49:44.000Z', | ||
| html: 'Hi there\n', | ||
| attachments: [] | ||
| } | ||
| ``` | ||
|
|
||
| ## Send an email | ||
|
|
||
| Wrangler can also simulate sending emails locally. Consider this example Email Worker script that uses the [`mimetext`](https://www.npmjs.com/package/mimetext) npm package: | ||
|
|
||
| ```ts | ||
| import { EmailMessage } from "cloudflare:email"; | ||
| import { createMimeMessage } from 'mimetext'; | ||
|
|
||
| export default { | ||
| async fetch(request, env, ctx) { | ||
| const msg = createMimeMessage(); | ||
| msg.setSender({ name: 'Sending email test', addr: '[email protected]' }); | ||
| msg.setRecipient('[email protected]'); | ||
| msg.setSubject('An email generated in a worker'); | ||
| msg.addMessage({ | ||
| contentType: 'text/plain', | ||
| data: `Congratulations, you just sent an email from a worker.`, | ||
| }); | ||
|
|
||
| var message = new EmailMessage('[email protected]', '[email protected]', msg.asRaw()); | ||
| await env.EMAIL.send(message); | ||
| return Response.json({ ok: true }); | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| Now when you run `npx wrangler dev`, go to http://localhost:8787/ to trigger the `fetch()` handler and send the email. You will see the follow message in your terminal: | ||
|
|
||
| ```txt | ||
| ⎔ Starting local server... | ||
| [wrangler:inf] Ready on http://localhost:8787 | ||
| [wrangler:inf] GET / 200 OK (19ms) | ||
| [wrangler:inf] send_email binding called with the following message: | ||
| /var/folders/33/pn86qymd0w50htvsjp93rys40000gn/T/miniflare-f9be031ff417b2e67f2ac4cf94cb1b40/files/email/33e0a255-a7df-4f40-b712-0291806ed2b3.eml | ||
| ``` | ||
|
|
||
| Wrangler simulated `env.EMAIL.send()` by writing the email to a local file in [eml](https://datatracker.ietf.org/doc/html/rfc5322) format. The file contains the raw email message: | ||
|
|
||
| ``` | ||
| Date: Fri, 04 Apr 2025 12:27:08 +0000 | ||
| From: =?utf-8?B?U2VuZGluZyBlbWFpbCB0ZXN0?= <[email protected]> | ||
| To: <[email protected]> | ||
| Message-ID: <[email protected]> | ||
| Subject: =?utf-8?B?QW4gZW1haWwgZ2VuZXJhdGVkIGluIGEgd29ya2Vy?= | ||
| MIME-Version: 1.0 | ||
| Content-Type: text/plain; charset=UTF-8 | ||
| Content-Transfer-Encoding: 7bit | ||
|
|
||
| Congratulations, you just sent an email from a worker. | ||
| ``` | ||
|
|
||
| ## Reply to and forward messages | ||
|
|
||
| Likewise, [`EmailMessage`](/email-routing/email-workers/runtime-api/#emailmessage-definition)'s `forward()` and `reply()` methods are also simulated locally. Consider this Worker that receives an email, parses it, replies to the sender, and forwards the original message to one your verified recipient addresses: | ||
|
|
||
| ```ts | ||
| import * as PostalMime from 'postal-mime'; | ||
| import { createMimeMessage } from 'mimetext'; | ||
| import { EmailMessage } from 'cloudflare:email'; | ||
|
|
||
| export default { | ||
| async email(message, env: any, ctx: any) { | ||
| // parses incoming message | ||
| const parser = new PostalMime.default(); | ||
| const rawEmail = new Response(message.raw); | ||
| const email = await parser.parse(await rawEmail.arrayBuffer()); | ||
|
|
||
| // creates some ticket | ||
| // const ticket = await createTicket(email); | ||
|
|
||
| // creates reply message | ||
| const msg = createMimeMessage(); | ||
| msg.setSender({ name: 'Thank you for your contact', addr: '[email protected]' }); | ||
| msg.setRecipient(message.from); | ||
| msg.setHeader('In-Reply-To', message.headers.get('Message-ID')); | ||
| msg.setSubject('An email generated in a worker'); | ||
| msg.addMessage({ | ||
| contentType: 'text/plain', | ||
| data: `This is an automated reply. We received you email with the subject "${email.subject}", and will handle it as soon as possible.`, | ||
| }); | ||
|
|
||
| const replyMessage = new EmailMessage('[email protected]', message.from, msg.asRaw()); | ||
|
|
||
| await message.reply(replyMessage); | ||
| await message.forward("[email protected]"); | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| Run `npx wrangler dev` and use curl to `POST` the same message from the [Receive an email](#receive-an-email) example. Your terminal will show you where to find the replied message in your local disk and to whom the email was forwarded: | ||
|
|
||
| ```txt | ||
| ⎔ Starting local server... | ||
| [wrangler:inf] Ready on http://localhost:8787 | ||
| [wrangler:inf] Email handler replied to sender with the following message: | ||
| /var/folders/33/pn86qymd0w50htvsjp93rys40000gn/T/miniflare-381a79d7efa4e991607b30a079f6b17d/files/email/a1db7ebb-ccb4-45ef-b315-df49c6d820c0.eml | ||
| [wrangler:inf] Email handler forwarded message with | ||
| rcptTo: [email protected] | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid "see" here.