# 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 </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.