Skip to content

feat(worker): add send_email binding support#975

Open
connyay wants to merge 3 commits intocloudflare:mainfrom
connyay:cjh-sending-email
Open

feat(worker): add send_email binding support#975
connyay wants to merge 3 commits intocloudflare:mainfrom
connyay:cjh-sending-email

Conversation

@connyay
Copy link
Copy Markdown
Contributor

@connyay connyay commented Apr 15, 2026

Adds a SendEmail binding and EmailMessage type so workers can dispatch email via the Cloudflare Email Sending service configured under [[send_email]] in wrangler.toml. Includes worker-sys bindings for cloudflare:email, a runnable example under examples/send-email, and integration tests.

Adds a `SendEmail` binding and `EmailMessage` type so workers can dispatch
email via the Cloudflare Email Sending service configured under
`[[send_email]]` in wrangler.toml. Includes worker-sys bindings for
`cloudflare:email`, a runnable example under `examples/send-email`, and
integration tests.
@connyay
Copy link
Copy Markdown
Contributor Author

connyay commented Apr 15, 2026

There are currently two open PRs around workers + email: #624 #715

Sorry for further muddying the waters here. I opened this with the narrow focus on sending + tests + example. If this lands I am happy to work on email recieving in a follow up.

@connyay connyay marked this pull request as draft April 16, 2026 13:44
Extend the SendEmail binding to cover the public-beta builder overload in
addition to the raw MIME path. Adds `Email`/`EmailBuilder`, `EmailAddress`,
`EmailAttachment` (with `AttachmentContent::{Base64, Binary}`), and
`EmailSendResult { message_id }`. `SendEmail::send` now takes `&Email`;
the raw MIME path moves to `SendEmail::send_mime(&EmailMessage)`.
@connyay connyay marked this pull request as ready for review April 17, 2026 02:46
Comment thread worker/src/send_email.rs
Comment on lines +119 to +121
// Miniflare's `send_email` binding resolves to `undefined`; real
// workerd resolves to `{ messageId }`. Tolerate both so local dev
// with `wrangler dev` doesn't throw on deserialize.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edevil
Copy link
Copy Markdown
Contributor

edevil commented Apr 17, 2026

Duplicate headers silently collapseworker/src/send_email.rs:335-341, 353-362

The field comment on Email::headers says Vec<(String, String)> is used because "duplicate header names are meaningful in RFC 5322," but the serialization path makes duplicates unreachable:

  • serialize_headers calls SerializeMap::serialize_entry for each pair.
  • The encoder (line 103) is Serializer::new().serialize_maps_as_objects(true), so the map becomes a plain JS object — duplicate keys overwrite.
  • The runtime-side type is jsg::Dict<kj::String> (also a map; no duplicates).

So duplicates silently collapse to the last value instead of being preserved. Two ways to tighten:

  1. Drop the RFC 5322 claim and back the field with a BTreeMap<String, String> (or HashMap) so the type reflects what actually gets sent.
  2. Error on duplicates in EmailBuilder::build() so callers don't lose data without noticing.

(1) is simpler and fine given the runtime can't represent duplicates anyway.

@edevil
Copy link
Copy Markdown
Contributor

edevil commented Apr 17, 2026

AttachmentContent::Base64 is misnamed — the runtime does not base64-decode string contentworker/src/send_email.rs:221-250

The runtime treats attachment string content as UTF-8 bytes, not base64. The function in edgeworker that feeds the mail stream does this:

kj::ArrayPtr<kj::byte> getArrayPtrFromContent(kj::OneOf<kj::String, jsg::BufferSource>& content) {
  KJ_SWITCH_ONEOF(content) {
    KJ_CASE_ONEOF(string, kj::String)        { return string.asBytes(); }
    KJ_CASE_ONEOF(buffer, jsg::BufferSource) { return buffer.asArrayPtr(); }
  }
}

kj::String::asBytes() returns raw UTF-8 — no base64 decoding happens. So:

  • AttachmentContent::Base64("Hello, World!") → recipient gets the text Hello, World!. Works, despite the name.
  • AttachmentContent::Base64("JVBERi0xLjQK…") (a user who followed the public docs and pre-base64-encoded a PDF) → recipient gets the literal ASCII of the base64 string, not the decoded PDF. Broken, silently.

(Sidebar: the public docs at email-service/api/send-emails/workers-api/ claim content: string | ArrayBuffer; // Base64 string or binary content — that contradicts the runtime and should be filed as a docs bug.)

Suggested fix — rename to reflect actual semantics

pub enum AttachmentContent {
    Text(String),      // sent as UTF-8 bytes on the wire
    Bytes(Vec<u8>),    // sent as Uint8Array
}

with the obvious From impls (&str/StringText, &[u8]/Vec<u8>Bytes) and a doc note: "If your source is base64, decode it to bytes first." This matches the runtime's OneOf<String, BufferSource> faithfully and removes the footgun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants