@flink-app/email-plugin 1.0.0 → 2.0.0-alpha.100
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 +370 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +20 -1
- package/dist/postmarkClient.d.ts +19 -0
- package/dist/postmarkClient.js +91 -0
- package/dist/schemas/client.d.ts +3 -1
- package/dist/schemas/emailPostmark.d.ts +67 -0
- package/dist/schemas/emailPostmark.js +2 -0
- 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 +10 -4
- package/readme.md +258 -7
- package/spec/sesClient.spec.ts +464 -0
- package/spec/support/jasmine.json +7 -0
- package/src/index.ts +7 -2
- package/src/postmarkClient.ts +53 -0
- package/src/schemas/client.ts +3 -1
- package/src/schemas/emailPostmark.ts +73 -0
- 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,13 @@ 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";
|
|
7
|
+
export { postmarkClient } from "./postmarkClient";
|
|
6
8
|
export type { email } from "./schemas/email";
|
|
9
|
+
export type { emailSes } from "./schemas/emailSes";
|
|
10
|
+
export type { emailPostmark, PostmarkAttachment } from "./schemas/emailPostmark";
|
|
11
|
+
export * from "./ses-types";
|
|
7
12
|
|
|
8
13
|
export type emailPluginOptions = {
|
|
9
14
|
/**
|
|
@@ -12,7 +17,7 @@ export type emailPluginOptions = {
|
|
|
12
17
|
client: client;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
|
-
export interface
|
|
20
|
+
export interface EmailPluginContext {
|
|
16
21
|
emailPlugin: {
|
|
17
22
|
client: client;
|
|
18
23
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ServerClient } from "postmark";
|
|
2
|
+
import { emailPostmark as email } from "./schemas/emailPostmark";
|
|
3
|
+
import { client } from "./schemas/client";
|
|
4
|
+
|
|
5
|
+
export interface postmarkClientOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Postmark server token
|
|
8
|
+
*/
|
|
9
|
+
serverToken: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default Postmark message stream, defaults to "outbound"
|
|
13
|
+
*/
|
|
14
|
+
defaultMessageStream?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class postmarkClient implements client {
|
|
18
|
+
postmark: ServerClient;
|
|
19
|
+
defaultMessageStream: string;
|
|
20
|
+
|
|
21
|
+
constructor(options: postmarkClientOptions) {
|
|
22
|
+
this.postmark = new ServerClient(options.serverToken);
|
|
23
|
+
this.defaultMessageStream = options.defaultMessageStream ?? "outbound";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async send(email: email) {
|
|
27
|
+
try {
|
|
28
|
+
await this.postmark.sendEmail({
|
|
29
|
+
From: email.from,
|
|
30
|
+
To: email.to.join(","),
|
|
31
|
+
Cc: email.cc?.join(","),
|
|
32
|
+
Bcc: email.bcc?.join(","),
|
|
33
|
+
ReplyTo: email.replyTo,
|
|
34
|
+
Subject: email.subject,
|
|
35
|
+
HtmlBody: "html" in email ? email.html : undefined,
|
|
36
|
+
TextBody: "text" in email ? email.text : undefined,
|
|
37
|
+
MessageStream: email.messageStream ?? this.defaultMessageStream,
|
|
38
|
+
Tag: email.tag,
|
|
39
|
+
Metadata: email.metadata,
|
|
40
|
+
Attachments: email.attachments?.map(a => ({
|
|
41
|
+
Name: a.filename,
|
|
42
|
+
Content: a.content,
|
|
43
|
+
ContentType: a.contentType ?? "application/octet-stream",
|
|
44
|
+
ContentID: a.contentId ?? "",
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
} catch (ex) {
|
|
48
|
+
console.log(JSON.stringify(ex));
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/schemas/client.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { email } from "./email";
|
|
2
2
|
import { emailFlowmailer } from "./emailFlowmailer";
|
|
3
|
+
import { emailPostmark } from "./emailPostmark";
|
|
3
4
|
import { emailSendgrid } from "./emailSendgrid";
|
|
5
|
+
import { emailSes } from "./emailSes";
|
|
4
6
|
|
|
5
7
|
export interface client {
|
|
6
|
-
send(email: email | emailSendgrid |
|
|
8
|
+
send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark): Promise<boolean>;
|
|
7
9
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type emailPostmark = {
|
|
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 address
|
|
14
|
+
*/
|
|
15
|
+
replyTo?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CC addresses
|
|
19
|
+
*/
|
|
20
|
+
cc?: string[];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* BCC addresses
|
|
24
|
+
*/
|
|
25
|
+
bcc?: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subject of email
|
|
29
|
+
*/
|
|
30
|
+
subject: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Postmark message stream id, defaults to "outbound"
|
|
34
|
+
*/
|
|
35
|
+
messageStream?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Postmark tag for categorization
|
|
39
|
+
*/
|
|
40
|
+
tag?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom metadata sent along with the email
|
|
44
|
+
*/
|
|
45
|
+
metadata?: Record<string, string>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* File attachments
|
|
49
|
+
*/
|
|
50
|
+
attachments?: PostmarkAttachment[];
|
|
51
|
+
} & ({ text: string } | { html: string } | { text: string; html: string });
|
|
52
|
+
|
|
53
|
+
export interface PostmarkAttachment {
|
|
54
|
+
/**
|
|
55
|
+
* Base64 encoded content
|
|
56
|
+
*/
|
|
57
|
+
content: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Filename shown to recipient
|
|
61
|
+
*/
|
|
62
|
+
filename: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* MIME content type (e.g. "application/pdf"). Defaults to "application/octet-stream"
|
|
66
|
+
*/
|
|
67
|
+
contentType?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Content-ID for inline attachments, e.g. "cid:image1"
|
|
71
|
+
*/
|
|
72
|
+
contentId?: string;
|
|
73
|
+
}
|
|
@@ -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
|
+
}
|