# `Raxol.Gateway.Adapter.Email`
[🔗](https://github.com/DROOdotFOO/raxol/blob/raxol_gateway-v0.2.0/lib/raxol/gateway/adapter/email.ex#L1)

`Raxol.Gateway.Adapter` implementation for email, both directions.

Outbound: `send_message/3` submits a plain-text message over SMTP, which is
what makes `Raxol.Gateway.Delivery`'s `{:home, route}` mode useful for cron
and background results without any platform approval process.

Inbound: `normalize_event/1` parses a raw RFC822 message into
`{:ok, route, %{text: body}}` so an email thread becomes a per-chat session
like any other platform. The adapter is a pure translator; the transport that
actually pulls mail off a mailbox lives in `Raxol.Gateway.Adapter.Email.Inbox`
(sink-agnostic, injectable), and reply threading state lives in
`Raxol.Gateway.Adapter.Email.ThreadStore`. Neither is bundled here so the
transport (IMAP/POP/Gmail API/SMTP listener) stays the deployment's choice.

Requires the optional `:gen_smtp` dependency for MIME composition, SMTP
submission (outbound), and parsing (`:mimemail`, inbound). Without it,
`send_message/3` returns `{:error, :gen_smtp_not_available}` and
`normalize_event/1` returns `:ignore`.

## Connection

`connect/1` requires `:relay` (the SMTP server) and `:from` (the sender
address) and fails fast without them. Everything else is optional:

  * `:port`, `:username`, `:password`, `:ssl`, `:tls`, `:tls_options`,
    `:auth`, `:hostname`, `:timeout`, `:retries`, `:no_mx_lookups` -
    passed through to `:gen_smtp_client` untouched
  * `:subject` - the Subject header for un-threaded sends (default
    "Raxol Gateway"); a threaded reply derives `Re: <original>` instead
  * `:thread_lookup` - a 1-arity `(route -> meta | {:ok, meta} | nil)`
    resolving the inbound message a reply threads against, where `meta` is
    the `:email` map `normalize_event/1` surfaced (`:message_id`,
    `:references`, `:subject`). `ThreadStore.thread_lookup_fn/1` builds one;
    absent, replies carry only their own generated `Message-ID`.
  * `:message_id_fn` - a 0-arity override generating the outbound
    `Message-ID`; the default derives one from the `:from` domain
  * `:send_fn` - a 2-arity `(email_tuple, relay_opts) -> receipt |
    {:error, ...}` override for tests or alternative submission paths;
    the default calls `:gen_smtp_client.send_blocking/2`

The connection is stateless; the returned handle is the validated
option list. Routes address a mailbox: `chat_id` is the address
(`platform: :email, chat_type: :dm, chat_id: "ops@example.com"`).

## Inbound

`normalize_event/1` accepts a raw RFC822 binary (or `%{rfc822: binary}`),
parses it with `:mimemail.decode`, and routes on the normalized sender
address (`chat_type: :dm`, `chat_id` = the lower-cased address, display name
and angle brackets stripped). The event is
`%{text: body, email: meta}` where `body` is the first `text/plain` part
with quoted history trimmed and `meta` carries `:message_id`, `:in_reply_to`,
`:references`, `:subject`, and `:from` for threading and session wiring.
A message with no parseable sender or no `text/plain` part, non-mail input,
or (when `:mimemail` is absent) any input, returns `:ignore`. Parsing never
raises: a malformed message is `:ignore`, not a crash.

## Security: the sender address is unauthenticated

The `From` header is trivially forgeable, and this adapter does NOT verify
SPF/DKIM/DMARC. The route's `chat_id`/`user_id` are the parsed sender address,
so any authorization keyed on them (`Raxol.Gateway.Pairing.authorize/2`,
allowlists) is only as trustworthy as the mail path that produced the message.
Run inbound email behind an MTA that rejects or authenticates spoofed senders,
and treat a raw-inbound feed with no upstream authentication as anonymous. The
agent runs tools and can spend, so a spoofed `From` reaching a paired session
is a privilege escalation. If you need in-process verification, gate the feed's
`:on_message` on the parsed `Authentication-Results` header before routing.

## Outbound

The rendered reply becomes the text/plain body (charset utf-8,
quoted-printable), one message per send - email has no chat-style
length limit, so nothing is chunked. Every send carries a generated
`Message-ID`; when `:thread_lookup` resolves the inbound message being
replied to, the send also sets `In-Reply-To` and `References` (and a
`Re:`-prefixed Subject) so mail clients keep the conversation together.
Empty or whitespace-only replies are a no-op. Failures emit
`[:raxol_gateway, :email_adapter, :error]` telemetry with the classified
reason only; the conn (which may carry the SMTP password) is never logged.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
