A newsletter you run on your own server

Docs

Send through a relay

Send your newsletter through Resend, Postmark, SES or any provider's API, even where your host blocks SMTP.

Paste this into your agent

Write me a relay that sends my Mailpiece newsletter through my mail provider's API. Read mailpiece.dev/docs/relay.md first.

Settings

Instead of SMTP, Mailpiece can POST each newsletter message as JSON to a URL you run. Your small relay turns it into your provider's API call.

The relay is yours: you write it, deploy it anywhere that serves a URL at a public address, and keep the provider's key there. Mailpiece only knows what the newsletter sets on Settings › Outgoing mail: the Relay address, posted to exactly as you set it, and a Token you choose, sent as Authorization: Bearer <token>. The token is never shown again; to change it, delete the relay and set it up again.

Mailpiece posts only to a public address. Loopback, private ranges and the cloud metadata address are refused, so a relay on localhost beside the app won't work: the delivery waits and the newsletter is told the relay turned it down.

# a long random string the relay checks
mailpiece settings sending relay update --url https://relay.example.com --token ...

What Mailpiece sends

One POST per message, with only the keys that have a value: from, from_name, to, cc, bcc, reply_to, reply_to_name, subject, html, text, list_unsubscribe, tags and headers. from and reply_to are bare addresses, with their names beside them; to, cc and bcc are lists.

list_unsubscribe is the one-click unsubscribe link: send it on as the List-Unsubscribe header, with List-Unsubscribe-Post: List-Unsubscribe=One-Click. tags is ["issue"] on an issue or a welcome email to a subscriber, and headers holds the sender's own X- headers.

{
  "from": "[email protected]",
  "from_name": "My Newsletter",
  "to": [ "[email protected]" ],
  "reply_to": "[email protected]",
  "reply_to_name": "Me",
  "subject": "...",
  "html": "...",
  "text": "...",
  "list_unsubscribe": "<https://...>",
  "tags": [ "issue" ]
}

The Idempotency-Key header

Each issue and welcome email sent to a subscriber carries an Idempotency-Key header, the same every time that message is posted. When Mailpiece gets no answer it can trust, it posts the message again with the same key, for hours if it must.

A relay must honour the key. Remember each key once its message is sent, for as long as Mailpiece may post it again, and answer a repeat with a 2xx without sending again. Otherwise a reader can get the same issue twice. Test emails and subscription confirmations carry no key: send them each time.

What the relay answers

2xx: sent, optionally with {"message_id": "..."}. 400, 413 or 422: refused, never tried again. 401 or 403: the token was turned down; reported, and tried again later. 429 or 503: nothing was sent, tried again later.

Anything else, a 502 or 504 included, or no answer at all: Mailpiece cannot tell whether the message went, so it posts it again later under the same key.

An example for Resend

A fetch handler in the style of a Cloudflare Worker, with RELAY_TOKEN and RESEND_API_KEY set where it runs. It remembers no key itself, and hands each on to Resend, which keeps it for 24 hours: the Cloudflare Workers relay adds that. Any AI agent can write one for another provider from this page and the provider's API docs.

const address = (email, name) => name ? `${JSON.stringify(name)} <${email}>` : email

export default {
  async fetch(request, env) {
    if (request.headers.get("Authorization") !== `Bearer ${env.RELAY_TOKEN}`) {
      return new Response("Unauthorized", { status: 401 })
    }
    const key = request.headers.get("Idempotency-Key")
    const message = await request.json()
    const headers = { ...message.headers }
    if (message.list_unsubscribe) {
      headers["List-Unsubscribe"] = message.list_unsubscribe
      headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
    }
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
        ...(key && { "Idempotency-Key": key })
      },
      body: JSON.stringify({
        from: address(message.from, message.from_name),
        to: message.to,
        cc: message.cc,
        bcc: message.bcc,
        reply_to: message.reply_to && address(message.reply_to, message.reply_to_name),
        subject: message.subject,
        html: message.html,
        text: message.text,
        headers,
        tags: message.tags?.map(name => ({ name, value: "true" }))
      })
    })
    if (!response.ok) return new Response(await response.text(), { status: response.status })
    const sent = await response.json()
    return Response.json({ message_id: sent.id })
  }
}

Still need help? Ask on GitHub.