runlot
Data

Email

Send mail from your worker, and let your worker receive mail sent to the project address. Runlot runs the sender reputation and the receiving server.

runlot.json
{ "email": true }
runlot deploy

Every project gets one mail address on deploy. For project shop in org acme:

Address
Sending[email protected]
Receiving*@shop.acme.runlot.email — any local part works

See the address in the dashboard's Email tab or with runlot email.

Sending

export default {
  async fetch(request, env) {
    await env.email.send({
      to: "[email protected]",           // string or array (up to 10)
      subject: "Order received",
      text: "Thanks. Shipping soon.",
      html: "<p>Thanks. Shipping soon.</p>",   // text or html is required
      replyTo: "[email protected]",       // optional
    });
    return new Response("ok");
  },
};

There is no from. The sender is the project address and cannot be changed. The call returns { id } and throws on failure — e.status is the HTTP status (429 means the hourly limit).

Receiving

export default {
  async fetch(request, env) { /* … */ },

  async email(message, env, ctx) {
    // message.from, message.to, message.headers (Headers), message.raw (ReadableStream), message.rawSize
    const raw = await new Response(message.raw).text();
    await env.db.query("insert into inbox (from_addr, to_addr, raw) values ($1, $2, $3)",
      [message.from, message.to, raw]);

    // To refuse — the sender sees this text
    if (message.headers.get("authentication-results")?.includes("spf=fail")) {
      message.setReject("SPF check failed");
    }
  },
};

The email handler has the same shape as Cloudflare Email Workers; ported code usually runs as is. message.raw is the full message including headers — use a parser such as postal-mime for attachments.

Mail to a project that does not export a handler bounces. If the handler throws, the sender retries later (treated as a temporary failure).

Limits

Value
Sends per hourFree 30 · Pro 300 — shared with sign-in mail (env.auth)
Recipients per message10
Received per hour600
Received message size25 MiB
Handler runtime30 s

Over the send limit you get 429; over the receive limit the sender is told to try again later.

Seeing what was sent and received

Every message, sent or received, gets one line — in the dashboard under the Email tab: time, direction, from, to, subject, outcome, size. Click a line for headers and body; the raw message downloads as .eml. HTML bodies are shown as source, not rendered.

PlanKept for
Free30 days
Pro365 days
runlot email log                       # latest 50
runlot email log --direction in        # received only
runlot email log --cursor <cursor>     # next page
runlot email raw <mailId> > mail.eml   # raw message

For received mail the outcome is what the handler did — handled, rejected, no handler, handler error. For sent mail it is whether the relay accepted it, and if not, why.

What is not here

  • Custom sending domains. Only *.runlot.email for now.
  • Attachments on send. text and html only.
  • Newsletters. Ten recipients is the line.
  • forward() / reply(). Both can be done with env.email.send.

CLI

runlot email            # address and sent/received counts for the last hour and 24 hours
runlot email --json
runlot email delete     # turn off (admin). Mail to this address will bounce

On this page