Raxol.Gateway.Adapter.Email.Inbox (RaxolGateway v0.2.0)

Copy Markdown View Source

Polls a mailbox for new messages and hands each raw message to a caller-supplied function.

This is the inbound feed for Raxol.Gateway.Adapter.Email: the stateful loop that turns a mailbox into a stream of raw RFC822 messages. It is transport-agnostic on purpose -- gen_smtp only speaks SMTP, and the mailbox a deployment reads (IMAP, POP3, the Gmail API, or an SMTP listener) is its choice, so the fetch is injected. There is no default :fetch_fn, the same way Raxol.Gateway.Pipeline.Transcribe has no default audio fetch: the transport is environment-specific.

It mirrors Raxol.Telegram.UpdatePoller: sink-agnostic (:on_message decides what a message means), authorize-before-route in the sink, and exponential backoff on fetch errors. A gateway wiring:

{:ok, store} = ThreadStore.start_link(name: MyThreads)

Raxol.Gateway.Adapter.Email.Inbox.start_link(
  fetch_fn: fn cursor -> MyImap.fetch_since(cursor) end,
  on_message: fn raw ->
    case Raxol.Gateway.Adapter.Email.normalize_event(raw) do
      {:ok, route, event} ->
        # NOTE: `route` is keyed on the unauthenticated `From` header.
        # authorize is only meaningful behind an MTA that verifies the
        # sender (SPF/DKIM/DMARC) -- see the `Adapter.Email` "Security"
        # section. A raw feed with no upstream auth is anonymous.
        with :allow <- Raxol.Gateway.Pairing.authorize(MyPairing, route) do
          Raxol.Gateway.Adapter.Email.ThreadStore.record_event(store, route, event)
          Raxol.Gateway.SessionRouter.route(MyRouter, route, event)
        else
          :deny -> Logger.info("unauthorized sender denied")
        end

      :ignore ->
        :ok
    end
  end
)

Options

  • :fetch_fn (required) - (cursor -> {:ok, [raw], next_cursor} | {:error, reason}). Fetches messages newer than cursor and returns the cursor to poll from next. cursor starts at :initial_cursor (default nil). The transport owns durable cursor state (e.g. an IMAP UIDVALIDITY/UID checkpoint), so a restart resumes where the transport left off rather than re-reading the mailbox.
  • :on_message (required) - 1-arity function called per raw message, in order. A crash inside it is caught, logged, and reported as [:raxol_gateway, :email_inbox, :handler_error] telemetry; the loop continues and the cursor still advances (holding it would let one poison message stall the feed), so the failed message is dropped -- a consumer that needs exactly-once must dedup on Message-ID, and an operator should alert on that telemetry to catch drops from a transient downstream outage.
  • :interval_ms - delay between polls (default 60_000). Applied after each completed poll, empty or not.
  • :max_bytes - a raw message larger than this is dropped before on_message (default 10MB), with a Logger.warning and [:raxol_gateway, :email_inbox, :oversized] telemetry. Bounds the memory :mimemail.decode spends parsing untrusted mail into part trees.
  • :initial_cursor - the cursor for the first fetch (default nil).
  • :name - optional registered name.

Errors

A :fetch_fn error backs off exponentially (1s doubling, capped at 60s) and resets on the next success. The fetch reason is logged classified only -- the :fetch_fn closure may capture mailbox credentials, so neither it nor the state is ever dumped (format_status/1 redacts both).

Summary

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

handle_manager_call(request, from, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_call/3.

handle_manager_cast(msg, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.

start_link(init_opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()