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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
# 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/http2lmtp /usr/local/bin/
cp http2lmtp.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.
## Environment
All configuration is read from the environment at startup. Missing
required variables cause the process to exit before binding.
| Variable | Required | Default | Description |
|---|---|---|---|
| `INGEST_TOKEN` | yes | — | Shared secret. Requests must carry `Authorization: Bearer <token>`. Compared in constant time. |
| `LMTP_SOCK` | yes | — | Path to the LMTP unix socket, e.g. `/var/run/dovecot/lmtp`. The process must have write access to it. |
| `LISTEN_ADDR` | no | `127.0.0.1:8080` | Address and port for the HTTP listener. |
| `LMTP_RCPT` | no | — | Deliver every message to this single recipient instead of the address in `x-envelope-to`. The original recipient is preserved as an `X-Original-To:` header. Unset: the envelope recipient is passed through as-is. |
| `RUST_LOG` | no | — | Log filter, e.g. `http2lmtp=info`. Standard `tracing-subscriber` syntax. |
### Example
```sh
INGEST_TOKEN=$(openssl rand -base64 32)
LMTP_SOCK=/var/run/dovecot/lmtp
LISTEN_ADDR=127.0.0.1:1234
LMTP_RCPT=[email protected]
RUST_LOG=http2lmtp=info
```
|