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
@spec start_link() :: :ignore
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.
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.