From 8642bc9386eb2b08a74dfbe7ec52fb2864db3f28 Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:10:34 -0400 Subject: [PATCH] docs(inbound-email): document AWS SES adapter across greenfield frameworks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third provider alongside Postmark + Mailgun. AWS SES receipt rules publish to an SNS topic; host apps subscribe via HTTP and SNS POSTs the envelope to the same /...webhook/email/inbound endpoint, with ?adapter=ses selecting the parser. Each greenfield framework page (dotnet/spring/go/phoenix/symfony) gains an "AWS SES" subsection under Provider setup with: - SNS subscription URL and adapter label. - How to handle the one-time SubscriptionConfirmation envelope — the typed exception / tuple the parser returns + what the host controller should do with SubscribeURL. - Where to inject the X-Escalated-Inbound-Secret header when SNS itself doesn't forward custom headers (infrastructure-level: load balancer / API gateway / CDN / edge proxy). - What receipt rule action + encoding to choose for full MIME content delivery (action.type=SNS, encoding=BASE64), plus the fallback behavior when the rule doesn't supply content (metadata-only routing still works via threading). _intro.md now lists SES alongside Postmark + Mailgun and the adapter selector shows `postmark|mailgun|ses` in every framework row of the webhook URL table. Phoenix page additionally shows the updated :inbound_parsers list that includes SESParser. --- sections/inbound-email/_intro.md | 12 ++++++------ sections/inbound-email/dotnet.md | 13 ++++++++++++- sections/inbound-email/go.md | 13 ++++++++++++- sections/inbound-email/phoenix.md | 24 +++++++++++++++++++++++- sections/inbound-email/spring.md | 13 ++++++++++++- sections/inbound-email/symfony.md | 13 ++++++++++++- 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/sections/inbound-email/_intro.md b/sections/inbound-email/_intro.md index f228c82..601cf24 100644 --- a/sections/inbound-email/_intro.md +++ b/sections/inbound-email/_intro.md @@ -1,6 +1,6 @@ # Inbound Email -Create and reply to tickets directly from incoming emails. Escalated supports **Postmark** and **Mailgun** webhooks out of the box, with an open parser interface for plugging in additional providers. +Create and reply to tickets directly from incoming emails. Escalated supports **Postmark**, **Mailgun**, and **AWS SES** (via SNS HTTP subscription) out of the box, with an open parser interface for plugging in additional providers. ## How It Works @@ -15,11 +15,11 @@ All greenfield framework ports (.NET, Spring, Go, Phoenix, Symfony) expose a **s | Framework | Webhook URL | | --- | --- | -| .NET | `POST /support/webhook/email/inbound?adapter=` | -| Spring Boot | `POST /escalated/webhook/email/inbound?adapter=` | -| Go | `POST /escalated/webhook/email/inbound?adapter=` | -| Phoenix | `POST /support/webhook/email/inbound?adapter=` | -| Symfony | `POST /escalated/webhook/email/inbound?adapter=` | +| .NET | `POST /support/webhook/email/inbound?adapter=` | +| Spring Boot | `POST /escalated/webhook/email/inbound?adapter=` | +| Go | `POST /escalated/webhook/email/inbound?adapter=` | +| Phoenix | `POST /support/webhook/email/inbound?adapter=` | +| Symfony | `POST /escalated/webhook/email/inbound?adapter=` | The legacy host-app integrations (Laravel, Rails, Django, Adonis, Filament, WordPress) expose provider-specific endpoints — see the respective framework page for the exact URL. diff --git a/sections/inbound-email/dotnet.md b/sections/inbound-email/dotnet.md index 660cc82..9c4a47c 100644 --- a/sections/inbound-email/dotnet.md +++ b/sections/inbound-email/dotnet.md @@ -1,6 +1,6 @@ ### Webhook endpoint -The .NET bundle exposes a single webhook for all providers. Configure Postmark and/or Mailgun to POST inbound mail to: +The .NET bundle exposes a single webhook for all providers. Configure Postmark, Mailgun, or AWS SES (via SNS) to POST inbound mail to: ``` POST /support/webhook/email/inbound?adapter=postmark @@ -52,6 +52,17 @@ https://yourapp.com/support/webhook/email/inbound?adapter=mailgun Set the HMAC header the same way. +**AWS SES** — create an SES receipt rule that publishes to an SNS topic, then subscribe your webhook URL to that topic: + +``` +https://yourapp.com/support/webhook/email/inbound?adapter=ses +``` + +SES-specific notes: +- **Subscription confirmation** — AWS SNS sends a one-time `SubscriptionConfirmation` envelope when you first subscribe the endpoint. The bundle throws `SESSubscriptionConfirmationException` carrying the `SubscribeUrl`; your host controller should catch it and GET that URL to activate the subscription (then return `200 OK`). +- **Custom headers** — SNS doesn't forward per-request custom headers from you, but it signs each delivery itself. Since the endpoint is secret-key-guarded, configure your infrastructure (load balancer, API gateway, or CloudFront) to inject the `X-Escalated-Inbound-Secret` header on requests to the SES path. +- **Body extraction** — configure the SES receipt rule with action type `SNS` and encoding `BASE64` to receive the full raw MIME body. The bundle decodes `text/plain`, `text/html`, and `multipart/alternative` bodies. Without full content, metadata (from/to/subject/threading headers) is still extracted and tickets still route correctly via Message-ID threading. + ### Testing ```bash diff --git a/sections/inbound-email/go.md b/sections/inbound-email/go.md index 2707a2f..5fea52b 100644 --- a/sections/inbound-email/go.md +++ b/sections/inbound-email/go.md @@ -1,6 +1,6 @@ ### Webhook endpoint -The Go module exposes a single webhook handler for all providers. Configure Postmark and/or Mailgun to POST inbound mail to: +The Go module exposes a single webhook handler for all providers. Configure Postmark, Mailgun, or AWS SES (via SNS) to POST inbound mail to: ``` POST /escalated/webhook/email/inbound?adapter=postmark @@ -57,6 +57,17 @@ https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun Set the HMAC header the same way. +**AWS SES** — create an SES receipt rule that publishes to an SNS topic, then subscribe your webhook URL to that topic: + +``` +https://yourapp.com/escalated/webhook/email/inbound?adapter=ses +``` + +SES-specific notes: +- **Subscription confirmation** — AWS SNS sends a one-time `SubscriptionConfirmation` envelope when you first subscribe the endpoint. The parser returns a sentinel `email.ErrSESSubscriptionConfirmation` wrapping an `*email.SESSubscriptionConfirmation` that carries the `SubscribeURL`. Use `errors.As` to unwrap it; your handler should GET that URL to activate the subscription (then return 200). +- **Custom headers** — SNS doesn't forward per-request custom headers, but it signs each delivery itself. Since the endpoint is secret-key-guarded, configure your infrastructure (load balancer, API gateway, or CDN) to inject the `X-Escalated-Inbound-Secret` header on requests to the SES path. +- **Body extraction** — configure the SES receipt rule with action type `SNS` and encoding `BASE64` to receive the full raw MIME body. The module decodes `text/plain`, `text/html`, and `multipart/alternative` bodies using stdlib `net/mail` + `mime/multipart`. Without full content, threading metadata is still extracted and tickets still route correctly via Message-ID threading. + ### Testing ```bash diff --git a/sections/inbound-email/phoenix.md b/sections/inbound-email/phoenix.md index fbb074d..38783a4 100644 --- a/sections/inbound-email/phoenix.md +++ b/sections/inbound-email/phoenix.md @@ -1,6 +1,6 @@ ### Webhook endpoint -The Phoenix library exposes a single webhook controller for all providers. Configure Postmark and/or Mailgun to POST inbound mail to: +The Phoenix library exposes a single webhook controller for all providers. Configure Postmark, Mailgun, or AWS SES (via SNS) to POST inbound mail to: ``` POST /support/webhook/email/inbound?adapter=postmark @@ -56,6 +56,28 @@ https://yourapp.com/support/webhook/email/inbound?adapter=mailgun Set the HMAC header the same way. +**AWS SES** — create an SES receipt rule that publishes to an SNS topic, then subscribe your webhook URL to that topic: + +``` +https://yourapp.com/support/webhook/email/inbound?adapter=ses +``` + +SES-specific notes: +- **Subscription confirmation** — AWS SNS sends a one-time `SubscriptionConfirmation` envelope when you first subscribe the endpoint. The parser returns `{:error, {:ses_subscription_confirmation, %{subscribe_url: ..., topic_arn: ..., token: ...}}}`. Match on the tuple in your controller and GET the `subscribe_url` to activate the subscription (then return 200). +- **Custom headers** — SNS doesn't forward per-request custom headers, but it signs each delivery itself. Since the endpoint is secret-key-guarded, configure your infrastructure (load balancer, API gateway, or edge proxy) to inject the `x-escalated-inbound-secret` header on requests to the SES path. +- **Body extraction** — configure the SES receipt rule with action type `SNS` and encoding `BASE64` to receive the full raw MIME body. The library's hand-rolled splitter decodes `text/plain`, `text/html`, `multipart/alternative`, and `quoted-printable` transfer encoding — no external MIME dep. Without full content, threading metadata is still extracted and tickets still route correctly via Message-ID threading. + +Register the SES parser alongside the others in `config/runtime.exs`: + +```elixir +config :escalated, + inbound_parsers: [ + Escalated.Services.Email.Inbound.PostmarkParser, + Escalated.Services.Email.Inbound.MailgunParser, + Escalated.Services.Email.Inbound.SESParser + ] +``` + ### Testing ```bash diff --git a/sections/inbound-email/spring.md b/sections/inbound-email/spring.md index a3f2361..9991c41 100644 --- a/sections/inbound-email/spring.md +++ b/sections/inbound-email/spring.md @@ -1,6 +1,6 @@ ### Webhook endpoint -The Spring Boot starter exposes a single webhook for all providers. Configure Postmark and/or Mailgun to POST inbound mail to: +The Spring Boot starter exposes a single webhook for all providers. Configure Postmark, Mailgun, or AWS SES (via SNS) to POST inbound mail to: ``` POST /escalated/webhook/email/inbound?adapter=postmark @@ -49,6 +49,17 @@ https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun Set the HMAC header the same way. +**AWS SES** — create an SES receipt rule that publishes to an SNS topic, then subscribe your webhook URL to that topic: + +``` +https://yourapp.com/escalated/webhook/email/inbound?adapter=ses +``` + +SES-specific notes: +- **Subscription confirmation** — AWS SNS sends a one-time `SubscriptionConfirmation` envelope when you first subscribe the endpoint. The starter throws `SESSubscriptionConfirmationException` carrying `getSubscribeUrl()`; your controller should catch it and GET that URL to activate the subscription (then return 200). +- **Custom headers** — SNS doesn't forward per-request custom headers, but it signs each delivery itself. Since the endpoint is secret-key-guarded, configure your infrastructure (load balancer, API gateway, or ingress) to inject the `X-Escalated-Inbound-Secret` header on requests to the SES path. +- **Body extraction** — configure the SES receipt rule with action type `SNS` and encoding `BASE64` to receive the full raw MIME body. The starter decodes `text/plain`, `text/html`, and `multipart/alternative` bodies via `jakarta.mail`. Without full content, threading metadata is still extracted and tickets still route correctly via Message-ID threading. + ### Testing ```bash diff --git a/sections/inbound-email/symfony.md b/sections/inbound-email/symfony.md index beb34d2..5964b65 100644 --- a/sections/inbound-email/symfony.md +++ b/sections/inbound-email/symfony.md @@ -1,6 +1,6 @@ ### Webhook endpoint -The Symfony bundle exposes a single webhook controller for all providers. Configure Postmark and/or Mailgun to POST inbound mail to: +The Symfony bundle exposes a single webhook controller for all providers. Configure Postmark, Mailgun, or AWS SES (via SNS) to POST inbound mail to: ``` POST /escalated/webhook/email/inbound?adapter=postmark @@ -48,6 +48,17 @@ https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun Set the HMAC header the same way. +**AWS SES** — create an SES receipt rule that publishes to an SNS topic, then subscribe your webhook URL to that topic: + +``` +https://yourapp.com/escalated/webhook/email/inbound?adapter=ses +``` + +SES-specific notes: +- **Subscription confirmation** — AWS SNS sends a one-time `SubscriptionConfirmation` envelope when you first subscribe the endpoint. The bundle throws `SESSubscriptionConfirmationException` carrying `$subscribeUrl`; your controller should catch it and GET that URL to activate the subscription (then return 202). +- **Custom headers** — SNS doesn't forward per-request custom headers, but it signs each delivery itself. Since the endpoint is secret-key-guarded, configure your infrastructure (load balancer, API gateway, or edge proxy) to inject the `X-Escalated-Inbound-Secret` header on requests to the SES path. +- **Body extraction** — configure the SES receipt rule with action type `SNS` and encoding `BASE64` to receive the full raw MIME body. The bundle's hand-rolled splitter decodes `text/plain`, `text/html`, `multipart/alternative`, and `quoted-printable` transfer encoding — no external MIME dep. Without full content, threading metadata is still extracted and tickets still route correctly via Message-ID threading. + ### Testing ```bash