Markdown

Send and receive email in OpenOrange Workers

OpenOrange runs Cloudflare Workers on AWS, Google Cloud, Azure or your own servers. Its email binding sends through an SMTP server you choose, and a Worker's email() handler runs when a raw message is POSTed to /cdn-cgi/handler/email. Groundmail does both: it sends your Worker's mail, and it receives mail on your domain and forwards each message to your Worker.

Updated Nov 14, 20262 min read


Prerequisites

  • A Groundmail account with a verified domain. To receive, use a subdomain such as inbound.yourdomain.com so your main mailboxes keep their MX.
  • An SMTP credential (gmsmtp_…) from SMTP in the dashboard, to send.
  • An OpenOrange Worker (orange dev locally, or deployed with orange deploy).

Send

Bind the sender in wrangler.jsonc, as on Cloudflare:

JSON
{
  "send_email": [{ "name": "EMAIL" }]
}

Point OpenOrange at Groundmail's SMTP relay in .dev.vars (and orange deploy --secrets-file .dev.vars on a cluster). URL-encode the password if it has /, + or =:

.env
ORANGE_EMAIL_SMTP_URL=smtp://groundmail:gmsmtp_your_password@smtp.groundmail.dev:587

Send from the Worker:

TypeScript
await env.EMAIL.send({
  from: "hello@yourdomain.com",
  to: "user@example.com",
  subject: "Welcome",
  text: "Thanks for signing up.",
})

The From address must be on your verified domain. message.forward() and message.reply() in your email() handler send through the same relay. To set the email's category, add an X-Groundmail-Category header or set it on the SMTP credential.


Receive

  1. Turn on inbound for your domain and publish the MX record it returns. See Inbound email.
  2. Pick a secret and give it to the Worker as ORANGE_EMAIL_INBOUND_SECRET (in .dev.vars, and with --secrets-file on a cluster).
  3. Add a route that sends the address to your Worker's email route, with the same secret. In the dashboard: the domain's Routing tab, Add route, Send to URL. Or by API:
Terminal
curl -X POST https://api.groundmail.dev/domains/inbound.yourdomain.com/routes \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "*",
    "action": "url",
    "url": "https://your-worker.example.com/cdn-cgi/handler/email",
    "secret": "the-same-secret-as-ORANGE_EMAIL_INBOUND_SECRET"
  }'

* sends every address to the Worker. Use support (or any address) to send just that one, and give other addresses their own routes: forward billing@ to a teammate, drop noreply@. See Email routing.

Each email to the domain now runs your Worker's handler:

TypeScript
export default {
  async email(message, env) {
    if (message.headers.get("subject")?.includes("unsubscribe")) {
      message.setReject("Not accepted")
      return
    }
    await message.forward("team@yourdomain.com")
  },
}

A setReject() answers 400, which Groundmail records as rejected and doesn't retry. If the Worker is down or answers 5xx, Groundmail retries for about 2 hours. See Send to a URL for the request format and retries. Each received email also stays in Groundmail, with its forward.status, and email.received webhooks still go.


Was this page helpful?