# 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/http2lmtp.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 `. 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=you@example.com RUST_LOG=http2lmtp=info ```