Gateway for the optional ex_aws / ex_aws_s3 dependencies
({:ex_aws, "~> 2.7"} + {:ex_aws_s3, "~> 2.5"}).
Phase 46's SES inbound provider fetches the raw MIME body of a received
message from the adopter's S3 bucket (the SES receipt-rule S3 action stores
the message at s3://{bucketName}/{objectKey}). The real fetch implementation
(MailglassInbound.S3Fetcher.ExAwsS3) routes all ExAws/ExAws.S3
access through this gateway; the fake-adapter-first test default
(MailglassInbound.S3Fetcher.Fake) needs no AWS dependency at all (D-13).
STACK-lock departure (D-46-20)
ex_aws/ex_aws_s3 are the first new optional runtime deps since the v1.0
STACK lock ("Optional deps: Add none"). The addition is deliberate and is
recorded in the inbound CHANGELOG. Both deps are optional, so a default install
carries no AWS footprint; adopters who run SES inbound add :ex_aws,
:ex_aws_s3, an HTTP client (:hackney/:req), and :sweet_xml themselves
(Phase 50 setup guide). Credentials resolve via ex_aws's standard chain
(env → pod-identity → instance/task role) with no mailglass-specific config
(D-46-15).
Inbound-local placement (D-46-14)
This gateway lives in mailglass_inbound, NOT core. MailglassInbound
"keeps optional runtime integrations behind its own gateway surface instead of
reusing Mailglass.OptionalDeps.* across package boundaries." SESI-04's
literal wording Mailglass.OptionalDeps.ExAwsS3 is an erratum — the consumer
(S3Fetcher.ExAwsS3) lives in inbound, and core's NoBareOptionalDepReference
is scoped to lib/mailglass/. The NoBareOptionalDepReference Credo check
gates ExAws/ExAws.S3 to this module; bare references anywhere else in
inbound code are forbidden so mix compile --no-optional-deps --warnings-as-errors stays green.
get_object/2 — never raises, gates on available?/0
get_object/2 short-circuits on available?/0 (the documented normal
degraded path) and only then wraps ExAws.S3.get_object/2 |> ExAws.request/1
in try/rescue AND catch :exit (the defensive backstop, mirroring
Mailglass.OptionalDeps.GenSmtp.decode/2).
The absent-dep case is tagged distinctly as
{:error, {:s3_fetch_failed, :ex_aws_unavailable}} rather than collapsing to
the generic :undef rescue. This matters for the retry layer (WR-06): a
{:s3_fetch_failed, _} reason is classified non-retryable by
MailglassInbound.S3Fetcher.Retry.retryable?/1, so a dep-absent deployment
fails fast on the FIRST attempt instead of burning the full retry budget and
every backoff sleep on a config error that no amount of retrying can fix.
Summary
Functions
@spec available?() :: boolean()
Returns true when :ex_aws_s3 (ExAws.S3) is loaded in the current runtime.
Probes ExAws.S3 as the proxy for the ex_aws/ex_aws_s3 pair: the real S3
fetch entry point is ExAws.S3.get_object/2, so ExAws.S3 is the meaningful
availability signal.
Fetches an S3 object, never raising.
Returns {:ok, %{body: binary(), ...}} on success (the ExAws response map —
the caller extracts :body).
When the optional dep is absent, returns
{:error, {:s3_fetch_failed, :ex_aws_unavailable}} (WR-06) — a non-retryable
config error the retry layer short-circuits on, distinct from a genuine
transient AWS/HTTP failure.
Otherwise returns a tagged {:error, {kind, reason}} tuple where kind is
:error (rescued exception — the defensive backstop) or :exit (a process
EXIT from the HTTP client). All ExAws/ExAws.S3 access is confined to this
function.