1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
# http2lmtp
Recieve HTTP-enveloped mail and route it as LMTP to local Unix socket.
Cloudflare Email Workers can only reach a cf tunnel over HTTP, so the
message has to be wrapped in an HTTP request to get through. On the
server side, `http2lmtp` unwraps it and hands it to an LMTP-capable
MDA such as Dovecot.
## Quickstart
cat <<EOF >/etc/ingest.env
INGEST_TOKEN=$(uuidgen)
LMTP_SOCK=/var/run/dovecot/lmtp
LISTEN_ADDR=127.0.0.1:1234
RUST_LOG=info
EOF
cargo build --release
cp target/release/ingest /usr/local/bin/
cp mail-ingest.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now mail-ingest.service
## Example client
```ts
export default {
async email(message, env, ctx) {
const res = await fetch("https://example.com/inject", {
method: "POST",
headers: {
"authorization": `Bearer ${env.INGEST_TOKEN}`,
"content-type": "message/rfc822",
"x-envelope-from": message.from,
"x-envelope-to": message.to,
},
body: message.raw,
});
if (!res.ok) {
const detail = await res.text().catch(() => "");
throw new Error(`ingest ${res.status} ${detail.slice(0, 200)}`);
}
},
};
```
## Response codes
| Status | Meaning | Client action |
|---|---|---|
| `204 No Content` | Message accepted by the MDA | Done |
| `401 Unauthorized` | Missing or wrong bearer token | Fix config; do not retry |
| `413 Payload Too Large` | Body exceeds 26 MiB | Do not retry |
| `422 Unprocessable Entity` | LMTP replied with a permanent error (5xx), e.g. unknown recipient | Do not retry; bounce |
| `503 Service Unavailable` | LMTP unreachable or replied with a temporary error (4xx) | Retry later |
Note: `http2lmtp` itself never retries. The client is responsible for
it.
|