@composurecdk/ses
v0.11.2
Published
Compose-native builders for AWS SES — email identities, receipt rule sets, filters, and rule actions
Maintainers
Readme
@composurecdk/ses
Compose-native builders for AWS SES, part of ComposureCDK.
SES is Amazon's email service for sending and receiving mail. This package currently covers the receiving path — email identities, receipt rule sets, receipt filters, and rule actions. Sending-side builders (configuration sets, dedicated IP pools, VDM, reputation alarms) will follow.
Install
npm install @composurecdk/sesPeer dependencies: @composurecdk/core, @composurecdk/logs (for the activation provider's log group), @composurecdk/route53 (for .publishDkim() / .publishInboundMx()), aws-cdk-lib, constructs.
Email identity
An SES email identity is a domain or email address you've verified ownership of, so SES will send or receive mail for it. This builder verifies the identity and sets up DKIM signing.
import { createEmailIdentityBuilder } from "@composurecdk/ses";
import { ref } from "@composurecdk/core";
const zone = ref<HostedZoneBuilderResult>("zone").get("hostedZone");
const { emailIdentity, dkim } = createEmailIdentityBuilder()
.domain("ask.example.com") // .email(...) / .publicHostedZone(...) also
.easyDkim() // default; .byoDkim({ selector, privateKey, publicKey }) for BYODKIM
.publishDkim(zone)
.publishInboundMx(zone) // route the domain's mail to SES
.build(stack, "MailIdentity");.domain()/.email()/.publicHostedZone()— the three identity variants..publicHostedZone(zone)verifies the zone apex and lets CDK auto-publish DKIM into it — the concise "I own the whole zone" form..publishDkim(zone)— for the.domain()case (e.g. a subdomain whose apex lives elsewhere), publishes the DKIM DNS records intozone: three CNAMEs for Easy DKIM, one TXT for BYODKIM. Mutually exclusive with.publicHostedZone()(which already publishes); not valid for an email identity..publishInboundMx(zone, { priority? })— publishes the MX record that routes the domain's mail to SES's inbound endpoint (inbound-smtp.<region>.amazonaws.com, derived from the stack's Region). Without it senders have nowhere to deliver, however the rule set is configured. Valid for.domain()and.publicHostedZone()— CDK's zone form publishes DKIM but never a receiving MX. Priority defaults to10(DEFAULT_INBOUND_MX_PRIORITY). Emits the@composurecdk/ses:receiving-regionwarning when the stack's Region can't receive mail..mailFromDomain(...)— a custom MAIL FROM domain; defaults toREJECT_MESSAGEon MX failure (no insecure fallback toamazonses.com, preserving SPF/DMARC alignment).- The result exposes
dkim— the identity's DKIM DNS records as{ name, value }[](CDK'sdkimRecords), for manual publication when a zone isn't available — anddkimRecords/inboundMxRecords(the Route 53 records) when.publishDkim()/.publishInboundMx()were used.
Receipt rule set
A receipt rule set is an ordered list of rules that tell SES how to handle inbound mail. Each rule matches recipients and runs an ordered list of actions (store to S3, invoke a Lambda, bounce, …).
import { createReceiptRuleSetBuilder, s3Action, lambdaAction } from "@composurecdk/ses";
import { ref } from "@composurecdk/core";
const { ruleSet, rules } = createReceiptRuleSetBuilder()
.rule("inbound", (r) =>
r
.recipients(["[email protected]"])
.addAction(
"store",
s3Action(ref<BucketBuilderResult>("mailBucket").get("bucket"), {
objectKeyPrefix: "inbound/",
}),
)
.addAction("process", lambdaAction(ref<FunctionBuilderResult>("intake").get("function"))),
)
.build(stack, "MailRuleSet");- Rules run in declaration order, and actions run in the order added — ordering is semantic (a
Stop/Bounceaction halts later rules), so it is preserved. .addAction(key, action)takes any CDKIReceiptRuleAction. Thes3Action/lambdaAction/snsAction/bounceAction/stopAction/addHeaderActionfree functions are optional convenience helpers — they wrap the CDKaws-ses-actionsclasses and acceptResolvables so a rule canref()sibling components. For an action type we don't yet provide a helper for, construct the CDK action and pass it to.addAction()directly.s3Actioninjects the SES bucket policy transparently and, when given akmsKey, grantsses.amazonaws.comthe encrypt permissions the key needs — encryption at rest works out of the box.
Secure defaults
Every rule gets AWS-recommended defaults, each individually overridable:
| Default | Value | Why |
| ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| scanEnabled | true | Spam & virus scanning (CloudFormation defaults it off). |
| tlsPolicy | REQUIRE | Encrypt in transit. ~95%+ of legitimate inbound mail already uses TLS, and a non-TLS sender receives a hard bounce (fail-loud, not a silent drop). Override with .tlsPolicy(TlsPolicy.OPTIONAL) for a domain that must accept mail from legacy senders without STARTTLS. |
Migrating from raw
aws-cdk-libconstructs?tlsPolicydefaults toREQUIREhere, whereas CloudFormation defaults toOPTIONAL— a deliberately more secure posture, but a behaviour change on migration. Senders without STARTTLS will be bounced; set.tlsPolicy(TlsPolicy.OPTIONAL)per rule to retain the raw-construct behaviour.
Activation (on by default)
A receipt rule set is inert until it is the account's active rule set — a separate, account-level ses:SetActiveReceiptRuleSet call that has no CloudFormation resource (SES docs). Miss it and mail silently vanishes, so .activate() is on by default (opt out with .activate(false)).
Activation is backed by a purpose-built provider (see ADR-0016) that conditionally deactivates: on delete it clears the active slot only if the currently-active set is this one, so tearing down a stack never disables another stack's rule set. The custom resource is exposed on the result as activation. It depends on every rule, so the set never goes live empty mid-deploy, and on teardown it is deactivated before its rules are removed.
Both provider Lambdas (the handler and CDK's provider framework) write to one declared log group, activationLogGroup on the result, carrying the @composurecdk/logs defaults (two-year retention, retained on delete). Without it Lambda would create /aws/lambda/<name> on first invocation with indefinite retention, outside the template. Change its removal policy with activationLogGroup.applyRemovalPolicy(...).
Only one rule set is active per account/region. If you run multiple rule sets across stacks and need bespoke arbitration, disable activation here and drive
setActiveReceiptRuleSetyourself with@composurecdk/custom-resources.
Receipt filters
A receipt filter allows or blocks inbound mail from specific IP addresses or CIDR ranges, before any rule runs.
import { createReceiptFilterBuilder, createAllowListReceiptFilterBuilder } from "@composurecdk/ses";
import { ReceiptFilterPolicy } from "aws-cdk-lib/aws-ses";
createReceiptFilterBuilder()
.ip("10.0.0.0/24")
.policy(ReceiptFilterPolicy.BLOCK)
.build(stack, "Blocklist");
createAllowListReceiptFilterBuilder().ips(["203.0.113.0/24"]).build(stack, "Allowlist");createReceiptFilterBuilder— a single allow/block rule for one IP or CIDR range.createAllowListReceiptFilterBuilder— blocks all senders except the supplied IPs (a block-all filter plus one allow filter per address).
Region support
SES email receiving is available only in a subset of Regions. The rule-set and filter builders, and .publishInboundMx(), emit a synth-time warning (@composurecdk/ses:receiving-region) when built in a Region that can't receive mail. Identity verification and DKIM work in far more Regions, so the warning is scoped to the receiving constructs. See the email receiving endpoints table.
