@odla-ai/email
v0.3.1
Published
Send and receive email from odla apps and agents — structured messages for Cloudflare Email Service's modern send() API (multi-recipient, cc/bcc, attachments, messageId receipts), invite/onboarding rendering, inbound parsing with DMARC verification, and t
Maintainers
Readme
@odla-ai/email
Send and receive email from odla apps and agents. A small, dependency-free, isomorphic library (Node 20+, Cloudflare Workers, the browser) that:
- shapes structured outbound messages (
EmailPayload) for Cloudflare Email Service's modernsend()API —html/textbodies, multiple recipients,cc/bcc/replyTo, custom headers, and attachments (EmailAttachment) — validated fail-closed byvalidatePayload; - renders invite / onboarding emails (
renderInvite) whose default body sends people to the Quickstart and starts agents withnpx @odla-ai/cli setup; - parses inbound mail (
parseInbound,verifyInbound) and threads replies (buildReply).
It never imports cloudflare:email. Transport is a seam: a Worker implements
the one-method EmailSender interface over its send_email binding and
returns the binding's receipt (EmailSendReceipt, carrying the transport's
messageId); tests implement it as a spy. That keeps this package pure and
portable, and confines the Cloudflare runtime to the @odla/email-router
Worker that consumes it.
Sending an invite
import { renderInvite, sendMessage, type EmailSender } from "@odla-ai/email";
// In a Worker, `sender` adapts env.EMAIL (the send_email binding); see
// @odla/email-router's cfSender for the reference adapter:
// const sender = { async send(p) {
// const { messageId } = await env.EMAIL.send({ ...p }); // structured API
// return { messageId };
// }};
const invite = renderInvite({
from: { name: "odla", email: "[email protected]" },
to: "[email protected]",
acceptUrl: "https://odla.ai/accept?__clerk_ticket=…", // from Clerk
});
const { messageId } = await sendMessage(sender, invite);renderInvite returns a validated EmailPayload — the structured shape
Cloudflare Email Service's send() accepts ({ from, to, subject, html, text,
cc, bcc, replyTo, headers, attachments }). Pass instructions to override the
default onboarding block, or appName / recipientName to personalise.
A failed send surfaces as EmailError with the transport's machine-readable
code preserved — Cloudflare's E_SENDER_NOT_VERIFIED,
E_RATE_LIMIT_EXCEEDED, and E_TOO_MANY_RECIPIENTS.
Payload safety
validatePayload rejects CR/LF in every caller-controlled header component
(subject, addresses, display names, custom header names/values, attachment
metadata), checks address syntax on from/to/cc/bcc/replyTo, and
enforces Cloudflare's limits, exported as MAX_RECIPIENTS (50 combined
to/cc/bcc; toAddressList normalizes the single-or-array Recipients forms)
and MAX_ATTACHMENTS (32). renderInvite and buildReply validate before
returning, so an unsafe reflected inbound Message-ID fails closed. These
checks protect message structure; they do not sanitize HTML bodies.
Receiving mail
import { parseInbound, verifyInbound, buildReply } from "@odla-ai/email";
// In a Worker's email() handler:
const raw = await new Response(message.raw).text();
const mail = parseInbound(raw);
if (!verifyInbound(mail).ok) return message.setReject("DMARC failed");
const reply = buildReply(mail, { from: "[email protected]", text: "Got it." });
await sender.send(reply); // threaded via In-Reply-To / References headersparseInbound handles single-part bodies (base64 / quoted-printable). Rich
multipart + attachment parsing is intentionally left to postal-mime in the
receiving Worker so this library stays dependency-free.
Local development
wrangler dev simulates the send_email binding: the message is logged
and written to a temp file, and send() returns a synthesized messageId —
nothing is delivered. Add "remote": true to the binding in wrangler.jsonc
to send real email from local dev (requires the onboarded sending domain).
The local simulator cannot serialize ArrayBuffer attachment content — use
"remote": true or string content when exercising attachments locally. The
inbound email() handler is also drivable locally:
POST /cdn-cgi/handler/email?from=…&to=… with a raw RFC 5322 body.
API
Inspect the installed package's exported TypeScript declarations/JSDoc, or the
rendered reference at https://odla.ai/docs/packages/email. Key exports:
sendMessage, validatePayload, toAddressList, renderInvite /
inviteContent, parseInbound, verifyInbound, buildReply, EmailError;
the EmailPayload, EmailAttachment, Recipients, EmailSender, and
EmailSendReceipt types; the MAX_RECIPIENTS / MAX_ATTACHMENTS limits; and
the decode primitives (decodeBase64, decodeHeaderWord, …).
