LemonChannels.Adapters.Email (lemon_channels v0.1.0)

View Source

Email channel adapter (Phase 2.4/B3).

Email is the one gateway ingress surface that is genuinely message-shaped, so it is the one ported to LemonChannels.Plugin; see docs/platform/transport-unification.md for why webhook, SMS and voice are staying in lemon_gateway.

Both halves are here: inbound parses a provider webhook payload into a LemonCore.InboundMessage, and deliver/1 sends the reply over SMTP through LemonChannels.Adapters.Email.Outbound.

Threading

Email's References header is a list of ancestors, while LemonCore.InboundMessage's reply_to_id is a single optional id. The adapter therefore owns its own thread resolution: thread_id/1 computes a stable id from the message alone, and LemonChannels.Adapters.Email.ThreadStore remembers which thread each message id belongs to — so a chain met out of order still converges, and so the reply has a subject and a reference chain to send, neither of which the outbound payload carries. This is channel-owned state, the same way Telegram owns its message-id tables, not a gap in the Plugin contract.

Configuring

config :lemon_channels, LemonChannels.Adapters.Email,
  webhook_token: "…",
  from: "agent@example.com",
  smtp_relay: "mail.example.com",
  smtp_username: "…",
  smtp_password: "…"

The canonical TOML [gateway] config's email block is read as well, so a deployment configured while email lived in the gateway keeps working. See LemonChannels.Adapters.Email.Config.

Receiving — off until you turn it on

The adapter is registered by default, but inbound mail is not. Outbound works as soon as a relay is configured; receiving takes two more deliberate steps, because an inbound mail endpoint is a public door into an agent and nobody should open one by upgrading:

config :lemon_channels, LemonChannels.InboundHttp,
  enabled: true,
  port: 4090,
  ip: {127, 0, 0, 1}

config :lemon_channels, LemonChannels.Adapters.Email,
  webhook_token: "a long random string"

With the listener on, the adapter registers LemonChannels.Adapters.Email.Webhook at POST /email, and your provider should send there with the token in an x-webhook-token header. Without the token every request is refused with a 401 — an open endpoint would be a spam relay into someone's agent — and without the listener nothing binds a port at all. This is the same posture the gateway transport it replaced had: dead until explicitly enabled.

Point the listener at loopback and put a reverse proxy in front of it unless you have a reason not to.

Summary

Functions

Registers the webhook handler. Returns :ignore so the adapter occupies no process — all it needs is a route.

Resolves a stable thread id from a message alone.

Functions

start_link()

@spec start_link() :: :ignore

Registers the webhook handler. Returns :ignore so the adapter occupies no process — all it needs is a route.

thread_id(parsed)

@spec thread_id(map()) :: binary()

Resolves a stable thread id from a message alone.

Prefers the oldest ancestor the message names so every message in a chain lands on the same id regardless of which one arrives first: the first entry of References, else In-Reply-To, else the message's own id. Falls back to the subject with any reply/forward prefixes stripped, so a client that drops threading headers still groups sensibly.

Pure, and the seed rather than the last word: normalize_inbound/1 asks LemonChannels.Adapters.Email.ThreadStore first, which knows the threads of messages this adapter has already seen and can therefore join a chain whose members name different ancestors.