Prerequisites
- A Groundmail account with a verified domain. To receive, use a subdomain such as
inbound.yourdomain.comso your main mailboxes keep their MX. - An SMTP credential (
gmsmtp_…) from SMTP in the dashboard, to send. - An OpenOrange Worker (
orange devlocally, or deployed withorange deploy).
Send
Bind the sender in wrangler.jsonc, as on Cloudflare:
{
"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 =:
ORANGE_EMAIL_SMTP_URL=smtp://groundmail:gmsmtp_your_password@smtp.groundmail.dev:587Send from the Worker:
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
- Turn on inbound for your domain and publish the MX record it returns. See Inbound email.
- Pick a secret and give it to the Worker as
ORANGE_EMAIL_INBOUND_SECRET(in.dev.vars, and with--secrets-fileon a cluster). - 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:
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:
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.