@mathrunet/masamune_cloudflare_send_mail
v3.1.1
Published
Masamune framework package plugin for sending mail through Cloudflare Email Service (Email Sending) from Cloudflare Workers.
Downloads
315
Maintainers
Readme
[GitHub] | [YouTube] | [Packages] | [X] | [LinkedIn] | [mathru.net]
Masamune framework package plugin for sending mail through Cloudflare Email Service (Email Sending, beta) from Cloudflare Workers.
Two transports are supported.
| Transport | Use it when | How it sends |
| --- | --- | --- |
| binding | The sender domain is onboarded on the same Cloudflare account as the Worker. | Workers send_email binding (env.EMAIL.send()). |
| api | The sender domain is onboarded on another Cloudflare account. | REST API POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send with an API token. |
Also, masamune_functions_cloudflare can be used to execute server-side functions from methods defined on the client side, allowing for safe implementation.
Installation
Install the following packages
npm install @mathrunet/masamune_cloudflare_send_mailBefore sending, onboard the sender domain in the Cloudflare dashboard under Compute > Email Service > Email Sending > Onboard Domain. The domain must use Cloudflare DNS. See Send emails.
Setup
Binding transport
Add a send_email binding to your Wrangler configuration.
// wrangler.jsonc
{
"send_email": [
// "remote": true lets `wrangler dev` send real emails through the remote binding.
{ "name": "EMAIL", "remote": true }
]
}# wrangler.toml
[[send_email]]
name = "EMAIL"
remote = trueThe binding can be restricted with allowed_sender_addresses, allowed_destination_addresses or destination_address. See Configure send bindings.
If you use a binding name other than EMAIL, set it with the bindingName option or the MAIL_CLOUDFLARE_BINDING variable.
REST API transport
- On the account that owns the sender domain, create an API token with the Email Sending: Edit permission.
- Copy the account ID of that account.
- Register them in the Worker that sends mail.
npx wrangler secret put MAIL_CLOUDFLARE_API_TOKEN// wrangler.jsonc
{
"vars": {
"MAIL_CLOUDFLARE_ACCOUNT_ID": "<account_id>"
}
}Never commit the API token. For local development, put it in .dev.vars.
Implementation
Library
import { sendMail, SendMailError } from "@mathrunet/masamune_cloudflare_send_mail";
interface Env {
EMAIL: SendEmail; // From @cloudflare/workers-types. Any object with a compatible send() works.
MAIL_CLOUDFLARE_ACCOUNT_ID: string;
MAIL_CLOUDFLARE_API_TOKEN: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const result = await sendMail({
// Same-account domain:
transport: { type: "binding", binding: env.EMAIL },
// Another account's domain:
// transport: { type: "api", accountId: env.MAIL_CLOUDFLARE_ACCOUNT_ID, apiToken: env.MAIL_CLOUDFLARE_API_TOKEN },
message: {
from: { email: "[email protected]", name: "Your Service" },
to: ["[email protected]", { email: "[email protected]", name: "Jane Doe" }],
replyTo: "[email protected]",
subject: "Welcome!",
text: "Thanks for signing up.",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
},
});
return Response.json(result);
} catch (e) {
if (e instanceof SendMailError) {
// e.retryable is true for 429 / 5xx / network errors and retryable binding errors.
return Response.json({ error: e.message, code: e.code }, { status: e.retryable ? 503 : 400 });
}
throw e;
}
},
};Message
| Field | Type | Notes |
| --- | --- | --- |
| to | string \| SendMailAddress \| Array<string \| SendMailAddress> | Required. |
| cc, bcc | same as to | Optional. to + cc + bcc must not exceed 50 addresses. |
| from | string \| SendMailAddress | Required. Must belong to an onboarded domain. |
| replyTo | string \| SendMailAddress | Optional. |
| subject | string | Required. |
| text, html | string | At least one is required. |
| headers | Record<string, string> | Optional. Only allowed headers. |
SendMailAddress is { email: string; name?: string }. It is converted to { email, name } for the binding and to { address, name } for the REST API. replyTo is sent as replyTo to the binding and as reply_to to the REST API.
Result
| Field | binding | api |
| --- | --- | --- |
| messageId | messageId returned by send() | result.message_id |
| delivered | always [] | result.delivered |
| queued | always [] | result.queued |
| permanentBounces | always [] | result.permanent_bounces |
| suppressedRecipients | not set | result.suppressed_recipients when returned |
The binding does not report per-recipient status; a resolved promise means Cloudflare accepted the message.
Errors
sendMail throws SendMailError with status, code and retryable.
- Invalid input:
codeisINVALID_MESSAGEorINVALID_TRANSPORT. Nothing is sent. binding:codeis the binding error code (e.g.E_SENDER_NOT_VERIFIED).E_RATE_LIMIT_EXCEEDEDandE_INTERNAL_SERVER_ERRORare retryable.api:statusis the HTTP status andcodeis the first Cloudflare error code (e.g.10001). 429 and 5xx are retryable, other 4xx are not. Network failures useNETWORK_ERRORand are retryable.
The API token is never included in error messages.
Functions
Import the package as follows and pass the list of functions you wish to define to the deploy function.
import * as m from "@mathrunet/masamune_cloudflare_send_mail";
export default m.deploy([
// POST /send_mail
m.Functions.sendMail(),
]);The transport is resolved in the following order.
- The
transportoption. - The
send_emailbinding namedbindingName/MAIL_CLOUDFLARE_BINDING/EMAILin the Workers env (skipped whentype: "api"). - The REST API with
accountId/MAIL_CLOUDFLARE_ACCOUNT_IDandapiToken/MAIL_CLOUDFLARE_API_TOKEN(skipped whentype: "binding").
m.Functions.sendMail({
auth: new m.NoneAuthAdapter(), // Configure an appropriate authentication adapter in production.
type: "api",
accountId: "<account_id>",
});Request body:
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Welcome!",
"text": "Thanks for signing up.",
"html": "<h1>Welcome!</h1>"
}Response body:
{
"success": true,
"messageId": "<[email protected]>",
"delivered": ["[email protected]"],
"queued": [],
"permanentBounces": []
}Invalid input returns 400, rate limiting returns 429, and other failures return 500 with { "error": "..." }.
Anyone who can call this endpoint can send mail from your domain. Always protect it with an authentication adapter and rules, and restrict senders with allowed_sender_addresses where possible.
GitHub Sponsors
Sponsors are always welcome. Thank you for your support!
