@iskra-bun/mailer-kit 0.1.0 → 0.2.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-P5HWUNVS.js → chunk-4OJHFX3J.js} +26 -29
  4. package/dist/chunk-4OJHFX3J.js.map +1 -0
  5. package/dist/{chunk-H6SSV4G4.js → chunk-IACNO3YI.js} +31 -8
  6. package/dist/chunk-IACNO3YI.js.map +1 -0
  7. package/dist/chunk-U4ZHXPGW.js +65 -0
  8. package/dist/chunk-U4ZHXPGW.js.map +1 -0
  9. package/dist/chunk-URXCRWND.js +74 -0
  10. package/dist/chunk-URXCRWND.js.map +1 -0
  11. package/dist/{chunk-4Y2FQY2L.js → chunk-VRF3QYGZ.js} +24 -7
  12. package/dist/chunk-VRF3QYGZ.js.map +1 -0
  13. package/dist/index.d.ts +40 -10
  14. package/dist/index.js +24 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/mailgun-4CTSFUTK.js +8 -0
  17. package/dist/sendgrid-T6ZZERVV.js +8 -0
  18. package/dist/ses-WL4MNF4F.js +8 -0
  19. package/dist/smtp-J33QQBJ7.js +8 -0
  20. package/package.json +6 -3
  21. package/src/factory.ts +11 -11
  22. package/src/headers.ts +112 -0
  23. package/src/index.ts +9 -8
  24. package/src/mock.ts +14 -4
  25. package/src/providers/mailgun.ts +47 -54
  26. package/src/providers/sendgrid.ts +47 -19
  27. package/src/providers/ses.ts +44 -18
  28. package/src/providers/smtp.ts +35 -17
  29. package/src/types.ts +23 -6
  30. package/dist/chunk-4256VN6L.js +0 -40
  31. package/dist/chunk-4256VN6L.js.map +0 -1
  32. package/dist/chunk-4Y2FQY2L.js.map +0 -1
  33. package/dist/chunk-H6SSV4G4.js.map +0 -1
  34. package/dist/chunk-P5HWUNVS.js.map +0 -1
  35. package/dist/mailgun-CKVSFBJE.js +0 -7
  36. package/dist/sendgrid-EYSREU2M.js +0 -7
  37. package/dist/ses-KGJG5IB5.js +0 -7
  38. package/dist/smtp-GZ4DZMCQ.js +0 -7
  39. /package/dist/{mailgun-CKVSFBJE.js.map → mailgun-4CTSFUTK.js.map} +0 -0
  40. /package/dist/{sendgrid-EYSREU2M.js.map → sendgrid-T6ZZERVV.js.map} +0 -0
  41. /package/dist/{ses-KGJG5IB5.js.map → ses-WL4MNF4F.js.map} +0 -0
  42. /package/dist/{smtp-GZ4DZMCQ.js.map → smtp-J33QQBJ7.js.map} +0 -0
@@ -1,4 +1,5 @@
1
- import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../types";
1
+ import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from '../types';
2
+ import { checkRecipients, checkReplyTo, formatAddress, formatRecipient } from '../headers';
2
3
 
3
4
  /**
4
5
  * Minimal structural shape of the value returned by
@@ -34,6 +35,7 @@ export class SesEmailAdapter implements EmailAdapter {
34
35
  private defaultFrom?: { name?: string; email: string };
35
36
  private injectedClient?: SesClient;
36
37
  private injectedCommand?: SesCommandFactory;
38
+ private sdk?: Promise<{ client: SesClient; command: SesCommandFactory }>;
37
39
 
38
40
  constructor(config: EmailConfig, deps: SesAdapterDeps = {}) {
39
41
  // The effective `from` address is validated at send time (it can come
@@ -44,17 +46,25 @@ export class SesEmailAdapter implements EmailAdapter {
44
46
  this.injectedCommand = deps.sendEmailCommand;
45
47
  }
46
48
 
47
- private async resolveSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {
49
+ private resolveSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {
48
50
  if (this.injectedClient && this.injectedCommand) {
49
- return { client: this.injectedClient, command: this.injectedCommand };
51
+ return Promise.resolve({ client: this.injectedClient, command: this.injectedCommand });
50
52
  }
53
+ // One client per adapter, built on the first send. A failed import is
54
+ // not cached, so the next send retries it.
55
+ return (this.sdk ??= this.loadSdk().catch((err: unknown) => {
56
+ this.sdk = undefined;
57
+ throw err;
58
+ }));
59
+ }
51
60
 
61
+ private async loadSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {
52
62
  // Lazy load only when actually sending so `tsc`/`bun test` stay green
53
63
  // without `@aws-sdk/client-sesv2` installed. The specifier is held in a
54
64
  // variable so TypeScript does not try to resolve the module at compile
55
65
  // time (it is a real runtime dependency, declared in package.json).
56
- const sdkModule = "@aws-sdk/client-sesv2";
57
- const mod: any = await import(sdkModule);
66
+ const sdkModule = '@aws-sdk/client-sesv2';
67
+ const mod = (await import(sdkModule)) as Record<string, unknown>;
58
68
  const SESv2Client = mod.SESv2Client as new (cfg: { region?: string }) => SesClient;
59
69
  const SendEmailCommand = mod.SendEmailCommand as new (input: unknown) => SesCommand;
60
70
 
@@ -66,41 +76,57 @@ export class SesEmailAdapter implements EmailAdapter {
66
76
 
67
77
  async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {
68
78
  const from = message.from || this.defaultFrom;
69
- if (!from) throw new Error("From address required");
79
+ if (!from) throw new Error('From address required');
80
+ // Simple content has no place for them: they used to be dropped while
81
+ // the send reported success.
82
+ if (message.attachments?.length) {
83
+ throw new Error('Attachments are not supported by the ses adapter yet');
84
+ }
85
+ if (message.headers && Object.keys(message.headers).length > 0) {
86
+ throw new Error('Custom headers are not supported by the ses adapter yet');
87
+ }
70
88
 
71
- const { client, command } = await this.resolveSdk();
89
+ // SES parses each entry as an address list: one mailbox per entry.
90
+ const toAddresses = checkRecipients(message.to).map(formatRecipient);
91
+ const cc = checkRecipients(message.cc, 'cc recipient').map(formatRecipient);
92
+ const bcc = checkRecipients(message.bcc, 'bcc recipient').map(formatRecipient);
93
+ const ccAddresses = cc.length > 0 ? cc : undefined;
94
+ const bccAddresses = bcc.length > 0 ? bcc : undefined;
95
+ const replyTo = checkReplyTo(message.replyTo);
72
96
 
73
- const toAddresses = Array.isArray(message.to) ? message.to : [message.to];
74
- const ccAddresses = message.cc ? (Array.isArray(message.cc) ? message.cc : [message.cc]) : undefined;
75
- const bccAddresses = message.bcc ? (Array.isArray(message.bcc) ? message.bcc : [message.bcc]) : undefined;
97
+ const { client, command } = await this.resolveSdk();
76
98
 
77
99
  const body: Record<string, { Data: string; Charset: string }> = {};
78
- if (message.text) body.Text = { Data: message.text, Charset: "UTF-8" };
79
- if (message.html) body.Html = { Data: message.html, Charset: "UTF-8" };
100
+ if (message.text) body.Text = { Data: message.text, Charset: 'UTF-8' };
101
+ if (message.html) body.Html = { Data: message.html, Charset: 'UTF-8' };
80
102
 
81
103
  const input = {
82
- FromEmailAddress: from.name ? `${from.name} <${from.email}>` : from.email,
104
+ FromEmailAddress: formatAddress(from),
83
105
  Destination: {
84
106
  ToAddresses: toAddresses,
85
107
  ...(ccAddresses ? { CcAddresses: ccAddresses } : {}),
86
108
  ...(bccAddresses ? { BccAddresses: bccAddresses } : {}),
87
109
  },
88
- ...(message.replyTo ? { ReplyToAddresses: [message.replyTo] } : {}),
110
+ ...(replyTo ? { ReplyToAddresses: [formatRecipient(replyTo)] } : {}),
89
111
  Content: {
90
112
  Simple: {
91
- Subject: { Data: message.subject, Charset: "UTF-8" },
113
+ Subject: { Data: message.subject, Charset: 'UTF-8' },
92
114
  Body: body,
93
115
  },
94
116
  },
95
117
  };
96
118
 
97
119
  const result = await client.send(command(input));
98
- return { messageId: result.MessageId ?? "", success: true };
120
+ return { messageId: result.MessageId ?? '', success: true };
99
121
  }
100
122
 
101
- async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
123
+ async sendTemplate(
124
+ _templateName: string,
125
+ _to: string | string[],
126
+ _data: TemplateData,
127
+ ): Promise<{ messageId: string; success: boolean }> {
102
128
  // No template engine is implemented yet; fail loudly rather than
103
129
  // silently sending a placeholder body that looks like a real send.
104
- throw new Error("sendTemplate not supported by ses");
130
+ throw new Error('sendTemplate not supported by ses');
105
131
  }
106
132
  }
@@ -1,11 +1,15 @@
1
- import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../types";
2
- import * as nodemailer from "nodemailer";
1
+ import type { EmailAdapter, EmailAddress, EmailConfig, EmailMessage, TemplateData } from '../types';
2
+ import * as nodemailer from 'nodemailer';
3
+ import { checkEmail, checkHeaders, checkRecipients, checkReplyTo, cleanName } from '../headers';
4
+
5
+ /** nodemailer's form of a checked recipient: it quotes or encodes the name itself. */
6
+ const toMailbox = ({ name, address }: EmailAddress) => (name ? { name, address } : address);
3
7
 
4
8
  export class SmtpEmailAdapter implements EmailAdapter {
5
9
  private transporter: nodemailer.Transporter;
6
10
 
7
11
  constructor(private config: EmailConfig) {
8
- if (!config.smtp) throw new Error("SMTP config required");
12
+ if (!config.smtp) throw new Error('SMTP config required');
9
13
  // Implicit TLS on port 465; STARTTLS enforced (requireTLS) elsewhere so
10
14
  // credentials never transit in cleartext. An explicit `secure` wins.
11
15
  const secure = config.smtp.secure ?? config.smtp.port === 465;
@@ -17,37 +21,51 @@ export class SmtpEmailAdapter implements EmailAdapter {
17
21
  tls: { rejectUnauthorized: true },
18
22
  auth: {
19
23
  user: config.smtp.username,
20
- pass: config.smtp.password
21
- }
24
+ pass: config.smtp.password,
25
+ },
22
26
  });
23
27
  }
24
28
 
25
29
  async send(message: EmailMessage) {
26
30
  const from = message.from || this.config.from;
27
- if (!from) throw new Error("From address required");
31
+ if (!from) throw new Error('From address required');
32
+ // Joined into one string, nodemailer parsed each value as an address
33
+ // list: one entry could add recipients or a group.
34
+ const cc = checkRecipients(message.cc, 'cc recipient').map(toMailbox);
35
+ const bcc = checkRecipients(message.bcc, 'bcc recipient').map(toMailbox);
36
+ const replyTo = checkReplyTo(message.replyTo);
28
37
 
29
38
  const info = await this.transporter.sendMail({
30
- from: from.name ? `"${from.name}" <${from.email}>` : from.email,
31
- to: Array.isArray(message.to) ? message.to.join(", ") : message.to,
39
+ // An address object: nodemailer quotes or encodes the name, which
40
+ // interpolated into `"name" <email>` could add another sender.
41
+ from: { name: from.name ? cleanName(from.name) : '', address: checkEmail(from.email) },
42
+ to: checkRecipients(message.to).map(toMailbox),
32
43
  subject: message.subject,
33
44
  text: message.text,
34
45
  html: message.html,
35
- cc: message.cc ? (Array.isArray(message.cc) ? message.cc.join(", ") : message.cc) : undefined,
36
- bcc: message.bcc ? (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc) : undefined,
37
- replyTo: message.replyTo,
38
- attachments: message.attachments?.map(a => ({
46
+ cc: cc.length > 0 ? cc : undefined,
47
+ bcc: bcc.length > 0 ? bcc : undefined,
48
+ replyTo: replyTo && toMailbox(replyTo),
49
+ attachments: message.attachments?.map((a) => ({
39
50
  filename: a.filename,
40
51
  content: typeof a.content === 'string' ? a.content : Buffer.from(a.content),
41
- contentType: a.contentType
42
- }))
52
+ contentType: a.contentType,
53
+ })),
54
+ // Forwarded with the same allowlist as the other providers (it
55
+ // used to be dropped).
56
+ headers: checkHeaders(message.headers),
43
57
  });
44
58
 
45
- return { messageId: (info as any).messageId, success: true };
59
+ return { messageId: info.messageId, success: true };
46
60
  }
47
61
 
48
- async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
62
+ async sendTemplate(
63
+ _templateName: string,
64
+ _to: string | string[],
65
+ _data: TemplateData,
66
+ ): Promise<{ messageId: string; success: boolean }> {
49
67
  // No template engine is implemented yet; fail loudly rather than
50
68
  // silently sending a placeholder body that looks like a real send.
51
- throw new Error("sendTemplate not supported by smtp");
69
+ throw new Error('sendTemplate not supported by smtp');
52
70
  }
53
71
  }
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface EmailConfig {
2
- provider: "smtp" | "sendgrid" | "mock" | "mailgun" | "ses";
2
+ provider: 'smtp' | 'sendgrid' | 'mock' | 'mailgun' | 'ses';
3
3
  smtp?: {
4
4
  host: string;
5
5
  port: number;
@@ -16,15 +16,28 @@ export interface EmailConfig {
16
16
  templateDir?: string;
17
17
  }
18
18
 
19
+ /** An address with an optional display name. */
20
+ export interface EmailAddress {
21
+ name?: string;
22
+ address: string;
23
+ }
24
+
25
+ /** A recipient: a bare address, or `{ name, address }` to give it a display name. */
26
+ export type EmailRecipient = string | EmailAddress;
27
+
19
28
  export interface EmailMessage {
20
- to: string | string[];
29
+ /**
30
+ * Each entry is one bare address (`a@x.com`), or `{ name, address }` for a
31
+ * display name: a string with a name, a list or a group is rejected.
32
+ */
33
+ to: EmailRecipient | EmailRecipient[];
21
34
  from?: { name?: string; email: string };
22
35
  subject: string;
23
36
  text?: string;
24
37
  html?: string;
25
- cc?: string | string[];
26
- bcc?: string | string[];
27
- replyTo?: string;
38
+ cc?: EmailRecipient | EmailRecipient[];
39
+ bcc?: EmailRecipient | EmailRecipient[];
40
+ replyTo?: EmailRecipient;
28
41
  attachments?: Array<{ filename: string; content: Uint8Array | string; contentType?: string }>;
29
42
  headers?: Record<string, string>;
30
43
  }
@@ -35,5 +48,9 @@ export interface TemplateData {
35
48
 
36
49
  export interface EmailAdapter {
37
50
  send(message: EmailMessage): Promise<{ messageId: string; success: boolean }>;
38
- sendTemplate(templateName: string, to: string | string[], data: TemplateData): Promise<{ messageId: string; success: boolean }>;
51
+ sendTemplate(
52
+ templateName: string,
53
+ to: EmailRecipient | EmailRecipient[],
54
+ data: TemplateData,
55
+ ): Promise<{ messageId: string; success: boolean }>;
39
56
  }
@@ -1,40 +0,0 @@
1
- // src/providers/sendgrid.ts
2
- import sgMail from "@sendgrid/mail";
3
- var SendGridEmailAdapter = class {
4
- constructor(config) {
5
- this.config = config;
6
- if (!config.apiKey) throw new Error("SendGrid API Key required");
7
- sgMail.setApiKey(config.apiKey);
8
- }
9
- config;
10
- async send(message) {
11
- const from = message.from || this.config.from;
12
- if (!from) throw new Error("From address required");
13
- const msg = {
14
- to: message.to,
15
- from: from.name ? { email: from.email, name: from.name } : from.email,
16
- subject: message.subject,
17
- text: message.text,
18
- html: message.html,
19
- cc: message.cc,
20
- bcc: message.bcc,
21
- replyTo: message.replyTo,
22
- attachments: message.attachments?.map((a) => ({
23
- filename: a.filename,
24
- content: typeof a.content === "string" ? a.content : Buffer.from(a.content).toString("base64"),
25
- type: a.contentType,
26
- disposition: "attachment"
27
- }))
28
- };
29
- const [response] = await sgMail.send(msg);
30
- return { messageId: response.headers["x-message-id"], success: true };
31
- }
32
- async sendTemplate(_templateName, _to, _data) {
33
- throw new Error("sendTemplate not supported by sendgrid");
34
- }
35
- };
36
-
37
- export {
38
- SendGridEmailAdapter
39
- };
40
- //# sourceMappingURL=chunk-4256VN6L.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/providers/sendgrid.ts"],"sourcesContent":["import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from \"../types\";\nimport sgMail from \"@sendgrid/mail\";\n\nexport class SendGridEmailAdapter implements EmailAdapter {\n constructor(private config: EmailConfig) {\n if (!config.apiKey) throw new Error(\"SendGrid API Key required\");\n sgMail.setApiKey(config.apiKey);\n }\n\n async send(message: EmailMessage) {\n const from = message.from || this.config.from;\n if (!from) throw new Error(\"From address required\");\n\n const msg = {\n to: message.to,\n from: from.name ? { email: from.email, name: from.name } : from.email,\n subject: message.subject,\n text: message.text,\n html: message.html,\n cc: message.cc as any,\n bcc: message.bcc as any,\n replyTo: message.replyTo,\n attachments: message.attachments?.map(a => ({\n filename: a.filename,\n content: typeof a.content === 'string' ? a.content : Buffer.from(a.content).toString(\"base64\"),\n type: a.contentType,\n disposition: \"attachment\"\n }))\n } as any;\n\n const [response] = await sgMail.send(msg);\n return { messageId: response.headers[\"x-message-id\"] as string, success: true };\n }\n\n async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error(\"sendTemplate not supported by sendgrid\");\n }\n}\n"],"mappings":";AACA,OAAO,YAAY;AAEZ,IAAM,uBAAN,MAAmD;AAAA,EACtD,YAAoB,QAAqB;AAArB;AAChB,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AAC/D,WAAO,UAAU,OAAO,MAAM;AAAA,EAClC;AAAA,EAHoB;AAAA,EAKpB,MAAM,KAAK,SAAuB;AAC9B,UAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;AACzC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAElD,UAAM,MAAM;AAAA,MACR,IAAI,QAAQ;AAAA,MACZ,MAAM,KAAK,OAAO,EAAE,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,IAAI,KAAK;AAAA,MAChE,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,MACd,IAAI,QAAQ;AAAA,MACZ,KAAK,QAAQ;AAAA,MACb,SAAS,QAAQ;AAAA,MACjB,aAAa,QAAQ,aAAa,IAAI,QAAM;AAAA,QACxC,UAAU,EAAE;AAAA,QACZ,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,QAAQ;AAAA,QAC7F,MAAM,EAAE;AAAA,QACR,aAAa;AAAA,MACjB,EAAE;AAAA,IACN;AAEA,UAAM,CAAC,QAAQ,IAAI,MAAM,OAAO,KAAK,GAAG;AACxC,WAAO,EAAE,WAAW,SAAS,QAAQ,cAAc,GAAa,SAAS,KAAK;AAAA,EAClF;AAAA,EAEA,MAAM,aAAa,eAAuB,KAAwB,OAAuE;AAGrI,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC5D;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/providers/smtp.ts"],"sourcesContent":["import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from \"../types\";\nimport * as nodemailer from \"nodemailer\";\n\nexport class SmtpEmailAdapter implements EmailAdapter {\n private transporter: nodemailer.Transporter;\n\n constructor(private config: EmailConfig) {\n if (!config.smtp) throw new Error(\"SMTP config required\");\n // Implicit TLS on port 465; STARTTLS enforced (requireTLS) elsewhere so\n // credentials never transit in cleartext. An explicit `secure` wins.\n const secure = config.smtp.secure ?? config.smtp.port === 465;\n this.transporter = nodemailer.createTransport({\n host: config.smtp.host,\n port: config.smtp.port,\n secure,\n requireTLS: secure ? undefined : true,\n tls: { rejectUnauthorized: true },\n auth: {\n user: config.smtp.username,\n pass: config.smtp.password\n }\n });\n }\n\n async send(message: EmailMessage) {\n const from = message.from || this.config.from;\n if (!from) throw new Error(\"From address required\");\n\n const info = await this.transporter.sendMail({\n from: from.name ? `\"${from.name}\" <${from.email}>` : from.email,\n to: Array.isArray(message.to) ? message.to.join(\", \") : message.to,\n subject: message.subject,\n text: message.text,\n html: message.html,\n cc: message.cc ? (Array.isArray(message.cc) ? message.cc.join(\", \") : message.cc) : undefined,\n bcc: message.bcc ? (Array.isArray(message.bcc) ? message.bcc.join(\", \") : message.bcc) : undefined,\n replyTo: message.replyTo,\n attachments: message.attachments?.map(a => ({\n filename: a.filename,\n content: typeof a.content === 'string' ? a.content : Buffer.from(a.content),\n contentType: a.contentType\n }))\n });\n\n return { messageId: (info as any).messageId, success: true };\n }\n\n async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error(\"sendTemplate not supported by smtp\");\n }\n}\n"],"mappings":";AACA,YAAY,gBAAgB;AAErB,IAAM,mBAAN,MAA+C;AAAA,EAGlD,YAAoB,QAAqB;AAArB;AAChB,QAAI,CAAC,OAAO,KAAM,OAAM,IAAI,MAAM,sBAAsB;AAGxD,UAAM,SAAS,OAAO,KAAK,UAAU,OAAO,KAAK,SAAS;AAC1D,SAAK,cAAyB,2BAAgB;AAAA,MAC1C,MAAM,OAAO,KAAK;AAAA,MAClB,MAAM,OAAO,KAAK;AAAA,MAClB;AAAA,MACA,YAAY,SAAS,SAAY;AAAA,MACjC,KAAK,EAAE,oBAAoB,KAAK;AAAA,MAChC,MAAM;AAAA,QACF,MAAM,OAAO,KAAK;AAAA,QAClB,MAAM,OAAO,KAAK;AAAA,MACtB;AAAA,IACJ,CAAC;AAAA,EACL;AAAA,EAhBoB;AAAA,EAFZ;AAAA,EAoBR,MAAM,KAAK,SAAuB;AAC9B,UAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;AACzC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAElD,UAAM,OAAO,MAAM,KAAK,YAAY,SAAS;AAAA,MACzC,MAAM,KAAK,OAAO,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,MAAM,KAAK;AAAA,MAC1D,IAAI,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,GAAG,KAAK,IAAI,IAAI,QAAQ;AAAA,MAChE,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,MACd,IAAI,QAAQ,KAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,GAAG,KAAK,IAAI,IAAI,QAAQ,KAAM;AAAA,MACpF,KAAK,QAAQ,MAAO,MAAM,QAAQ,QAAQ,GAAG,IAAI,QAAQ,IAAI,KAAK,IAAI,IAAI,QAAQ,MAAO;AAAA,MACzF,SAAS,QAAQ;AAAA,MACjB,aAAa,QAAQ,aAAa,IAAI,QAAM;AAAA,QACxC,UAAU,EAAE;AAAA,QACZ,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,OAAO,KAAK,EAAE,OAAO;AAAA,QAC1E,aAAa,EAAE;AAAA,MACnB,EAAE;AAAA,IACN,CAAC;AAED,WAAO,EAAE,WAAY,KAAa,WAAW,SAAS,KAAK;AAAA,EAC/D;AAAA,EAEA,MAAM,aAAa,eAAuB,KAAwB,OAAuE;AAGrI,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACxD;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/providers/ses.ts"],"sourcesContent":["import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from \"../types\";\n\n/**\n * Minimal structural shape of the value returned by\n * `@aws-sdk/client-sesv2`'s `SendEmailCommand` constructor. We only need\n * an opaque object to hand back to `client.send`, so typing it as an\n * unknown-bearing record keeps `tsc` happy without the SDK installed.\n */\nexport interface SesCommand {\n readonly input: unknown;\n}\n\n/**\n * Structural type for the SESv2 client. Declared locally so this module\n * type-checks even when `@aws-sdk/client-sesv2` is not installed; the real\n * SDK client satisfies this shape at runtime.\n */\nexport interface SesClient {\n send(command: SesCommand): Promise<{ MessageId?: string }>;\n}\n\n/** Factory that builds a `SendEmailCommand` from its input object. */\nexport type SesCommandFactory = (input: unknown) => SesCommand;\n\nexport interface SesAdapterDeps {\n /** Pre-built client (used by tests to avoid importing the real SDK). */\n client?: SesClient;\n /** Pre-built command factory (used by tests to avoid importing the real SDK). */\n sendEmailCommand?: SesCommandFactory;\n}\n\nexport class SesEmailAdapter implements EmailAdapter {\n private region?: string;\n private defaultFrom?: { name?: string; email: string };\n private injectedClient?: SesClient;\n private injectedCommand?: SesCommandFactory;\n\n constructor(config: EmailConfig, deps: SesAdapterDeps = {}) {\n // The effective `from` address is validated at send time (it can come\n // from the message or the config), so construction stays permissive.\n this.region = config.region;\n this.defaultFrom = config.from;\n this.injectedClient = deps.client;\n this.injectedCommand = deps.sendEmailCommand;\n }\n\n private async resolveSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {\n if (this.injectedClient && this.injectedCommand) {\n return { client: this.injectedClient, command: this.injectedCommand };\n }\n\n // Lazy load only when actually sending so `tsc`/`bun test` stay green\n // without `@aws-sdk/client-sesv2` installed. The specifier is held in a\n // variable so TypeScript does not try to resolve the module at compile\n // time (it is a real runtime dependency, declared in package.json).\n const sdkModule = \"@aws-sdk/client-sesv2\";\n const mod: any = await import(sdkModule);\n const SESv2Client = mod.SESv2Client as new (cfg: { region?: string }) => SesClient;\n const SendEmailCommand = mod.SendEmailCommand as new (input: unknown) => SesCommand;\n\n const client = this.injectedClient ?? new SESv2Client({ region: this.region });\n const command: SesCommandFactory = this.injectedCommand ?? ((input) => new SendEmailCommand(input));\n\n return { client, command };\n }\n\n async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {\n const from = message.from || this.defaultFrom;\n if (!from) throw new Error(\"From address required\");\n\n const { client, command } = await this.resolveSdk();\n\n const toAddresses = Array.isArray(message.to) ? message.to : [message.to];\n const ccAddresses = message.cc ? (Array.isArray(message.cc) ? message.cc : [message.cc]) : undefined;\n const bccAddresses = message.bcc ? (Array.isArray(message.bcc) ? message.bcc : [message.bcc]) : undefined;\n\n const body: Record<string, { Data: string; Charset: string }> = {};\n if (message.text) body.Text = { Data: message.text, Charset: \"UTF-8\" };\n if (message.html) body.Html = { Data: message.html, Charset: \"UTF-8\" };\n\n const input = {\n FromEmailAddress: from.name ? `${from.name} <${from.email}>` : from.email,\n Destination: {\n ToAddresses: toAddresses,\n ...(ccAddresses ? { CcAddresses: ccAddresses } : {}),\n ...(bccAddresses ? { BccAddresses: bccAddresses } : {}),\n },\n ...(message.replyTo ? { ReplyToAddresses: [message.replyTo] } : {}),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: \"UTF-8\" },\n Body: body,\n },\n },\n };\n\n const result = await client.send(command(input));\n return { messageId: result.MessageId ?? \"\", success: true };\n }\n\n async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error(\"sendTemplate not supported by ses\");\n }\n}\n"],"mappings":";AA+BO,IAAM,kBAAN,MAA8C;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAqB,OAAuB,CAAC,GAAG;AAGxD,SAAK,SAAS,OAAO;AACrB,SAAK,cAAc,OAAO;AAC1B,SAAK,iBAAiB,KAAK;AAC3B,SAAK,kBAAkB,KAAK;AAAA,EAChC;AAAA,EAEA,MAAc,aAAyE;AACnF,QAAI,KAAK,kBAAkB,KAAK,iBAAiB;AAC7C,aAAO,EAAE,QAAQ,KAAK,gBAAgB,SAAS,KAAK,gBAAgB;AAAA,IACxE;AAMA,UAAM,YAAY;AAClB,UAAM,MAAW,MAAM,OAAO;AAC9B,UAAM,cAAc,IAAI;AACxB,UAAM,mBAAmB,IAAI;AAE7B,UAAM,SAAS,KAAK,kBAAkB,IAAI,YAAY,EAAE,QAAQ,KAAK,OAAO,CAAC;AAC7E,UAAM,UAA6B,KAAK,oBAAoB,CAAC,UAAU,IAAI,iBAAiB,KAAK;AAEjG,WAAO,EAAE,QAAQ,QAAQ;AAAA,EAC7B;AAAA,EAEA,MAAM,KAAK,SAAyE;AAChF,UAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAElD,UAAM,EAAE,QAAQ,QAAQ,IAAI,MAAM,KAAK,WAAW;AAElD,UAAM,cAAc,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC,QAAQ,EAAE;AACxE,UAAM,cAAc,QAAQ,KAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC,QAAQ,EAAE,IAAK;AAC3F,UAAM,eAAe,QAAQ,MAAO,MAAM,QAAQ,QAAQ,GAAG,IAAI,QAAQ,MAAM,CAAC,QAAQ,GAAG,IAAK;AAEhG,UAAM,OAA0D,CAAC;AACjE,QAAI,QAAQ,KAAM,MAAK,OAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,QAAQ;AACrE,QAAI,QAAQ,KAAM,MAAK,OAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,QAAQ;AAErE,UAAM,QAAQ;AAAA,MACV,kBAAkB,KAAK,OAAO,GAAG,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,KAAK;AAAA,MACpE,aAAa;AAAA,QACT,aAAa;AAAA,QACb,GAAI,cAAc,EAAE,aAAa,YAAY,IAAI,CAAC;AAAA,QAClD,GAAI,eAAe,EAAE,cAAc,aAAa,IAAI,CAAC;AAAA,MACzD;AAAA,MACA,GAAI,QAAQ,UAAU,EAAE,kBAAkB,CAAC,QAAQ,OAAO,EAAE,IAAI,CAAC;AAAA,MACjE,SAAS;AAAA,QACL,QAAQ;AAAA,UACJ,SAAS,EAAE,MAAM,QAAQ,SAAS,SAAS,QAAQ;AAAA,UACnD,MAAM;AAAA,QACV;AAAA,MACJ;AAAA,IACJ;AAEA,UAAM,SAAS,MAAM,OAAO,KAAK,QAAQ,KAAK,CAAC;AAC/C,WAAO,EAAE,WAAW,OAAO,aAAa,IAAI,SAAS,KAAK;AAAA,EAC9D;AAAA,EAEA,MAAM,aAAa,eAAuB,KAAwB,OAAuE;AAGrI,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACvD;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/providers/mailgun.ts"],"sourcesContent":["import type { EmailAdapter, EmailMessage, EmailConfig, TemplateData } from \"../types\";\n\n/**\n * Outbound custom mail headers callers are permitted to set. Anything outside\n * this set is rejected so a caller cannot spoof Reply-To / Sender / routing\n * headers via the generic `headers` map.\n */\nconst ALLOWED_HEADERS = new Set([\n \"reply-to\",\n \"in-reply-to\",\n \"references\",\n \"list-unsubscribe\",\n \"list-unsubscribe-post\",\n \"list-id\",\n \"x-mailgun-variables\",\n \"x-mailgun-tag\",\n]);\n\n/**\n * Truncate a header value at the first CR/LF. Anything after a line break is an\n * injected header (or folded continuation) and must be dropped, not preserved.\n */\nconst stripCrlf = (value: string): string => value.split(/[\\r\\n]/)[0] ?? \"\";\n\nexport class MailgunEmailAdapter implements EmailAdapter {\n private apiKey: string;\n private domain: string;\n private baseUrl: string;\n private defaultFrom?: { name?: string; email: string };\n\n constructor(config: EmailConfig) {\n if (!config.apiKey) throw new Error(\"Mailgun requires apiKey\");\n if (!config.domain) throw new Error(\"Mailgun requires domain\");\n\n this.apiKey = config.apiKey;\n this.domain = config.domain;\n this.baseUrl = config.baseUrl || \"https://api.mailgun.net/v3\";\n this.defaultFrom = config.from;\n }\n\n async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {\n const form = new FormData();\n\n const from = message.from || this.defaultFrom;\n if (from) {\n form.append(\"from\", from.name ? `${from.name} <${from.email}>` : from.email);\n }\n\n const to = Array.isArray(message.to) ? message.to.join(\",\") : message.to;\n form.append(\"to\", to);\n form.append(\"subject\", message.subject);\n\n if (message.text) form.append(\"text\", message.text);\n if (message.html) form.append(\"html\", message.html);\n if (message.cc) form.append(\"cc\", Array.isArray(message.cc) ? message.cc.join(\",\") : message.cc);\n if (message.bcc) form.append(\"bcc\", Array.isArray(message.bcc) ? message.bcc.join(\",\") : message.bcc);\n if (message.replyTo) form.append(\"h:Reply-To\", message.replyTo);\n\n if (message.headers) {\n for (const [key, value] of Object.entries(message.headers)) {\n if (!ALLOWED_HEADERS.has(key.toLowerCase())) {\n throw new Error(`Header \"${key}\" is not allowed`);\n }\n form.append(`h:${key}`, stripCrlf(value));\n }\n }\n\n if (message.attachments) {\n for (const att of message.attachments) {\n const content = typeof att.content === \"string\"\n ? new TextEncoder().encode(att.content)\n : att.content;\n const bytes = new Uint8Array(content);\n const blob = new Blob([bytes], { type: att.contentType || \"application/octet-stream\" });\n form.append(\"attachment\", blob, att.filename);\n }\n }\n\n const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {\n method: \"POST\",\n headers: {\n Authorization: \"Basic \" + btoa(`api:${this.apiKey}`),\n },\n body: form,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Mailgun API error (${response.status}): ${errorText}`);\n }\n\n const result = await response.json() as { id: string; message: string };\n return { messageId: result.id, success: true };\n }\n\n async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder that looks like a real send.\n throw new Error(\"sendTemplate not supported by mailgun\");\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +0,0 @@
1
- import {
2
- MailgunEmailAdapter
3
- } from "./chunk-P5HWUNVS.js";
4
- export {
5
- MailgunEmailAdapter
6
- };
7
- //# sourceMappingURL=mailgun-CKVSFBJE.js.map
@@ -1,7 +0,0 @@
1
- import {
2
- SendGridEmailAdapter
3
- } from "./chunk-4256VN6L.js";
4
- export {
5
- SendGridEmailAdapter
6
- };
7
- //# sourceMappingURL=sendgrid-EYSREU2M.js.map
@@ -1,7 +0,0 @@
1
- import {
2
- SesEmailAdapter
3
- } from "./chunk-H6SSV4G4.js";
4
- export {
5
- SesEmailAdapter
6
- };
7
- //# sourceMappingURL=ses-KGJG5IB5.js.map
@@ -1,7 +0,0 @@
1
- import {
2
- SmtpEmailAdapter
3
- } from "./chunk-4Y2FQY2L.js";
4
- export {
5
- SmtpEmailAdapter
6
- };
7
- //# sourceMappingURL=smtp-GZ4DZMCQ.js.map