@flink-app/email-plugin 2.0.0-alpha.74 → 2.0.0-alpha.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +18 -1
- package/dist/schemas/client.d.ts +2 -1
- package/dist/schemas/emailSes.d.ts +60 -0
- package/dist/schemas/emailSes.js +2 -0
- package/dist/ses-types.d.ts +94 -0
- package/dist/ses-types.js +2 -0
- package/dist/sesClient.d.ts +48 -0
- package/dist/sesClient.js +388 -0
- package/package.json +7 -5
- package/readme.md +199 -4
- package/spec/sesClient.spec.ts +464 -0
- package/spec/support/jasmine.json +7 -0
- package/src/index.ts +4 -1
- package/src/schemas/client.ts +2 -1
- package/src/schemas/emailSes.ts +73 -0
- package/src/ses-types.ts +93 -0
- package/src/sesClient.ts +352 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { sesClient, sesClientOptions } from "../src/sesClient";
|
|
2
|
+
import { emailSes } from "../src/schemas/emailSes";
|
|
3
|
+
|
|
4
|
+
// Mock AWS SESv2Client
|
|
5
|
+
function createMockAwsClient(overrides?: {
|
|
6
|
+
sendResponse?: any;
|
|
7
|
+
sendError?: any;
|
|
8
|
+
configRegion?: string;
|
|
9
|
+
}) {
|
|
10
|
+
const mock = {
|
|
11
|
+
send: jasmine
|
|
12
|
+
.createSpy("send")
|
|
13
|
+
.and.callFake(async (command: any) => {
|
|
14
|
+
if (overrides?.sendError) throw overrides.sendError;
|
|
15
|
+
return overrides?.sendResponse || { MessageId: "mock-message-id-123" };
|
|
16
|
+
}),
|
|
17
|
+
config: {
|
|
18
|
+
region: () => Promise.resolve(overrides?.configRegion || "eu-north-1"),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
return mock as any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("sesClient", () => {
|
|
25
|
+
describe("send()", () => {
|
|
26
|
+
it("should send a simple email and return true", async () => {
|
|
27
|
+
const mockAws = createMockAwsClient();
|
|
28
|
+
const client = new sesClient(
|
|
29
|
+
{ region: "eu-north-1" },
|
|
30
|
+
mockAws
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const result = await client.send({
|
|
34
|
+
from: "sender@example.com",
|
|
35
|
+
to: ["recipient@example.com"],
|
|
36
|
+
subject: "Test",
|
|
37
|
+
html: "<p>Hello</p>",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
expect(mockAws.send).toHaveBeenCalledTimes(1);
|
|
42
|
+
|
|
43
|
+
const command = mockAws.send.calls.first().args[0];
|
|
44
|
+
expect(command.input.FromEmailAddress).toBe("sender@example.com");
|
|
45
|
+
expect(command.input.Destination.ToAddresses).toEqual([
|
|
46
|
+
"recipient@example.com",
|
|
47
|
+
]);
|
|
48
|
+
expect(command.input.Content.Simple.Subject.Data).toBe("Test");
|
|
49
|
+
expect(command.input.Content.Simple.Body.Html.Data).toBe("<p>Hello</p>");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return false on AWS error", async () => {
|
|
53
|
+
const mockAws = createMockAwsClient({
|
|
54
|
+
sendError: { name: "MessageRejected", message: "Email rejected" },
|
|
55
|
+
});
|
|
56
|
+
const client = new sesClient(
|
|
57
|
+
{ region: "eu-north-1" },
|
|
58
|
+
mockAws
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const result = await client.send({
|
|
62
|
+
from: "sender@example.com",
|
|
63
|
+
to: ["recipient@example.com"],
|
|
64
|
+
subject: "Test",
|
|
65
|
+
html: "<p>Hello</p>",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should use defaultFrom when email has no from", async () => {
|
|
72
|
+
const mockAws = createMockAwsClient();
|
|
73
|
+
const client = new sesClient(
|
|
74
|
+
{ region: "eu-north-1", defaultFrom: "default@example.com" },
|
|
75
|
+
mockAws
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await client.send({
|
|
79
|
+
from: "",
|
|
80
|
+
to: ["recipient@example.com"],
|
|
81
|
+
subject: "Test",
|
|
82
|
+
text: "Hello",
|
|
83
|
+
} as any);
|
|
84
|
+
|
|
85
|
+
// send will use defaultFrom since from is falsy
|
|
86
|
+
const command = mockAws.send.calls.first().args[0];
|
|
87
|
+
expect(command.input.FromEmailAddress).toBe("default@example.com");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return false when no from address is available", async () => {
|
|
91
|
+
const mockAws = createMockAwsClient();
|
|
92
|
+
const client = new sesClient({ region: "eu-north-1" }, mockAws);
|
|
93
|
+
|
|
94
|
+
const result = await client.send({
|
|
95
|
+
from: "",
|
|
96
|
+
to: ["recipient@example.com"],
|
|
97
|
+
subject: "Test",
|
|
98
|
+
text: "Hello",
|
|
99
|
+
} as any);
|
|
100
|
+
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
expect(mockAws.send).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("sendEmail()", () => {
|
|
107
|
+
it("should return rich result with messageId on success", async () => {
|
|
108
|
+
const mockAws = createMockAwsClient({
|
|
109
|
+
sendResponse: { MessageId: "ses-msg-456" },
|
|
110
|
+
});
|
|
111
|
+
const client = new sesClient(
|
|
112
|
+
{ region: "eu-north-1" },
|
|
113
|
+
mockAws
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const result = await client.sendEmail({
|
|
117
|
+
from: "sender@example.com",
|
|
118
|
+
to: ["recipient@example.com"],
|
|
119
|
+
subject: "Test",
|
|
120
|
+
text: "Hello",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.messageId).toBe("ses-msg-456");
|
|
125
|
+
expect(result.error).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should return structured error on failure", async () => {
|
|
129
|
+
const mockAws = createMockAwsClient({
|
|
130
|
+
sendError: {
|
|
131
|
+
name: "AccountSendingPausedException",
|
|
132
|
+
message: "Sending is paused",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const client = new sesClient(
|
|
136
|
+
{ region: "eu-north-1" },
|
|
137
|
+
mockAws
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const result = await client.sendEmail({
|
|
141
|
+
from: "sender@example.com",
|
|
142
|
+
to: ["recipient@example.com"],
|
|
143
|
+
subject: "Test",
|
|
144
|
+
html: "<p>Hello</p>",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(false);
|
|
148
|
+
expect(result.error?.code).toBe("AccountSendingPausedException");
|
|
149
|
+
expect(result.error?.message).toBe("Sending is paused");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should send both text and html", async () => {
|
|
153
|
+
const mockAws = createMockAwsClient();
|
|
154
|
+
const client = new sesClient(
|
|
155
|
+
{ region: "eu-north-1" },
|
|
156
|
+
mockAws
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await client.sendEmail({
|
|
160
|
+
from: "sender@example.com",
|
|
161
|
+
to: ["recipient@example.com"],
|
|
162
|
+
subject: "Test",
|
|
163
|
+
text: "Hello plain",
|
|
164
|
+
html: "<p>Hello html</p>",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const command = mockAws.send.calls.first().args[0];
|
|
168
|
+
expect(command.input.Content.Simple.Body.Text.Data).toBe("Hello plain");
|
|
169
|
+
expect(command.input.Content.Simple.Body.Html.Data).toBe(
|
|
170
|
+
"<p>Hello html</p>"
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should include cc, bcc, and replyTo", async () => {
|
|
175
|
+
const mockAws = createMockAwsClient();
|
|
176
|
+
const client = new sesClient(
|
|
177
|
+
{ region: "eu-north-1" },
|
|
178
|
+
mockAws
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await client.sendEmail({
|
|
182
|
+
from: "sender@example.com",
|
|
183
|
+
to: ["to@example.com"],
|
|
184
|
+
cc: ["cc@example.com"],
|
|
185
|
+
bcc: ["bcc@example.com"],
|
|
186
|
+
replyTo: ["reply@example.com"],
|
|
187
|
+
subject: "Test",
|
|
188
|
+
text: "Hello",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const command = mockAws.send.calls.first().args[0];
|
|
192
|
+
expect(command.input.Destination.CcAddresses).toEqual([
|
|
193
|
+
"cc@example.com",
|
|
194
|
+
]);
|
|
195
|
+
expect(command.input.Destination.BccAddresses).toEqual([
|
|
196
|
+
"bcc@example.com",
|
|
197
|
+
]);
|
|
198
|
+
expect(command.input.ReplyToAddresses).toEqual(["reply@example.com"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should pass configuration set and tags", async () => {
|
|
202
|
+
const mockAws = createMockAwsClient();
|
|
203
|
+
const client = new sesClient(
|
|
204
|
+
{ region: "eu-north-1", configurationSet: "default-set" },
|
|
205
|
+
mockAws
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await client.sendEmail({
|
|
209
|
+
from: "sender@example.com",
|
|
210
|
+
to: ["recipient@example.com"],
|
|
211
|
+
subject: "Test",
|
|
212
|
+
text: "Hello",
|
|
213
|
+
tags: { campaign: "welcome", env: "prod" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const command = mockAws.send.calls.first().args[0];
|
|
217
|
+
expect(command.input.ConfigurationSetName).toBe("default-set");
|
|
218
|
+
expect(command.input.EmailTags).toEqual([
|
|
219
|
+
{ Name: "campaign", Value: "welcome" },
|
|
220
|
+
{ Name: "env", Value: "prod" },
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should override default configuration set per-email", async () => {
|
|
225
|
+
const mockAws = createMockAwsClient();
|
|
226
|
+
const client = new sesClient(
|
|
227
|
+
{ region: "eu-north-1", configurationSet: "default-set" },
|
|
228
|
+
mockAws
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await client.sendEmail({
|
|
232
|
+
from: "sender@example.com",
|
|
233
|
+
to: ["recipient@example.com"],
|
|
234
|
+
subject: "Test",
|
|
235
|
+
text: "Hello",
|
|
236
|
+
configurationSet: "override-set",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const command = mockAws.send.calls.first().args[0];
|
|
240
|
+
expect(command.input.ConfigurationSetName).toBe("override-set");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should use Raw content for emails with attachments", async () => {
|
|
244
|
+
const mockAws = createMockAwsClient();
|
|
245
|
+
const client = new sesClient(
|
|
246
|
+
{ region: "eu-north-1" },
|
|
247
|
+
mockAws
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await client.sendEmail({
|
|
251
|
+
from: "sender@example.com",
|
|
252
|
+
to: ["recipient@example.com"],
|
|
253
|
+
subject: "Test",
|
|
254
|
+
html: "<p>See attached</p>",
|
|
255
|
+
attachments: [
|
|
256
|
+
{
|
|
257
|
+
filename: "test.txt",
|
|
258
|
+
content: Buffer.from("hello world"),
|
|
259
|
+
contentType: "text/plain",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const command = mockAws.send.calls.first().args[0];
|
|
265
|
+
expect(command.input.Content.Raw).toBeDefined();
|
|
266
|
+
expect(command.input.Content.Simple).toBeUndefined();
|
|
267
|
+
|
|
268
|
+
// Verify MIME message contains expected parts
|
|
269
|
+
const rawMessage = new TextDecoder().decode(command.input.Content.Raw.Data);
|
|
270
|
+
expect(rawMessage).toContain("From: sender@example.com");
|
|
271
|
+
expect(rawMessage).toContain("To: recipient@example.com");
|
|
272
|
+
expect(rawMessage).toContain("multipart/mixed");
|
|
273
|
+
expect(rawMessage).toContain("text/html");
|
|
274
|
+
expect(rawMessage).toContain("<p>See attached</p>");
|
|
275
|
+
expect(rawMessage).toContain('filename="test.txt"');
|
|
276
|
+
expect(rawMessage).toContain(
|
|
277
|
+
Buffer.from("hello world").toString("base64")
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("sendTemplated()", () => {
|
|
283
|
+
it("should send a templated email", async () => {
|
|
284
|
+
const mockAws = createMockAwsClient({
|
|
285
|
+
sendResponse: { MessageId: "tmpl-msg-789" },
|
|
286
|
+
});
|
|
287
|
+
const client = new sesClient(
|
|
288
|
+
{ region: "eu-north-1", defaultFrom: "noreply@example.com" },
|
|
289
|
+
mockAws
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const result = await client.sendTemplated({
|
|
293
|
+
to: ["recipient@example.com"],
|
|
294
|
+
template: "WelcomeEmail",
|
|
295
|
+
templateData: { name: "Joel", company: "Frost" },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.success).toBe(true);
|
|
299
|
+
expect(result.messageId).toBe("tmpl-msg-789");
|
|
300
|
+
|
|
301
|
+
const command = mockAws.send.calls.first().args[0];
|
|
302
|
+
expect(command.input.FromEmailAddress).toBe("noreply@example.com");
|
|
303
|
+
expect(command.input.Content.Template.TemplateName).toBe("WelcomeEmail");
|
|
304
|
+
expect(command.input.Content.Template.TemplateData).toBe(
|
|
305
|
+
JSON.stringify({ name: "Joel", company: "Frost" })
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should return error when no from address", async () => {
|
|
310
|
+
const mockAws = createMockAwsClient();
|
|
311
|
+
const client = new sesClient({ region: "eu-north-1" }, mockAws);
|
|
312
|
+
|
|
313
|
+
const result = await client.sendTemplated({
|
|
314
|
+
to: ["recipient@example.com"],
|
|
315
|
+
template: "WelcomeEmail",
|
|
316
|
+
templateData: { name: "Joel" },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(result.success).toBe(false);
|
|
320
|
+
expect(result.error?.code).toBe("MISSING_FROM");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("sendBulk()", () => {
|
|
325
|
+
it("should send bulk emails", async () => {
|
|
326
|
+
const mockAws = createMockAwsClient({
|
|
327
|
+
sendResponse: {
|
|
328
|
+
BulkEmailEntryResults: [
|
|
329
|
+
{ Status: "SUCCESS", MessageId: "bulk-1" },
|
|
330
|
+
{ Status: "SUCCESS", MessageId: "bulk-2" },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
const client = new sesClient(
|
|
335
|
+
{ region: "eu-north-1", defaultFrom: "noreply@example.com" },
|
|
336
|
+
mockAws
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const result = await client.sendBulk({
|
|
340
|
+
template: "Newsletter",
|
|
341
|
+
defaultTemplateData: { company: "Frost" },
|
|
342
|
+
destinations: [
|
|
343
|
+
{ to: ["a@example.com"], templateData: { name: "Alice" } },
|
|
344
|
+
{ to: ["b@example.com"], templateData: { name: "Bob" } },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(result.success).toBe(true);
|
|
349
|
+
expect(result.successCount).toBe(2);
|
|
350
|
+
expect(result.failureCount).toBe(0);
|
|
351
|
+
expect(result.results.length).toBe(2);
|
|
352
|
+
expect(result.results[0].messageId).toBe("bulk-1");
|
|
353
|
+
|
|
354
|
+
const command = mockAws.send.calls.first().args[0];
|
|
355
|
+
expect(command.input.DefaultContent.Template.TemplateName).toBe(
|
|
356
|
+
"Newsletter"
|
|
357
|
+
);
|
|
358
|
+
expect(command.input.BulkEmailEntries.length).toBe(2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should handle partial failures", async () => {
|
|
362
|
+
const mockAws = createMockAwsClient({
|
|
363
|
+
sendResponse: {
|
|
364
|
+
BulkEmailEntryResults: [
|
|
365
|
+
{ Status: "SUCCESS", MessageId: "bulk-1" },
|
|
366
|
+
{ Status: "FAILED", Error: "MailFromDomainNotVerified" },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
const client = new sesClient(
|
|
371
|
+
{ region: "eu-north-1", defaultFrom: "noreply@example.com" },
|
|
372
|
+
mockAws
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const result = await client.sendBulk({
|
|
376
|
+
template: "Newsletter",
|
|
377
|
+
destinations: [
|
|
378
|
+
{ to: ["a@example.com"] },
|
|
379
|
+
{ to: ["b@example.com"] },
|
|
380
|
+
],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(result.success).toBe(false);
|
|
384
|
+
expect(result.successCount).toBe(1);
|
|
385
|
+
expect(result.failureCount).toBe(1);
|
|
386
|
+
expect(result.results[0].success).toBe(true);
|
|
387
|
+
expect(result.results[1].success).toBe(false);
|
|
388
|
+
expect(result.results[1].error?.code).toBe("MailFromDomainNotVerified");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should handle complete AWS failure", async () => {
|
|
392
|
+
const mockAws = createMockAwsClient({
|
|
393
|
+
sendError: { name: "ServiceUnavailable", message: "Try later" },
|
|
394
|
+
});
|
|
395
|
+
const client = new sesClient(
|
|
396
|
+
{ region: "eu-north-1", defaultFrom: "noreply@example.com" },
|
|
397
|
+
mockAws
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const result = await client.sendBulk({
|
|
401
|
+
template: "Newsletter",
|
|
402
|
+
destinations: [{ to: ["a@example.com"] }],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(result.success).toBe(false);
|
|
406
|
+
expect(result.successCount).toBe(0);
|
|
407
|
+
expect(result.failureCount).toBe(1);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("verifySetup()", () => {
|
|
412
|
+
it("should return verified status", async () => {
|
|
413
|
+
const mockAws = createMockAwsClient({
|
|
414
|
+
sendResponse: { ProductionAccessEnabled: true },
|
|
415
|
+
configRegion: "eu-north-1",
|
|
416
|
+
});
|
|
417
|
+
const client = new sesClient(
|
|
418
|
+
{ region: "eu-north-1" },
|
|
419
|
+
mockAws
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const status = await client.verifySetup();
|
|
423
|
+
|
|
424
|
+
expect(status.verified).toBe(true);
|
|
425
|
+
expect(status.sendingEnabled).toBe(true);
|
|
426
|
+
expect(status.region).toBe("eu-north-1");
|
|
427
|
+
expect(status.error).toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should report sandbox mode", async () => {
|
|
431
|
+
const mockAws = createMockAwsClient({
|
|
432
|
+
sendResponse: { ProductionAccessEnabled: false },
|
|
433
|
+
});
|
|
434
|
+
const client = new sesClient(
|
|
435
|
+
{ region: "eu-north-1" },
|
|
436
|
+
mockAws
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const status = await client.verifySetup();
|
|
440
|
+
|
|
441
|
+
expect(status.verified).toBe(true);
|
|
442
|
+
expect(status.sendingEnabled).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("should handle credential errors", async () => {
|
|
446
|
+
const mockAws = createMockAwsClient({
|
|
447
|
+
sendError: {
|
|
448
|
+
name: "InvalidClientTokenId",
|
|
449
|
+
message: "The security token included in the request is invalid",
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
const client = new sesClient(
|
|
453
|
+
{ region: "eu-north-1" },
|
|
454
|
+
mockAws
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const status = await client.verifySetup();
|
|
458
|
+
|
|
459
|
+
expect(status.verified).toBe(false);
|
|
460
|
+
expect(status.sendingEnabled).toBe(false);
|
|
461
|
+
expect(status.error).toContain("security token");
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { FlinkApp, FlinkPlugin } from "@flink-app/flink";
|
|
|
2
2
|
import { client } from "./schemas/client";
|
|
3
3
|
export { sendgridClient } from "./sendgridClient";
|
|
4
4
|
export { smtpClient } from "./smtpClient";
|
|
5
|
-
export { flowMailerClient } from "./flowmailerClient"
|
|
5
|
+
export { flowMailerClient } from "./flowmailerClient";
|
|
6
|
+
export { sesClient } from "./sesClient";
|
|
6
7
|
export type { email } from "./schemas/email";
|
|
8
|
+
export type { emailSes } from "./schemas/emailSes";
|
|
9
|
+
export * from "./ses-types";
|
|
7
10
|
|
|
8
11
|
export type emailPluginOptions = {
|
|
9
12
|
/**
|
package/src/schemas/client.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { email } from "./email";
|
|
2
2
|
import { emailFlowmailer } from "./emailFlowmailer";
|
|
3
3
|
import { emailSendgrid } from "./emailSendgrid";
|
|
4
|
+
import { emailSes } from "./emailSes";
|
|
4
5
|
|
|
5
6
|
export interface client {
|
|
6
|
-
send(email: email | emailSendgrid |
|
|
7
|
+
send(email: email | emailSendgrid | emailFlowmailer | emailSes): Promise<boolean>;
|
|
7
8
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type emailSes = {
|
|
2
|
+
/**
|
|
3
|
+
* From address used to send the email
|
|
4
|
+
*/
|
|
5
|
+
from: string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Email addresses to send to
|
|
9
|
+
*/
|
|
10
|
+
to: string[];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reply-to addresses
|
|
14
|
+
*/
|
|
15
|
+
replyTo?: string[];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CC addresses
|
|
19
|
+
*/
|
|
20
|
+
cc?: string[];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Email addresses to add as BCC
|
|
24
|
+
*/
|
|
25
|
+
bcc?: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subject of email
|
|
29
|
+
*/
|
|
30
|
+
subject: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Plain text body
|
|
34
|
+
*/
|
|
35
|
+
text?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* HTML body
|
|
39
|
+
*/
|
|
40
|
+
html?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* File attachments
|
|
44
|
+
*/
|
|
45
|
+
attachments?: SesAttachment[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* SES configuration set for tracking (opens, clicks, bounces)
|
|
49
|
+
*/
|
|
50
|
+
configurationSet?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* SES tags for event filtering
|
|
54
|
+
*/
|
|
55
|
+
tags?: Record<string, string>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface SesAttachment {
|
|
59
|
+
/**
|
|
60
|
+
* Filename shown to recipient
|
|
61
|
+
*/
|
|
62
|
+
filename: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Raw content as Buffer or base64 string
|
|
66
|
+
*/
|
|
67
|
+
content: Buffer | string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* MIME content type (e.g. "application/pdf"). Defaults to "application/octet-stream"
|
|
71
|
+
*/
|
|
72
|
+
contentType?: string;
|
|
73
|
+
}
|
package/src/ses-types.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters for sending a templated email via SES
|
|
3
|
+
*/
|
|
4
|
+
export interface SesTemplatedEmailParams {
|
|
5
|
+
/** Sender address (overrides defaultFrom) */
|
|
6
|
+
from?: string;
|
|
7
|
+
/** Recipient addresses */
|
|
8
|
+
to: string[];
|
|
9
|
+
/** CC addresses */
|
|
10
|
+
cc?: string[];
|
|
11
|
+
/** BCC addresses */
|
|
12
|
+
bcc?: string[];
|
|
13
|
+
/** Reply-to addresses */
|
|
14
|
+
replyTo?: string[];
|
|
15
|
+
/** SES template name */
|
|
16
|
+
template: string;
|
|
17
|
+
/** Template data as key-value pairs (will be JSON.stringify'd for SES) */
|
|
18
|
+
templateData: Record<string, unknown>;
|
|
19
|
+
/** SES configuration set (overrides client default) */
|
|
20
|
+
configurationSet?: string;
|
|
21
|
+
/** SES tags for event filtering */
|
|
22
|
+
tags?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parameters for sending bulk emails via SES template
|
|
27
|
+
*/
|
|
28
|
+
export interface SesBulkEmailParams {
|
|
29
|
+
/** Sender address (overrides defaultFrom) */
|
|
30
|
+
from?: string;
|
|
31
|
+
/** SES template name */
|
|
32
|
+
template: string;
|
|
33
|
+
/** Default template data applied to all destinations */
|
|
34
|
+
defaultTemplateData?: Record<string, unknown>;
|
|
35
|
+
/** Individual recipients with optional per-recipient template overrides */
|
|
36
|
+
destinations: SesBulkDestination[];
|
|
37
|
+
/** SES configuration set (overrides client default) */
|
|
38
|
+
configurationSet?: string;
|
|
39
|
+
/** Reply-to addresses */
|
|
40
|
+
replyTo?: string[];
|
|
41
|
+
/** SES tags for event filtering */
|
|
42
|
+
tags?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SesBulkDestination {
|
|
46
|
+
to: string[];
|
|
47
|
+
cc?: string[];
|
|
48
|
+
bcc?: string[];
|
|
49
|
+
/** Per-destination template data (merged with defaultTemplateData) */
|
|
50
|
+
templateData?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Result from a single send operation
|
|
55
|
+
*/
|
|
56
|
+
export interface SesResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
/** AWS MessageId, present on success */
|
|
59
|
+
messageId?: string;
|
|
60
|
+
/** Error details, present on failure */
|
|
61
|
+
error?: { code: string; message: string };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result from a bulk send operation
|
|
66
|
+
*/
|
|
67
|
+
export interface SesBulkResult {
|
|
68
|
+
success: boolean;
|
|
69
|
+
/** Per-destination results */
|
|
70
|
+
results: Array<{
|
|
71
|
+
success: boolean;
|
|
72
|
+
messageId?: string;
|
|
73
|
+
error?: { code: string; message: string };
|
|
74
|
+
}>;
|
|
75
|
+
/** Number of successful sends */
|
|
76
|
+
successCount: number;
|
|
77
|
+
/** Number of failed sends */
|
|
78
|
+
failureCount: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Result from verifySetup()
|
|
83
|
+
*/
|
|
84
|
+
export interface SesSetupStatus {
|
|
85
|
+
/** Whether credentials are valid and SES is accessible */
|
|
86
|
+
verified: boolean;
|
|
87
|
+
/** Whether the account is out of the SES sandbox */
|
|
88
|
+
sendingEnabled: boolean;
|
|
89
|
+
/** The configured AWS region */
|
|
90
|
+
region: string;
|
|
91
|
+
/** Error message if verification failed */
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|