# Bot email addresses

Give each bot its own address, such as `scout@bots.example.com`. 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](/docs/routines/). Bots can also [send email](#sending) 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:

```bash
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:

```http
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:

```js
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`:

```text
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:

```text
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:

```sh
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:

```sh
#!/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](/docs/approvals/) 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 `send` subdomain 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](/docs/approvals/) 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](/docs/approvals/) 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.
