AWS Lambda Function for Email2Webhook
AWS Lambda Function that receives inbound email through Amazon SES and forwards it as a signed HTTP webhook, so a downstream system (for example, a support ticketing workflow) can process it.
How it works
Amazon SES invokes this function directly through a receipt rule’s Lambda action (no SNS topic involved). The Lambda action never delivers the email body, only metadata, so the receipt rule must also have an S3 action placed before the Lambda action to store the raw MIME message. This function:
- Reads the
messageIdfrom the SES event. - Downloads the raw email from the S3 bucket (object key =
messageId, optionally prefixed). - Builds an envelope:
{"meta": {"source": "aws-ses", "format": "...", "version": "1.0.0"}, "data": {...}}.meta.format(WEBHOOK_FORMAT) selects which builder producesdata, so the shape ofdatais fully determined byformat. See “Output formats” below. - Signs the envelope (
WEBHOOK_SIGNATURE) and sends it as a POST request to the configured webhook.
Output formats (WEBHOOK_FORMAT)
-
postal(default) —datamatches the JSON shape Postal sends for HTTP endpoints configured withEncoding=BodyAsJSONandFormat=Hash(verified against Postal’s own source,app/senders/http_sender.rb), so an existing Postal-based integration can receive it without changes to its parsing logic. Two fields have no real SES equivalent and are approximated:id/tokenare Postal’s internal sequential integer ID and per-message secret; both are filled with SES’smessageId(a string, not an integer) since nothing downstream is known to rely onidbeing numeric.spam_statusis derived from SES’sspamVerdict.statusvia theSPAM_STATUS_MAPdict inlambda_function.py(edit it directly if the mapping needs to change) —GRAYandPROCESSING_FAILEDare mapped to"Spam"on purpose: an uncertain or failed scan shouldn’t be trusted as much as aPASS, so it gets the same downstream priority as a confirmedFAILinstead of being silently treated as clean.received_with_sslis alwaysnull— SES doesn’t report whether the original SMTP session used TLS.bounceis alwaysfalse— an inbound SES message is never a bounce notification.
-
ses—datais the envelope’s own native representation, no MIME parsing performed by this function:{"ses": {"mail": {...}, "receipt": {...}}, "email_base64": "<raw .eml, base64>"}. Use this for a receiver that wants to parse the MIME message itself.
Adding a new output format means adding one function to DATA_BUILDERS in lambda_function.py — nothing else in the function needs to change.
Signing (WEBHOOK_SIGNATURE)
hmac(default) — HMAC-SHA256 of the exact request body, sent asX-Webhook-Signature-256: <hex digest>, with no prefix by default.WEBHOOK_SIGNATURE_PREFIXprepends any literal string to the digest if the receiving end expects one (e.g. set it tosha256=for the convention GitHub webhooks use) — see “Verifying the webhook signature” below.none— no signature header sent.
There is intentionally no rsa option that mimics Postal’s own signature scheme (X-Postal-Signature-256, RSA-SHA256 signed with Postal’s private key): that private key lives on the Postal server, and reusing it here would mean sharing one signing key across two independent systems, which is the wrong tradeoff for what it saves. If your receiving system already authenticates some other source using its own signature scheme, add a separate branch or webhook trigger for this Lambda’s hmac signature instead of trying to make this function reproduce that other scheme — the format: "postal" envelope already gets the data shape right, only the authentication step needs its own path.