Bot email addresses
Give each bot its own address, such as [email protected]. Every email sent to it is saved
to the bot’s inbox, which the bot reads when it needs to, for example to pick up the code when it
signs up for a service. Your own mail to it starts the bot, and mail can start its
routines. Bots can also send email from their address. This is
optional and off until you set it up.
You need a domain (or subdomain) that receives mail for every name on it, and something that hands each message to Xieling. Xieling does not run a mail server itself: any mail relay that can make an HTTPS request works.
Configure Xieling
Set both on the API, and INBOUND_EMAIL_DOMAIN on the worker too, so each bot knows its address.
Then restart them:
INBOUND_EMAIL_DOMAIN=bots.example.com
INBOUND_EMAIL_SECRET=$(openssl rand -base64 32)
Use a domain that nothing else receives mail on, and set exactly the domain the relay receives mail for. Once it is set, a bot’s owner can get it an address under its name in the bot’s settings, by asking the bot in chat, or when adding an Email trigger to a routine. Claimed addresses are listed in the Vault. Names are lowercase, never change, and are never given to another bot, even after the bot is deleted.
Hand mail to Xieling
The relay posts each message, unchanged, once per recipient:
POST /api/v1/email/inbound?to=<recipient address>
Authorization: Bearer <INBOUND_EMAIL_SECRET>
Content-Type: message/rfc822
<the raw message>
Xieling answers 200 for every message it accepted, including mail it drops, so the relay never
retries or bounces it. 401 means the secret is wrong and 413 means the message is over 25 MB.
Treat any other error as temporary and retry.
Cloudflare Email Routing
Cloudflare receives mail for every name only on a domain’s apex, such as example-bots.com, not
on a subdomain, so set INBOUND_EMAIL_DOMAIN=example-bots.com too. Add the domain to Cloudflare,
turn on Email Routing, then deploy this Worker
with XIELING_URL set to your public URL and INBOUND_EMAIL_SECRET stored as a secret:
export default {
async email(message, env) {
const url = new URL("/api/v1/email/inbound", env.XIELING_URL);
url.searchParams.set("to", message.to);
const response = await fetch(url, {
method: "POST",
headers: {
authorization: `Bearer ${env.INBOUND_EMAIL_SECRET}`,
"content-type": "message/rfc822",
},
// Streamed as received; the Worker never holds the whole message.
body: message.raw,
});
if (response.status === 413) message.setReject("Message too large");
else if (!response.ok) throw new Error(`Xieling answered ${response.status}`);
},
};
In Email Routing → Routing rules, set the Catch-all rule to Send to a Worker and pick this Worker.
Postfix
On a host that already runs Postfix with an MX record for the domain, add a pipe transport. In
master.cf:
xieling unix - n n - - pipe
flags=Rq user=nobody argv=/usr/local/bin/xieling-inbound-email ${recipient}
In main.cf, add the domain to relay_domains and a transport map, keeping any values already
there, and hand the transport one recipient at a time:
relay_domains = <existing domains> bots.example.com
transport_maps = <existing maps> hash:/etc/postfix/transport
xieling_destination_recipient_limit = 1
Add bots.example.com xieling: to /etc/postfix/transport and run postmap on it. Save the same
INBOUND_EMAIL_SECRET where the pipe’s user can read it and no one else can:
sudo install -d -m 755 /etc/xieling
printf %s "$INBOUND_EMAIL_SECRET" | sudo install -m 400 -o nobody /dev/stdin /etc/xieling/inbound-email-secret
Save the script below as
/usr/local/bin/xieling-inbound-email, make it executable (chmod 755), and run postfix reload
once the configuration is in place. The script needs curl and jq. It bounces a message that is too large and exits with 75 for anything else that fails, so Postfix
keeps the message and tries again:
#!/bin/sh
recipient=$(printf %s "$1" | jq -sRr @uri) && [ -n "$recipient" ] || exit 75
status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 60 \
-H "Authorization: Bearer $(cat /etc/xieling/inbound-email-secret)" \
-H "Content-Type: message/rfc822" \
--data-binary @- \
"https://xieling.example.com/api/v1/email/inbound?to=$recipient")
case "$status" in
2??) exit 0 ;;
413) exit 69 ;;
*) exit 75 ;;
esac
The inbox
Each message is saved once, however often the relay delivers it, until the bot deletes it or the bot itself is deleted. Deleting mail follows the owner’s approval settings, and files kept from that mail stay in the bot’s files. A bot saves at most 500 messages a day that start nothing; past that, such mail is dropped until the day’s count falls, while mail that starts the bot or a routine is always saved. The bot reads its inbox only in its owner’s own chat, routines and mail, never in a group chat or a shared channel, and what it reads counts as outside data.
Sending
Bots send from their own address through the same provider as account email (EMAIL_PROVIDER
and its settings), so set those on the worker as well as the API. The provider must be allowed to
send as the bot domain:
- Resend: add the domain under Domains and publish the DNS records it shows. They sit on
a
sendsubdomain and the domain’s DKIM selector, so they don’t clash with the MX records that receive mail. - SMTP: the relay must accept a From address on the domain and sign it with DKIM, or recipients will mark the mail as spam.
Replies go to the bot’s address and land in its inbox. Each send follows the bot’s usual approval settings. A bot sends at most 50 emails a day, and one owner’s bots 200 between them, each to at most 10 people, and never includes a secret the run has used. What it sends stays in its inbox, and deleting mail never removes it.
Who can start work
Xieling checks each message’s DKIM signature itself and does not trust results a relay added. A message is verified only when it is signed by the domain in its From address and that signature covers its To and Cc. Many services sign with another domain, so their mail is saved as unverified: the bot can still read it but is told its sender could be forged. Signed mail that doesn’t name the bot in To or Cc, such as a BCC or a message replayed from someone else’s inbox, is saved as unverified too, without its files.
Only verified mail that names the bot in To or Cc starts anything, and never an automatic reply or mailing-list mail. The owner’s own mail sent To the bot starts it in its chat, once the owner has confirmed their account’s email address; mail that only copies the bot is saved. An email trigger starts its routine for the owner’s own address, or for anyone at the owner’s email domain when the routine allows it. Mail that claims the owner’s address but fails the check posts a note in the bot’s chat, at most once a day, without any of its content. That usually means the owner’s domain does not sign its own mail: Google Workspace and Microsoft 365 need DKIM turned on for a custom domain.
A routine’s mail reaches the bot as outside data, never as instructions. In the owner’s own mail, the bot follows what the owner asks but treats anything forwarded or quoted as data. Runs started by mail follow the stricter approval rules for outside events. Up to four attachments of verified mail are kept, within the usual upload type and size limits; the bot sees the names of any it could not keep, and of every file in unverified mail.