Skip to content
Report library
Purpose / Other

Cloudflare Email Service Skill Security Audit

What the author says it does (original text)

Implement or troubleshoot Cloudflare Email Sending and Email Routing integrations and their delivery configuration.

Independent security check

Security risks found

Files checked
6
Risks found
5
Could it run dangerous commands?Looks for programs run straight after downloading, remote control of your computer, and hidden commands.Risks found: 1
Medium risk

`npx wrangler` may download and execute an unpinned package

Source references: 3
What we found

Several checks and account operations invoke `npx wrangler` directly. If the project has no pinned local copy, npx may retrieve and execute a current registry version rather than code fixed by the repository.

Why this matters

A compromised, substituted, or incompatible package version could access the project and available credentials or perform incorrect Cloudflare operations.

The source supports this conditional risk: active instructions invoke unversioned `npx wrangler`. If the project lacks a locally pinned Wrangler, npx may obtain and execute a registry version with the command's user-level permissions. The guide says to inspect the installed version but does not explicitly prohibit downloads. The user can restrict network access and require the resolved local version to be shown first.

SKILL.md:27In the instructionsOpen original file
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)3. **postal-mime installed?** Run `npm ls postal-mime` (only needed for receiving/parsing emails)
Show 2 other places
references/cli-and-mcp.md:5In the instructionsOpen original file
For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
references/sending.md:5In the instructionsOpen original file
Read the documentation for the selected task before implementing. Inspect the project's installed Wrangler and Agents SDK versions, configuration, and existing conventions first. Run `wrangler types` through the project's package manager after changing bindings; use its generated types instead of handwritten email interfaces. See [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) for matching types to the project's compatibility date and flags. Do not upgrade dependencies just to match an example.
Could it expose your files or keys?Looks for uploads of files containing passwords or keys, and keys written directly in the code.Risks found: 2
Medium risk

Examples place full email content and identity data in logs or persistent storage

Source references: 4
What we found

The parsing example logs the subject and body, while the storage example writes sender, recipient, subject, body, and message identifiers to a Durable Object. Email can contain password-reset links, personal data, customer communications, or other secrets.

Why this matters

People or services with access to logs, Durable Objects, or backups could read message content; long retention would increase breach and compliance exposure.

The examples log the parsed subject, body, and attachment count, and persist sender, recipient, subject, body, and message identifiers in a Durable Object. They are instructional examples rather than automatic execution, but copying them into a live inbound-mail handler could expose sensitive correspondence to log readers or storage users. Users can require body-free logging, retention and access controls, and field minimization or encryption before storage.

references/routing.md:99In the instructionsOpen original file
async email(message, env, ctx) {  const rawBuffer = await new Response(message.raw).arrayBuffer();  const parsed = await PostalMime.parse(rawBuffer);  console.log("Subject:", parsed.subject);  console.log("Text:", parsed.text);  console.log("Attachments:", parsed.attachments.length);}```
Show 3 other places
references/routing.md:126In the instructionsOpen original file
export class MailboxDO extends DurableObject {  async storeEmail(from: string, to: string, subject: string, body: string,                   messageId: string, inReplyTo: string | null) {    this.ctx.storage.sql.exec(      `INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)       VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,      from, to, subject, body, messageId, inReplyTo    );  }
references/routing.md:127In the instructionsOpen original file
export class MailboxDO extends DurableObject {  async storeEmail(from: string, to: string, subject: string, body: string,                   messageId: string, inReplyTo: string | null) {    this.ctx.storage.sql.exec(      `INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)       VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,      from, to, subject, body, messageId, inReplyTo    );  }
references/routing.md:145In the instructionsOpen original file
    await stub.storeEmail(      message.from,      message.to,      parsed.subject || "(no subject)",      parsed.text || parsed.html || "",      message.headers.get("message-id") || "",      message.headers.get("in-reply-to") || null,    );
Medium risk

Reply text is inserted into outbound HTML without escaping

Source references: 2
What we found

The example uses `replyBody` as both plain text and HTML, directly interpolating it inside `<p>` without escaping or sanitization. If the body comes from inbound mail, an AI draft, or another untrusted source, tags, links, or tracking resources become part of the outgoing HTML.

Why this matters

Recipients could receive attacker-controlled links, deceptive formatting, or remote tracking content. Because the message comes from the user's verified domain, it may appear especially trustworthy.

The example inserts `replyBody` directly into HTML and states that a user or agent may decide the reply; no HTML escaping or sanitization is shown. If the value contains untrusted tags, links, or remote images, they become part of the outgoing HTML and could mislead recipients or load tracking resources. The risk depends on the value's origin. Users can require text-only mail or reviewed escaping/sanitization plus a pre-send preview.

references/routing.md:162In the instructionsOpen original file
When a user (or agent) decides to reply, build proper threading headers and send via the `send_email` binding:```typescript// In an HTTP handler or agent tool — not in the email() handlerasync function replyToStoredEmail(env: Env, original: StoredEmail, replyBody: string) {  // Build threading headers (In-Reply-To + References per RFC 2822)  const headers: Record<string, string> = {};  if (original.messageId) {    headers["In-Reply-To"] = original.messageId;    headers["References"] = original.messageId;  }  await env.EMAIL.send({    to: original.sender,    from: original.recipient,    subject: `Re: ${original.subject}`,    text: replyBody,    html: `<p>${replyBody}</p>`,    headers,  });}
Show 1 other places
references/routing.md:174In the instructionsOpen original file
  await env.EMAIL.send({    to: original.sender,    from: original.recipient,    subject: `Re: ${original.subject}`,    text: replyBody,    html: `<p>${replyBody}</p>`,    headers,  });}
Could it delete files or keep running?Looks for broad file deletion, disk overwrites, and programs set to start automatically.No risks found
Could it bypass safety checks?Looks for skipped website security checks, excessive file access, or actions that skip your approval.Risks found: 2
Medium risk

Prerequisite and send flows can change an account or contact recipients using the active Cloudflare identity

Source references: 5
What we found

The Skill directs the agent to run an enable command when a domain is absent, and its CLI, REST, and MCP examples can send real mail. These operations can alter account/DNS-related state or contact outside recipients, without a consistent requirement to confirm the account, domain, recipient, and content immediately before execution.

Why this matters

Selecting the wrong account, domain, or address could change email configuration for the wrong domain, consume quota, disclose message content, or send unapproved communications to a third party.

Enabling a domain is an active prerequisite instruction, while the CLI/MCP sections contain commands that produce real external email; the local-development section explicitly says messages are actually sent. The send snippets are examples, not unconditional automation, but there is no consistent requirement to confirm the account, domain, recipient, and content before these actions. Users can require per-action confirmation, controlled test recipients, and least-privilege tokens.

SKILL.md:27In the instructionsOpen original file
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
Show 4 other places
references/cli-and-mcp.md:34In the instructionsOpen original file
```bashnpx wrangler email sending enable yourdomain.comnpx wrangler email sending dns get yourdomain.com   # Verify records```
references/cli-and-mcp.md:103In the instructionsOpen original file
## Sending from CLI / Agents```bashnpx wrangler email sending send \  --from "agent@yourdomain.com" \  --to "developer@company.com" \  --subject "Deployment Complete" \  --text "Your Worker was deployed successfully."```
references/cli-and-mcp.md:115In the instructionsOpen original file
```bashcurl "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/email/sending/send" \  --header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \  --header "Content-Type: application/json" \  --data '{    "to": "developer@company.com",    "from": {"address": "agent@yourdomain.com", "name": "Build Agent"},    "subject": "Deployment Complete",    "text": "Your Worker was deployed successfully."  }'```
references/cli-and-mcp.md:41In the instructionsOpen original file
Add `"remote": true` to send real emails during `wrangler dev`:```jsonc{ "send_email": [{ "name": "EMAIL", "remote": true }] }``````bashnpx wrangler dev```Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.
Low risk

The install command does not pin a dependency version

Source references: 1
What we found

The installation command does not specify dependency versions. The same command may download different code later, so what you install can differ from what was checked.

Why this matters

A later install may download different code even though the command and this report have not changed.

What this evidence establishes

The line uses `npx wrangler` without a version, but it is not explicitly a dependency-install step. Whether it downloads a package depends on whether the project already provides a resolvable Wrangler version and on the npx environment. If no pinned local version exists, the user can require use of the lockfile version and prohibit automatic downloads.

This assessment concerns the code and conditions shown, not proof that harm has occurred.
SKILL.md:27In the instructionsOpen original file
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
Could it mislead the AI or hide text?Checks the skill instructions for requests to ignore you, influence the report, or hide text in invisible characters.No risks found
Could it change links or payment recipients without asking?Looks for forced referral or payment changes combined with instructions to hide the change.No risks found

Inside this skill

6 instruction sections

This Skill is an operational guide that directs an agent to choose Workers bindings, the REST API, Wrangler CLI, or MCP. It covers not only code generation but also account-changing actions such as enabling domains, sending mail, and managing suppressions.

View source
SKILL.md:37In the instructionsOpen original file
|--------------|------|-----------|| **Send emails from a Cloudflare Worker** | Workers binding (no API keys needed) | [sending.md](references/sending.md) || **Send emails from an AI agent built with [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)** | `onEmail()` + `replyToEmail()` in Agent class | [sending.md](references/sending.md) || **Send emails from an external app or agent** (Node.js, Go, Python, etc.) | REST API with Bearer token | [rest-api.md](references/rest-api.md) || **Send emails from a coding agent** (Claude Code, Cursor, Copilot, etc.) | MCP tools, wrangler CLI, or REST API | [cli-and-mcp.md](references/cli-and-mcp.md) || **Receive and process incoming emails** (Email Routing) | Workers `email()` handler | [routing.md](references/routing.md) || **Set up Email Sending or Email Routing** | `wrangler email sending enable` / `wrangler email routing enable`, or Dashboard | [cli-and-mcp.md](references/cli-and-mcp.md) || **Improve deliverability, avoid spam folders** | Authentication, content, compliance | [deliverability.md](references/deliverability.md) |

The guide distinguishes local simulation from real delivery and explicitly warns that `remote: true` causes actual email delivery during development.

View source
references/cli-and-mcp.md:41In the instructionsOpen original file
Add `"remote": true` to send real emails during `wrangler dev`:```jsonc{ "send_email": [{ "name": "EMAIL", "remote": true }] }``````bashnpx wrangler dev```Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.

The inbound-mail example parses message bodies and provides a pattern that persists sender, recipient, subject, body, and threading identifiers in Durable Object SQLite.

View source
references/routing.md:126In the instructionsOpen original file
export class MailboxDO extends DurableObject {  async storeEmail(from: string, to: string, subject: string, body: string,                   messageId: string, inReplyTo: string | null) {    this.ctx.storage.sql.exec(      `INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)       VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,      from, to, subject, body, messageId, inReplyTo    );  }
references/routing.md:139In the instructionsOpen original file
  async email(message, env, ctx) {    const raw = await new Response(message.raw).arrayBuffer();    const parsed = await PostalMime.parse(raw);    const id = env.MAILBOX.idFromName(message.to);    const stub = env.MAILBOX.get(id);    await stub.storeEmail(      message.from,      message.to,      parsed.subject || "(no subject)",      parsed.text || parsed.html || "",      message.headers.get("message-id") || "",      message.headers.get("in-reply-to") || null,    );

The “reply later” pattern calls for a user or agent decision and recommends review before sending. This reduces automatic-send risk, but does not eliminate the possibility that untrusted email content influences an AI draft or reply body.

View source
references/routing.md:160In the instructionsOpen original file
### Reply LaterWhen a user (or agent) decides to reply, build proper threading headers and send via the `send_email` binding:
references/routing.md:191In the instructionsOpen original file
- **Store attachments separately** in R2 (binary blobs), with metadata in SQLite.- **Defer heavy work** (AI drafting, notifications) via `ctx.waitUntil()` so the `email()` handler returns quickly.- **Never auto-send from the `email()` handler** in a human-in-the-loop flow. Store a draft, let the user review, then send via a separate action.
Start here · InstructionsSKILL.md
cloudflare-email-service
Lines connect the instruction file to its sections, not an observed execution order. Select a section to read the source.

File reference map

References: 10
Files making referencesReferenced content
Lines show actual file references, not execution order. Select a node to highlight its connections and inspect the files and source locations. Dashed lines include files that still need locating.
Files and check records6 files

Coverage and gaps

Content covered in each file

These are the source ranges included in this check, not a guarantee that every issue has been resolved.

  • SKILL.mdFull text included
  • references/cli-and-mcp.mdFull text included
  • references/deliverability.mdFull text included
  • references/rest-api.mdFull text included
  • references/routing.mdFull text included
  • references/sending.mdFull text included

This report is for the version above. We read the available code and instructions without running the skill or checking extra packages it installs. This is not a promise of safety: a different version or setup may behave differently.

  • SKILL.mdInstructions
  • references/cli-and-mcp.mdSupporting file
  • references/deliverability.mdSupporting file
  • references/rest-api.mdSupporting file
  • references/routing.mdSupporting file
  • references/sending.mdSupporting file

Operations mentioned in code and instructions

Connect to websites
SKILL.md:18In the instructionsOpen original file
|--------|----------------|---------|| Cloudflare docs | Cloudflare MCP `docs` tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features || REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API |
SKILL.md:19In the instructionsOpen original file
| Cloudflare docs | Cloudflare MCP `docs` tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features || REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API || Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes |
SKILL.md:20In the instructionsOpen original file
| REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API || Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes || Agents SDK docs | [Email agent walkthrough](https://developers.cloudflare.com/agents/examples/email-agent/) | Email handling in Agents SDK |
Install extra software packages
SKILL.md:27In the instructionsOpen original file
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
references/cli-and-mcp.md:5In the instructionsOpen original file
For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
references/cli-and-mcp.md:35In the instructionsOpen original file
```bashnpx wrangler email sending enable yourdomain.comnpx wrangler email sending dns get yourdomain.com   # Verify records
Run commands
references/cli-and-mcp.md:34In the instructionsOpen original file
```bashnpx wrangler email sending enable yourdomain.com
references/cli-and-mcp.md:47In the instructionsOpen original file
```bashnpx wrangler dev
references/cli-and-mcp.md:105In the instructionsOpen original file
```bashnpx wrangler email sending send \
Read keys or account settings
references/rest-api.md:5In the instructionsOpen original file
Read the relevant page before building the request. Keep credentials in the project's existing secret or environment-variable mechanism, and inspect the installed client SDK version if one is used.
Lines read
724
File checksum (to compare versions)
dd1205fc61c7d139fd33fd37383adbd6dda0fd0619936a41a8bbb73ef957cf7a