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 sameShape,
17 syncMailManagerTags,
18} from "./internal.ts";
19
20export interface RelayProps {
21 /**
22 * Name of the relay. If omitted, a deterministic physical name is
23 * generated from the app, stage, and logical ID. Renames apply in place.
24 */
25 relayName?: string;
26 /**
27 * Hostname of the downstream SMTP server emails are relayed to. Updates
28 * apply in place.
29 */
30 serverName: string;
31 /**
32 * Port of the downstream SMTP server (e.g. 25, 587). Updates apply in
33 * place.
34 */
35 serverPort: number;
36 /**
37 * How Mail Manager authenticates to the downstream server: a Secrets
38 * Manager secret ARN holding SMTP credentials, or explicit
39 * `{ NoAuthentication: {} }`. Updates apply in place.
40 */
41 authentication: mm.RelayAuthentication;
42 /**
43 * Tags applied to the relay. Alchemy ownership tags are merged in
44 * automatically.
45 */
46 tags?: Record<string, string>;
47}
48
49export interface Relay extends Resource<
50 "AWS.MailManager.Relay",
51 RelayProps,
52 {
53 /** Server-assigned ID of the relay. */
54 relayId: string;
55 /** ARN of the relay. */
56 relayArn: string;
57 /** Name of the relay. */
58 relayName: string;
59 },
60 never,
61 Providers
62> {}
63
64/**
65 * An SES Mail Manager relay — a downstream SMTP destination that rule-set
66 * `Relay` actions forward incoming email to (e.g. an on-prem Exchange
67 * server or a third-party filter).
68 *
69 * All aspects (name, server, port, authentication, tags) update in place.
70 * ### Creating Relays
71 * **Example:** Unauthenticated Relay
72 * ```typescript
73 * import * as MailManager from "alchemy/AWS/MailManager";
74 *
75 * const relay = yield* MailManager.Relay("Downstream", {
76 * serverName: "smtp.example.com",
77 * serverPort: 25,
78 * authentication: { NoAuthentication: {} },
79 * });
80 * ```
81 *
82 * **Example:** Authenticated Relay
83 * ```typescript
84 * const relay = yield* MailManager.Relay("Downstream", {
85 * serverName: "smtp.example.com",
86 * serverPort: 587,
87 * authentication: { SecretArn: secret.secretArn },
88 * });
89 * ```
90 *
91 * ### Using in a Rule Set
92 * **Example:** Relay Action
93 * ```typescript
94 * const ruleSet = yield* MailManager.RuleSet("Inbound", {
95 * rules: [
96 * {
97 * Name: "RelayAll",
98 * Actions: [{ Relay: { Relay: relay.relayId } }],
99 * },
100 * ],
101 * });
102 * ```
103 *
104 * @resource
105 */
106export const Relay = Resource<Relay>("AWS.MailManager.Relay");
107
108export const RelayProvider = () =>
109 Provider.effect(
110 Relay,
111 Effect.gen(function* () {
112 const createName = Effect.fn(function* (
113 id: string,
114 props: { relayName?: string },
115 ) {
116 return (
117 props.relayName ?? (yield* createPhysicalName({ id, maxLength: 100 }))
118 );
119 });
120
121 const getById = (relayId: string) =>
122 mm
123 .getRelay({ RelayId: relayId })
124 .pipe(
125 Effect.catchTag("ResourceNotFoundException", () =>
126 Effect.succeed(undefined),
127 ),
128 );
129
130 const findByName = (name: string) =>
131 mm.listRelays.pages({}).pipe(
132 Stream.runCollect,
133 Effect.map((chunk) =>
134 Array.from(chunk)
135 .flatMap((page) => page.Relays ?? [])
136 .find((r) => r.RelayName === name),
137 ),
138 );
139
140 const observe = Effect.fn(function* (
141 output: Relay["Attributes"] | undefined,
142 name: string,
143 ) {
144 if (output?.relayId !== undefined) {
145 const found = yield* getById(output.relayId);
146 if (found !== undefined) return found;
147 }
148 const summary = yield* findByName(name);
149 if (summary?.RelayId === undefined) return undefined;
150 return yield* getById(summary.RelayId);
151 });
152
153 const toAttrs = Effect.fn(function* (relay: mm.GetRelayResponse) {
154 if (relay.RelayArn === undefined || relay.RelayName === undefined) {
155 return yield* Effect.fail(
156 new Error(
157 `Mail Manager relay '${relay.RelayId}' returned without ARN/name`,
158 ),
159 );
160 }
161 return {
162 relayId: relay.RelayId,
163 relayArn: relay.RelayArn,
164 relayName: relay.RelayName,
165 };
166 });
167
168 return Relay.Provider.of({
169 stables: ["relayId", "relayArn"],
170
171 list: () =>
172 mm.listRelays.pages({}).pipe(
173 Stream.runCollect,
174 Effect.flatMap((chunk) =>
175 Effect.forEach(
176 Array.from(chunk)
177 .flatMap((page) => page.Relays ?? [])
178 .flatMap((r) => (r.RelayId !== undefined ? [r.RelayId] : [])),
179 (relayId) => getById(relayId),
180 ),
181 ),
182 Effect.flatMap((results) =>
183 Effect.forEach(
184 results.flatMap((r) => (r === undefined ? [] : [r])),
185 (r) => toAttrs(r),
186 ),
187 ),
188 ),
189
190 read: Effect.fn(function* ({ id, olds, output }) {
191 const name = output?.relayName ?? (yield* createName(id, olds ?? {}));
192 const relay = yield* observe(output, name);
193 if (relay === undefined) return undefined;
194 const attrs = yield* toAttrs(relay);
195 const tags = yield* readMailManagerTags(attrs.relayArn);
196 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
197 }),
198
199 // Every prop updates in place — no replacement triggers, so the
200 // engine's default update path applies (no diff needed).
201
202 reconcile: Effect.fn(function* ({ id, news, output, session }) {
203 const name = yield* createName(id, news);
204 const internalTags = yield* createInternalTags(id);
205 const desiredTags = { ...news.tags, ...internalTags };
206
207 // 1. OBSERVE.
208 let relay = yield* observe(output, name);
209
210 // 2. ENSURE — create if missing; a Conflict race re-observes.
211 if (relay === undefined) {
212 yield* session.note(`creating relay ${name}`);
213 const created = yield* mm
214 .createRelay({
215 RelayName: name,
216 ServerName: news.serverName,
217 ServerPort: news.serverPort,
218 Authentication: news.authentication,
219 Tags: createTagsList(desiredTags),
220 })
221 .pipe(
222 Effect.catchTag("ConflictException", () =>
223 Effect.succeed(undefined),
224 ),
225 );
226 relay =
227 created !== undefined
228 ? yield* getById(created.RelayId)
229 : yield* observe(undefined, name);
230 }
231 if (relay === undefined) {
232 return yield* Effect.fail(
233 new Error(`Mail Manager relay '${name}' not found after create`),
234 );
235 }
236
237 // 3. SYNC — diff OBSERVED state against desired; apply only the
238 // delta.
239 if (
240 relay.RelayName !== name ||
241 relay.ServerName !== news.serverName ||
242 relay.ServerPort !== news.serverPort ||
243 !sameShape(relay.Authentication, news.authentication)
244 ) {
245 yield* mm.updateRelay({
246 RelayId: relay.RelayId,
247 RelayName: name,
248 ServerName: news.serverName,
249 ServerPort: news.serverPort,
250 Authentication: news.authentication,
251 });
252 }
253
254 // 3b. SYNC TAGS.
255 const attrs = yield* toAttrs(relay);
256 yield* syncMailManagerTags(attrs.relayArn, desiredTags);
257
258 yield* session.note(attrs.relayId);
259 return { ...attrs, relayName: name };
260 }),
261
262 delete: Effect.fn(function* ({ output }) {
263 yield* mm.deleteRelay({ RelayId: output.relayId }).pipe(
264 Effect.asVoid,
265 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
266 );
267 }),
268 });
269 }),
270 );