1import * as ses from "@distilled.cloud/aws/ses";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface ReceiptRuleSetProps {
10 /**
11 * Name of the receipt rule set. May contain letters, numbers, dashes,
12 * underscores and periods, up to 64 characters, and must start and end with
13 * a letter or number. If omitted, a deterministic physical name is generated
14 * from the app, stage, and logical ID. Changing the name replaces the rule
15 * set.
16 */
17 ruleSetName?: string;
18}
19
20export interface ReceiptRuleSet extends Resource<
21 "AWS.SES.ReceiptRuleSet",
22 ReceiptRuleSetProps,
23 {
24 /** Name of the receipt rule set. */
25 ruleSetName: string;
26 },
27 never,
28 Providers
29> {}
30
31/**
32 * An Amazon SES receipt rule set — the ordered container for the receipt rules
33 * that decide what happens to inbound email (deliver to S3, invoke a Lambda,
34 * publish to SNS, bounce, etc.).
35 *
36 * A rule set is an empty container on creation; add `SES.ReceiptRule`s to it
37 * and point the account at it with `SES.ActiveReceiptRuleSet` to start
38 * processing mail. Email receiving is only available in a subset of regions
39 * (e.g. `us-east-1`, `us-west-2`, `eu-west-1`).
40 * ### Creating Rule Sets
41 * **Example:** Basic Rule Set
42 * ```typescript
43 * import * as SES from "alchemy/AWS/SES";
44 *
45 * const ruleSet = yield* SES.ReceiptRuleSet("Inbound", {});
46 * ```
47 *
48 * **Example:** Named Rule Set
49 * ```typescript
50 * const ruleSet = yield* SES.ReceiptRuleSet("Inbound", {
51 * ruleSetName: "my-inbound-rules",
52 * });
53 * ```
54 *
55 * ### Activating a Rule Set
56 * **Example:** Point the Account at a Rule Set
57 * ```typescript
58 * const ruleSet = yield* SES.ReceiptRuleSet("Inbound", {});
59 * yield* SES.ActiveReceiptRuleSet("Active", {
60 * ruleSetName: ruleSet.ruleSetName,
61 * });
62 * ```
63 *
64 * @resource
65 */
66export const ReceiptRuleSet = Resource<ReceiptRuleSet>(
67 "AWS.SES.ReceiptRuleSet",
68);
69
70export const ReceiptRuleSetProvider = () =>
71 Provider.effect(
72 ReceiptRuleSet,
73 Effect.gen(function* () {
74 const createName = Effect.fn(function* (
75 id: string,
76 props: Pick<ReceiptRuleSetProps, "ruleSetName">,
77 ) {
78 return (
79 props.ruleSetName ??
80 (yield* createPhysicalName({ id, maxLength: 64 }))
81 );
82 });
83
84 const describe = Effect.fn(function* (name: string) {
85 return yield* ses
86 .describeReceiptRuleSet({ RuleSetName: name })
87 .pipe(
88 Effect.catchTag("RuleSetDoesNotExistException", () =>
89 Effect.succeed(undefined),
90 ),
91 );
92 });
93
94 return ReceiptRuleSet.Provider.of({
95 stables: ["ruleSetName"],
96
97 // Account/region-scoped: enumerate every rule set so leaked test
98 // resources are cleaned by nuke. Classic receipt rule sets carry no
99 // tags, so there is no ownership signal to filter on.
100 list: () =>
101 Effect.gen(function* () {
102 const rows: { ruleSetName: string }[] = [];
103 let nextToken: string | undefined;
104 do {
105 const page = yield* ses.listReceiptRuleSets({
106 NextToken: nextToken,
107 });
108 for (const set of page.RuleSets ?? []) {
109 if (set.Name) rows.push({ ruleSetName: set.Name });
110 }
111 nextToken = page.NextToken;
112 } while (nextToken);
113 return rows;
114 }),
115
116 read: Effect.fn(function* ({ id, olds, output }) {
117 const name = output?.ruleSetName ?? (yield* createName(id, olds));
118 const found = yield* describe(name);
119 // Receipt rule sets are untagged in the classic API, so there is no
120 // way to distinguish a foreign rule set from one we own. Existence
121 // at our deterministic name is treated as ownership.
122 return found ? { ruleSetName: name } : undefined;
123 }),
124
125 diff: Effect.fn(function* ({ id, news, olds }) {
126 if (!isResolved(news)) return undefined;
127 const oldName = yield* createName(id, olds);
128 const newName = yield* createName(id, news);
129 if (oldName !== newName) {
130 return { action: "replace" } as const;
131 }
132 }),
133
134 reconcile: Effect.fn(function* ({ id, news, output }) {
135 const name = output?.ruleSetName ?? (yield* createName(id, news));
136
137 // OBSERVE — cloud state is authoritative.
138 const observed = yield* describe(name);
139
140 // ENSURE — create if missing; AlreadyExists is a race, not a failure.
141 if (observed === undefined) {
142 yield* ses
143 .createReceiptRuleSet({ RuleSetName: name })
144 .pipe(
145 Effect.catchTag("AlreadyExistsException", () =>
146 Effect.succeed({}),
147 ),
148 );
149 }
150
151 // A rule set has no mutable aspects of its own; rules are separate
152 // `SES.ReceiptRule` resources.
153 return { ruleSetName: name };
154 }),
155
156 delete: Effect.fn(function* ({ output }) {
157 // deleteReceiptRuleSet is idempotent for a missing set; the active
158 // set cannot be deleted and surfaces as a typed CannotDeleteException.
159 yield* ses.deleteReceiptRuleSet({
160 RuleSetName: output.ruleSetName,
161 });
162 }),
163 });
164 }),
165 );