1import * as mm from "@distilled.cloud/aws/mailmanager";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import {
9 createInternalTags,
10 createTagsList,
11 hasAlchemyTags,
12} from "../../Tags.ts";
13import type { Providers } from "../Providers.ts";
14import {
15 readMailManagerTags,
16 retryWhileMailManagerConflict,
17 sameShape,
18 syncMailManagerTags,
19} from "./internal.ts";
20
21export interface TrafficPolicyProps {
22 /**
23 * Name of the traffic policy. If omitted, a deterministic physical name is
24 * generated from the app, stage, and logical ID. Renames apply in place.
25 */
26 trafficPolicyName?: string;
27 /**
28 * Conditional statements evaluated against incoming SMTP connections and
29 * message metadata (sender IP, recipient, TLS protocol, analyzer verdicts,
30 * address-list membership). Each statement ALLOWs or DENYs matching
31 * traffic. Updates apply in place.
32 * @default []
33 */
34 policyStatements?: mm.PolicyStatement[];
35 /**
36 * Action applied to traffic that matches no policy statement.
37 */
38 defaultAction: mm.AcceptAction;
39 /**
40 * Maximum message size in bytes; larger messages are rejected.
41 */
42 maxMessageSizeBytes?: number;
43 /**
44 * Tags applied to the traffic policy. Alchemy ownership tags are merged in
45 * automatically.
46 */
47 tags?: Record<string, string>;
48}
49
50export interface TrafficPolicy extends Resource<
51 "AWS.MailManager.TrafficPolicy",
52 TrafficPolicyProps,
53 {
54 /** Server-assigned ID of the traffic policy. */
55 trafficPolicyId: string;
56 /** ARN of the traffic policy. */
57 trafficPolicyArn: string;
58 /** Name of the traffic policy. */
59 trafficPolicyName: string;
60 },
61 never,
62 Providers
63> {}
64
65/**
66 * An SES Mail Manager traffic policy — connection-level ALLOW/DENY rules an
67 * ingress point applies before email reaches the rule set (sender CIDRs,
68 * recipient patterns, TLS floor, message size cap).
69 *
70 * All aspects (name, statements, default action, size cap, tags) update in
71 * place.
72 * ### Creating Traffic Policies
73 * **Example:** Deny-by-Default with an Allowed CIDR
74 * ```typescript
75 * import * as MailManager from "alchemy/AWS/MailManager";
76 *
77 * const policy = yield* MailManager.TrafficPolicy("Edge", {
78 * defaultAction: "DENY",
79 * policyStatements: [
80 * {
81 * Action: "ALLOW",
82 * Conditions: [
83 * {
84 * IpExpression: {
85 * Evaluate: { Attribute: "SENDER_IP" },
86 * Operator: "CIDR_MATCHES",
87 * Values: ["10.0.0.0/8"],
88 * },
89 * },
90 * ],
91 * },
92 * ],
93 * });
94 * ```
95 *
96 * **Example:** Allow All with a Size Cap
97 * ```typescript
98 * const policy = yield* MailManager.TrafficPolicy("Edge", {
99 * defaultAction: "ALLOW",
100 * maxMessageSizeBytes: 10 * 1024 * 1024,
101 * });
102 * ```
103 *
104 * @resource
105 */
106export const TrafficPolicy = Resource<TrafficPolicy>(
107 "AWS.MailManager.TrafficPolicy",
108);
109
110export const TrafficPolicyProvider = () =>
111 Provider.effect(
112 TrafficPolicy,
113 Effect.gen(function* () {
114 const createName = Effect.fn(function* (
115 id: string,
116 props: { trafficPolicyName?: string },
117 ) {
118 return (
119 props.trafficPolicyName ??
120 (yield* createPhysicalName({ id, maxLength: 63 }))
121 );
122 });
123
124 const getById = (trafficPolicyId: string) =>
125 mm
126 .getTrafficPolicy({ TrafficPolicyId: trafficPolicyId })
127 .pipe(
128 Effect.catchTag("ResourceNotFoundException", () =>
129 Effect.succeed(undefined),
130 ),
131 );
132
133 const findByName = (name: string) =>
134 mm.listTrafficPolicies.pages({}).pipe(
135 Stream.runCollect,
136 Effect.map((chunk) =>
137 Array.from(chunk)
138 .flatMap((page) => page.TrafficPolicies ?? [])
139 .find((p) => p.TrafficPolicyName === name),
140 ),
141 );
142
143 const observe = Effect.fn(function* (
144 output: TrafficPolicy["Attributes"] | undefined,
145 name: string,
146 ) {
147 if (output?.trafficPolicyId !== undefined) {
148 const found = yield* getById(output.trafficPolicyId);
149 if (found !== undefined) return found;
150 }
151 const summary = yield* findByName(name);
152 if (summary?.TrafficPolicyId === undefined) return undefined;
153 return yield* getById(summary.TrafficPolicyId);
154 });
155
156 const toAttrs = Effect.fn(function* (
157 policy: mm.GetTrafficPolicyResponse,
158 ) {
159 if (policy.TrafficPolicyArn === undefined) {
160 return yield* Effect.fail(
161 new Error(
162 `Mail Manager traffic policy '${policy.TrafficPolicyId}' returned without an ARN`,
163 ),
164 );
165 }
166 return {
167 trafficPolicyId: policy.TrafficPolicyId,
168 trafficPolicyArn: policy.TrafficPolicyArn,
169 trafficPolicyName: policy.TrafficPolicyName,
170 };
171 });
172
173 return TrafficPolicy.Provider.of({
174 stables: ["trafficPolicyId", "trafficPolicyArn"],
175
176 list: () =>
177 mm.listTrafficPolicies.pages({}).pipe(
178 Stream.runCollect,
179 Effect.flatMap((chunk) =>
180 Effect.forEach(
181 Array.from(chunk)
182 .flatMap((page) => page.TrafficPolicies ?? [])
183 .flatMap((p) =>
184 p.TrafficPolicyId !== undefined ? [p.TrafficPolicyId] : [],
185 ),
186 (trafficPolicyId) => getById(trafficPolicyId),
187 ),
188 ),
189 Effect.flatMap((results) =>
190 Effect.forEach(
191 results.flatMap((p) => (p === undefined ? [] : [p])),
192 (p) => toAttrs(p),
193 ),
194 ),
195 ),
196
197 read: Effect.fn(function* ({ id, olds, output }) {
198 const name =
199 output?.trafficPolicyName ?? (yield* createName(id, olds ?? {}));
200 const policy = yield* observe(output, name);
201 if (policy === undefined) return undefined;
202 const attrs = yield* toAttrs(policy);
203 const tags = yield* readMailManagerTags(attrs.trafficPolicyArn);
204 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
205 }),
206
207 // Every prop updates in place — no replacement triggers, so the
208 // engine's default update path applies (no diff needed).
209
210 reconcile: Effect.fn(function* ({ id, news, output, session }) {
211 const name = yield* createName(id, news);
212 const internalTags = yield* createInternalTags(id);
213 const desiredTags = { ...news.tags, ...internalTags };
214 const desiredStatements = news.policyStatements ?? [];
215
216 // 1. OBSERVE.
217 let policy = yield* observe(output, name);
218
219 // 2. ENSURE — create if missing; a Conflict race re-observes.
220 if (policy === undefined) {
221 yield* session.note(`creating traffic policy ${name}`);
222 const created = yield* mm
223 .createTrafficPolicy({
224 TrafficPolicyName: name,
225 PolicyStatements: desiredStatements,
226 DefaultAction: news.defaultAction,
227 MaxMessageSizeBytes: news.maxMessageSizeBytes,
228 Tags: createTagsList(desiredTags),
229 })
230 .pipe(
231 Effect.catchTag("ConflictException", () =>
232 Effect.succeed(undefined),
233 ),
234 );
235 policy =
236 created !== undefined
237 ? yield* getById(created.TrafficPolicyId)
238 : yield* observe(undefined, name);
239 }
240 if (policy === undefined) {
241 return yield* Effect.fail(
242 new Error(
243 `Mail Manager traffic policy '${name}' not found after create`,
244 ),
245 );
246 }
247
248 // 3. SYNC — diff OBSERVED state against desired; apply only the
249 // delta.
250 if (
251 policy.TrafficPolicyName !== name ||
252 policy.DefaultAction !== news.defaultAction ||
253 policy.MaxMessageSizeBytes !== news.maxMessageSizeBytes ||
254 !sameShape(policy.PolicyStatements ?? [], desiredStatements)
255 ) {
256 yield* mm.updateTrafficPolicy({
257 TrafficPolicyId: policy.TrafficPolicyId,
258 TrafficPolicyName: name,
259 PolicyStatements: desiredStatements,
260 DefaultAction: news.defaultAction,
261 MaxMessageSizeBytes: news.maxMessageSizeBytes,
262 });
263 }
264
265 // 3b. SYNC TAGS.
266 const attrs = yield* toAttrs(policy);
267 yield* syncMailManagerTags(attrs.trafficPolicyArn, desiredTags);
268
269 yield* session.note(attrs.trafficPolicyId);
270 return { ...attrs, trafficPolicyName: name };
271 }),
272
273 delete: Effect.fn(function* ({ output }) {
274 // A traffic policy stays "in use" while a deleted ingress point
275 // finishes deprovisioning — retry through the Conflict window.
276 yield* retryWhileMailManagerConflict(
277 mm.deleteTrafficPolicy({
278 TrafficPolicyId: output.trafficPolicyId,
279 }),
280 ).pipe(
281 Effect.asVoid,
282 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
283 );
284 }),
285 });
286 }),
287 );