1---
2title: Email receiving
3description: Receive inbound email with SES — rule sets and rules that store mail in S3, fan out to SNS, invoke a Lambda, or bounce it, plus IP filters and the active rule set pointer.
4---
5
6**SES email receiving** turns a domain you've verified into an
7inbound mail endpoint. Mail arriving at any address on the domain
8is matched against an ordered list of **receipt rules**, and each
9rule runs a list of **actions**: store the raw message in S3,
10publish a notification to SNS, invoke a Lambda, bounce it back to
11the sender, add a header, or stop processing.
12
13The receiving primitives:
14
15- A **[`ReceiptRuleSet`](/providers/aws/ses/receiptruleset)** is
16 the ordered container for rules. An account can hold many, but
17 only one is active at a time.
18- A **[`ReceiptRule`](/providers/aws/ses/receiptrule)** matches
19 inbound mail by recipient and applies an ordered list of
20 actions.
21- The
22 **[`ActiveReceiptRuleSet`](/providers/aws/ses/activereceiptruleset)**
23 is the single rule set SES actually evaluates — an account
24 singleton pointer.
25- A **[`ReceiptFilter`](/providers/aws/ses/receiptfilter)**
26 allows or blocks inbound mail by source IP, before rules run.
27- **[`SendBounce`](/providers/aws/ses/sendbounce)** is a runtime
28 binding for bouncing a received message from inside a Lambda.
29
30:::note
31Email receiving is only available in a subset of AWS regions —
32today **`us-east-1`**, **`us-west-2`**, and **`eu-west-1`**.
33Deploy the stack that owns these resources to one of them, and
34point your domain's MX record at that region's SES inbound
35endpoint (e.g. `inbound-smtp.us-east-1.amazonaws.com`).
36:::
37
38## Create the Stack
39
40The snippets below run inside a Stack's `Effect.gen` body:
41
42```typescript
43// alchemy.run.ts
44import * as Alchemy from "alchemy";
45import * as AWS from "alchemy/AWS";
46import * as SES from "alchemy/AWS/SES";
47import * as Effect from "effect/Effect";
48
49export default Alchemy.Stack(
50 "InboundMail",
51 { providers: AWS.providers(), state: AWS.state() },
52 Effect.gen(function* () {
53 // resources go here
54 return {};
55 }),
56);
57```
58
59## Create a rule set
60
61A rule set starts empty — it's just the ordered container the
62rules hang off:
63
64```typescript
65const ruleSet = yield* SES.ReceiptRuleSet("Inbound", {});
66```
67
68Omitting `ruleSetName` lets Alchemy generate a deterministic name
69from the app, stage, and logical id. Nothing is evaluated against
70inbound mail until a rule set is made active (below).
71
72## Store inbound mail in S3
73
74The running example is a support inbox: store every raw message
75in a bucket, then notify a topic that new mail arrived. Start with
76the S3 action:
77
78```typescript
79const bucket = yield* AWS.S3.Bucket("InboundMail", {
80 forceDestroy: true,
81});
82
83const inbound = yield* SES.ReceiptRule("Support", {
84 ruleSetName: ruleSet.ruleSetName,
85 recipients: ["support@example.com"],
86 actions: [
87 {
88 S3Action: {
89 BucketName: bucket.bucketName,
90 ObjectKeyPrefix: "support/",
91 },
92 },
93 ],
94});
95```
96
97SES writes the full raw MIME message to the bucket under the
98prefix, keyed by message id. The bucket's policy must allow the
99SES service principal (`ses.amazonaws.com`) to `s3:PutObject` — SES
100validates this when the rule is created, so add it to the bucket
101before the rule.
102
103## Fan out to SNS
104
105Actions run in array order. Add an `SNSAction` after the S3 action
106so SES publishes a notification once the message is stored:
107
108```typescript
109const topic = yield* AWS.SNS.Topic("InboundNotifications", {});
110
111const inbound = yield* SES.ReceiptRule("Support", {
112 ruleSetName: ruleSet.ruleSetName,
113 recipients: ["support@example.com"],
114 actions: [
115 {
116 S3Action: {
117 BucketName: bucket.bucketName,
118 ObjectKeyPrefix: "support/",
119 },
120 },
121 { SNSAction: { TopicArn: topic.topicArn, Encoding: "UTF-8" } },
122 ],
123});
124```
125
126Anything subscribed to the topic — a queue, a Lambda, an
127HTTP endpoint — now fans out from the single notification. The
128topic's access policy must allow `ses.amazonaws.com` to
129`sns:Publish`. (An `SNSAction` publishes a notification, not the
130message body; for large messages the S3 copy is the source of
131truth.)
132
133## Invoke a Lambda on inbound mail
134
135A `LambdaAction` hands the matched message to a function. Point it
136at the function's ARN and choose `Event` (async, fire-and-forget)
137or `RequestResponse` (synchronous, lets the function influence
138delivery):
139
140```typescript
141const rule = yield* SES.ReceiptRule("Process", {
142 ruleSetName: ruleSet.ruleSetName,
143 recipients: ["support@example.com"],
144 actions: [
145 {
146 LambdaAction: {
147 FunctionArn: processor.functionArn,
148 InvocationType: "Event",
149 },
150 },
151 ],
152});
153```
154
155See [Lambda](/aws/compute/lambda) for standing up the `processor`
156function itself. SES's permission to invoke it is granted the same
157way as S3 and SNS — the function's resource policy must allow
158`ses.amazonaws.com` to `lambda:InvokeFunction`.
159
160## Order rules with `after`
161
162Rules evaluate top to bottom. The `after` prop pins a rule
163immediately behind another one in the set. Here a catch-all bounce
164runs after the support rule, rejecting mail to unknown recipients:
165
166```typescript
167const bounce = yield* SES.ReceiptRule("BounceUnknown", {
168 ruleSetName: ruleSet.ruleSetName,
169 after: inbound.ruleName,
170 actions: [
171 {
172 BounceAction: {
173 SmtpReplyCode: "550",
174 Message: "Mailbox does not exist",
175 Sender: "mailer-daemon@example.com",
176 },
177 },
178 ],
179});
180```
181
182A rule with no `recipients` matches everything, so ordering it
183last makes it a fallthrough. Omit `after` to place a rule at the
184top of the set.
185
186## Activate the rule set
187
188Rules do nothing until their set is the account's active one.
189`ActiveReceiptRuleSet` is the pointer SES evaluates against every
190inbound message:
191
192```typescript
193yield* SES.ActiveReceiptRuleSet("Active", {
194 ruleSetName: ruleSet.ruleSetName,
195});
196```
197
198Only one rule set is active per account and region. Re-pointing
199this resource at a different set switches receiving over in place.
200Destroying it deactivates receiving — but only if the account is
201still pointed at the set this resource activated; if something else
202has since taken over, the destroy is a no-op.
203
204## Filter senders by IP
205
206`ReceiptFilter` is an account-level allow/block list on the source
207IP of inbound connections, applied before any rule runs. Block
208filters win over allow filters:
209
210```typescript
211yield* SES.ReceiptFilter("BlockBadActors", {
212 ipFilter: { policy: "Block", cidr: "203.0.113.0/24" },
213});
214```
215
216`cidr` takes a single address (`203.0.113.7`) or a range
217(`203.0.113.0/24`). Filters have no update API, so changing the
218policy or range replaces the filter.
219
220## Bounce a message from a Lambda
221
222When a `LambdaAction` invokes your function synchronously, the
223function can bounce the message itself. `SES.SendBounce` is a
224runtime binding — an account-level operation with no resource to
225scope to, so it's bound with no argument and its implementation is
226provided as `SES.SendBounceHttp`:
227
228```typescript
229// Construction — inside the Lambda's Effect.gen body
230const sendBounce = yield* SES.SendBounce();
231```
232
233At runtime, generate the bounce from the id of the message SES
234delivered to you:
235
236```typescript
237yield* sendBounce({
238 OriginalMessageId: messageId,
239 BounceSender: "mailer-daemon@example.com",
240 BouncedRecipientInfoList: [
241 { Recipient: "nobody@example.com", BounceType: "DoesNotExist" },
242 ],
243});
244```
245
246Provide the implementation on the function with
247`Effect.provide(SES.SendBounceHttp)`. You can only bounce a message
248within 24 hours of receiving it, and only mail SES actually
249received for you.
250
251## Where next
252
253- [Lambda](/aws/compute/lambda) — the function a `LambdaAction`
254 invokes.
255- [S3](/aws/data/s3) — the bucket inbound mail lands in.
256- [SNS](/aws/messaging/sns) — fan the notification out to queues
257 and functions.
258
259Reference:
260
261- [ReceiptRuleSet API reference](/providers/aws/ses/receiptruleset)
262- [ReceiptRule API reference](/providers/aws/ses/receiptrule)
263- [ActiveReceiptRuleSet API reference](/providers/aws/ses/activereceiptruleset)
264- [ReceiptFilter API reference](/providers/aws/ses/receiptfilter)
265- [SendBounce API reference](/providers/aws/ses/sendbounce)