@iskra-bun/mailer-kit 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @iskra-bun/mailer-kit
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f9654df: Initial public release. Transport-agnostic email kit extracted from web-kit: `EmailAdapter` interface, `MockEmailAdapter`, SMTP/SendGrid/Mailgun/SES providers, and a `createEmailAdapter` factory — usable from workers and cron jobs without an HTTP layer. The previously-unimplemented SES provider is now complete.
8
+
9
+ ### Patch Changes
10
+
11
+ - Mailer hardening across providers:
12
+
13
+ - `sendTemplate` now throws `sendTemplate not supported by <provider>` for the SMTP, Mailgun, SES and SendGrid providers instead of silently reporting success without sending anything.
14
+ - The SMTP provider enforces TLS: `secure: true` on port 465, otherwise `requireTLS: true`, with `tls.rejectUnauthorized: true`.
15
+ - The Mailgun provider applies a custom-header allowlist (throwing on non-allowlisted headers) and strips CR/LF from header values, preventing email header injection.
16
+
17
+ - Updated dependencies [f9654df]
18
+ - Updated dependencies
19
+ - Updated dependencies [f9654df]
20
+ - @iskra-bun/core@0.1.1
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @iskra-bun/mailer-kit
2
+
3
+ Envio de correo de Iskra, agnostico del transporte. Adaptadores intercambiables de SMTP, SendGrid, Mailgun, SES y un mock para tests. Sin acoplamiento HTTP: usalo en workers, cron o cualquier proceso.
4
+
5
+ ## Instalacion
6
+
7
+ ```bash
8
+ bun add @iskra-bun/mailer-kit @iskra-bun/core
9
+ ```
10
+
11
+ ## Uso rapido
12
+
13
+ ```typescript
14
+ import { createEmailAdapter } from '@iskra-bun/mailer-kit'
15
+
16
+ const mailer = await createEmailAdapter({
17
+ provider: 'smtp',
18
+ smtp: { host: 'smtp.example.com', port: 587, username: 'user', password: 'pass' },
19
+ from: { name: 'Mi App', email: 'noreply@example.com' },
20
+ })
21
+
22
+ await mailer.send({
23
+ to: 'usuario@example.com',
24
+ subject: 'Hola',
25
+ html: '<p>Bienvenido</p>',
26
+ })
27
+ ```
28
+
29
+ Cambia `provider` entre `mock`, `smtp`, `sendgrid`, `mailgun` o `ses`; la API (`send`/`sendTemplate`) es identica entre adaptadores.
30
+
31
+ ## Documentacion
32
+
33
+ Guia completa: [docs/mailer-kit.md](../../docs/mailer-kit.md)
34
+
35
+ ## Licencia
36
+
37
+ AGPL-3.0-or-later
@@ -0,0 +1,40 @@
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
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,50 @@
1
+ // src/providers/smtp.ts
2
+ import * as nodemailer from "nodemailer";
3
+ var SmtpEmailAdapter = class {
4
+ constructor(config) {
5
+ this.config = config;
6
+ if (!config.smtp) throw new Error("SMTP config required");
7
+ const secure = config.smtp.secure ?? config.smtp.port === 465;
8
+ this.transporter = nodemailer.createTransport({
9
+ host: config.smtp.host,
10
+ port: config.smtp.port,
11
+ secure,
12
+ requireTLS: secure ? void 0 : true,
13
+ tls: { rejectUnauthorized: true },
14
+ auth: {
15
+ user: config.smtp.username,
16
+ pass: config.smtp.password
17
+ }
18
+ });
19
+ }
20
+ config;
21
+ transporter;
22
+ async send(message) {
23
+ const from = message.from || this.config.from;
24
+ if (!from) throw new Error("From address required");
25
+ const info = await this.transporter.sendMail({
26
+ from: from.name ? `"${from.name}" <${from.email}>` : from.email,
27
+ to: Array.isArray(message.to) ? message.to.join(", ") : message.to,
28
+ subject: message.subject,
29
+ text: message.text,
30
+ html: message.html,
31
+ cc: message.cc ? Array.isArray(message.cc) ? message.cc.join(", ") : message.cc : void 0,
32
+ bcc: message.bcc ? Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc : void 0,
33
+ replyTo: message.replyTo,
34
+ attachments: message.attachments?.map((a) => ({
35
+ filename: a.filename,
36
+ content: typeof a.content === "string" ? a.content : Buffer.from(a.content),
37
+ contentType: a.contentType
38
+ }))
39
+ });
40
+ return { messageId: info.messageId, success: true };
41
+ }
42
+ async sendTemplate(_templateName, _to, _data) {
43
+ throw new Error("sendTemplate not supported by smtp");
44
+ }
45
+ };
46
+
47
+ export {
48
+ SmtpEmailAdapter
49
+ };
50
+ //# sourceMappingURL=chunk-4Y2FQY2L.js.map
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,61 @@
1
+ // src/providers/ses.ts
2
+ var SesEmailAdapter = class {
3
+ region;
4
+ defaultFrom;
5
+ injectedClient;
6
+ injectedCommand;
7
+ constructor(config, deps = {}) {
8
+ this.region = config.region;
9
+ this.defaultFrom = config.from;
10
+ this.injectedClient = deps.client;
11
+ this.injectedCommand = deps.sendEmailCommand;
12
+ }
13
+ async resolveSdk() {
14
+ if (this.injectedClient && this.injectedCommand) {
15
+ return { client: this.injectedClient, command: this.injectedCommand };
16
+ }
17
+ const sdkModule = "@aws-sdk/client-sesv2";
18
+ const mod = await import(sdkModule);
19
+ const SESv2Client = mod.SESv2Client;
20
+ const SendEmailCommand = mod.SendEmailCommand;
21
+ const client = this.injectedClient ?? new SESv2Client({ region: this.region });
22
+ const command = this.injectedCommand ?? ((input) => new SendEmailCommand(input));
23
+ return { client, command };
24
+ }
25
+ async send(message) {
26
+ const from = message.from || this.defaultFrom;
27
+ if (!from) throw new Error("From address required");
28
+ const { client, command } = await this.resolveSdk();
29
+ const toAddresses = Array.isArray(message.to) ? message.to : [message.to];
30
+ const ccAddresses = message.cc ? Array.isArray(message.cc) ? message.cc : [message.cc] : void 0;
31
+ const bccAddresses = message.bcc ? Array.isArray(message.bcc) ? message.bcc : [message.bcc] : void 0;
32
+ const body = {};
33
+ if (message.text) body.Text = { Data: message.text, Charset: "UTF-8" };
34
+ if (message.html) body.Html = { Data: message.html, Charset: "UTF-8" };
35
+ const input = {
36
+ FromEmailAddress: from.name ? `${from.name} <${from.email}>` : from.email,
37
+ Destination: {
38
+ ToAddresses: toAddresses,
39
+ ...ccAddresses ? { CcAddresses: ccAddresses } : {},
40
+ ...bccAddresses ? { BccAddresses: bccAddresses } : {}
41
+ },
42
+ ...message.replyTo ? { ReplyToAddresses: [message.replyTo] } : {},
43
+ Content: {
44
+ Simple: {
45
+ Subject: { Data: message.subject, Charset: "UTF-8" },
46
+ Body: body
47
+ }
48
+ }
49
+ };
50
+ const result = await client.send(command(input));
51
+ return { messageId: result.MessageId ?? "", success: true };
52
+ }
53
+ async sendTemplate(_templateName, _to, _data) {
54
+ throw new Error("sendTemplate not supported by ses");
55
+ }
56
+ };
57
+
58
+ export {
59
+ SesEmailAdapter
60
+ };
61
+ //# sourceMappingURL=chunk-H6SSV4G4.js.map
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,78 @@
1
+ // src/providers/mailgun.ts
2
+ var ALLOWED_HEADERS = /* @__PURE__ */ new Set([
3
+ "reply-to",
4
+ "in-reply-to",
5
+ "references",
6
+ "list-unsubscribe",
7
+ "list-unsubscribe-post",
8
+ "list-id",
9
+ "x-mailgun-variables",
10
+ "x-mailgun-tag"
11
+ ]);
12
+ var stripCrlf = (value) => value.split(/[\r\n]/)[0] ?? "";
13
+ var MailgunEmailAdapter = class {
14
+ apiKey;
15
+ domain;
16
+ baseUrl;
17
+ defaultFrom;
18
+ constructor(config) {
19
+ if (!config.apiKey) throw new Error("Mailgun requires apiKey");
20
+ if (!config.domain) throw new Error("Mailgun requires domain");
21
+ this.apiKey = config.apiKey;
22
+ this.domain = config.domain;
23
+ this.baseUrl = config.baseUrl || "https://api.mailgun.net/v3";
24
+ this.defaultFrom = config.from;
25
+ }
26
+ async send(message) {
27
+ const form = new FormData();
28
+ const from = message.from || this.defaultFrom;
29
+ if (from) {
30
+ form.append("from", from.name ? `${from.name} <${from.email}>` : from.email);
31
+ }
32
+ const to = Array.isArray(message.to) ? message.to.join(",") : message.to;
33
+ form.append("to", to);
34
+ form.append("subject", message.subject);
35
+ if (message.text) form.append("text", message.text);
36
+ if (message.html) form.append("html", message.html);
37
+ if (message.cc) form.append("cc", Array.isArray(message.cc) ? message.cc.join(",") : message.cc);
38
+ if (message.bcc) form.append("bcc", Array.isArray(message.bcc) ? message.bcc.join(",") : message.bcc);
39
+ if (message.replyTo) form.append("h:Reply-To", message.replyTo);
40
+ if (message.headers) {
41
+ for (const [key, value] of Object.entries(message.headers)) {
42
+ if (!ALLOWED_HEADERS.has(key.toLowerCase())) {
43
+ throw new Error(`Header "${key}" is not allowed`);
44
+ }
45
+ form.append(`h:${key}`, stripCrlf(value));
46
+ }
47
+ }
48
+ if (message.attachments) {
49
+ for (const att of message.attachments) {
50
+ const content = typeof att.content === "string" ? new TextEncoder().encode(att.content) : att.content;
51
+ const bytes = new Uint8Array(content);
52
+ const blob = new Blob([bytes], { type: att.contentType || "application/octet-stream" });
53
+ form.append("attachment", blob, att.filename);
54
+ }
55
+ }
56
+ const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {
57
+ method: "POST",
58
+ headers: {
59
+ Authorization: "Basic " + btoa(`api:${this.apiKey}`)
60
+ },
61
+ body: form
62
+ });
63
+ if (!response.ok) {
64
+ const errorText = await response.text();
65
+ throw new Error(`Mailgun API error (${response.status}): ${errorText}`);
66
+ }
67
+ const result = await response.json();
68
+ return { messageId: result.id, success: true };
69
+ }
70
+ async sendTemplate(_templateName, _to, _data) {
71
+ throw new Error("sendTemplate not supported by mailgun");
72
+ }
73
+ };
74
+
75
+ export {
76
+ MailgunEmailAdapter
77
+ };
78
+ //# sourceMappingURL=chunk-P5HWUNVS.js.map
@@ -0,0 +1 @@
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":";AAOA,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,CAAC;AAMD,IAAM,YAAY,CAAC,UAA0B,MAAM,MAAM,QAAQ,EAAE,CAAC,KAAK;AAElE,IAAM,sBAAN,MAAkD;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAqB;AAC7B,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAC7D,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAE7D,SAAK,SAAS,OAAO;AACrB,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO,WAAW;AACjC,SAAK,cAAc,OAAO;AAAA,EAC9B;AAAA,EAEA,MAAM,KAAK,SAAyE;AAChF,UAAM,OAAO,IAAI,SAAS;AAE1B,UAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,MAAM;AACN,WAAK,OAAO,QAAQ,KAAK,OAAO,GAAG,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,KAAK,KAAK;AAAA,IAC/E;AAEA,UAAM,KAAK,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,GAAG,KAAK,GAAG,IAAI,QAAQ;AACtE,SAAK,OAAO,MAAM,EAAE;AACpB,SAAK,OAAO,WAAW,QAAQ,OAAO;AAEtC,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ,QAAQ,IAAI;AAClD,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ,QAAQ,IAAI;AAClD,QAAI,QAAQ,GAAI,MAAK,OAAO,MAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,GAAG,KAAK,GAAG,IAAI,QAAQ,EAAE;AAC/F,QAAI,QAAQ,IAAK,MAAK,OAAO,OAAO,MAAM,QAAQ,QAAQ,GAAG,IAAI,QAAQ,IAAI,KAAK,GAAG,IAAI,QAAQ,GAAG;AACpG,QAAI,QAAQ,QAAS,MAAK,OAAO,cAAc,QAAQ,OAAO;AAE9D,QAAI,QAAQ,SAAS;AACjB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AACxD,YAAI,CAAC,gBAAgB,IAAI,IAAI,YAAY,CAAC,GAAG;AACzC,gBAAM,IAAI,MAAM,WAAW,GAAG,kBAAkB;AAAA,QACpD;AACA,aAAK,OAAO,KAAK,GAAG,IAAI,UAAU,KAAK,CAAC;AAAA,MAC5C;AAAA,IACJ;AAEA,QAAI,QAAQ,aAAa;AACrB,iBAAW,OAAO,QAAQ,aAAa;AACnC,cAAM,UAAU,OAAO,IAAI,YAAY,WACjC,IAAI,YAAY,EAAE,OAAO,IAAI,OAAO,IACpC,IAAI;AACV,cAAM,QAAQ,IAAI,WAAW,OAAO;AACpC,cAAM,OAAO,IAAI,KAAK,CAAC,KAAK,GAAG,EAAE,MAAM,IAAI,eAAe,2BAA2B,CAAC;AACtF,aAAK,OAAO,cAAc,MAAM,IAAI,QAAQ;AAAA,MAChD;AAAA,IACJ;AAEA,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,IAAI,KAAK,MAAM,aAAa;AAAA,MACpE,QAAQ;AAAA,MACR,SAAS;AAAA,QACL,eAAe,WAAW,KAAK,OAAO,KAAK,MAAM,EAAE;AAAA,MACvD;AAAA,MACA,MAAM;AAAA,IACV,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AACd,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,YAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,MAAM,SAAS,EAAE;AAAA,IAC1E;AAEA,UAAM,SAAS,MAAM,SAAS,KAAK;AACnC,WAAO,EAAE,WAAW,OAAO,IAAI,SAAS,KAAK;AAAA,EACjD;AAAA,EAEA,MAAM,aAAa,eAAuB,KAAwB,OAAuE;AAGrI,UAAM,IAAI,MAAM,uCAAuC;AAAA,EAC3D;AACJ;","names":[]}
@@ -0,0 +1,164 @@
1
+ interface EmailConfig {
2
+ provider: "smtp" | "sendgrid" | "mock" | "mailgun" | "ses";
3
+ smtp?: {
4
+ host: string;
5
+ port: number;
6
+ username: string;
7
+ password: string;
8
+ secure?: boolean;
9
+ };
10
+ apiKey?: string;
11
+ apiSecret?: string;
12
+ region?: string;
13
+ domain?: string;
14
+ baseUrl?: string;
15
+ from?: {
16
+ name?: string;
17
+ email: string;
18
+ };
19
+ templateDir?: string;
20
+ }
21
+ interface EmailMessage {
22
+ to: string | string[];
23
+ from?: {
24
+ name?: string;
25
+ email: string;
26
+ };
27
+ subject: string;
28
+ text?: string;
29
+ html?: string;
30
+ cc?: string | string[];
31
+ bcc?: string | string[];
32
+ replyTo?: string;
33
+ attachments?: Array<{
34
+ filename: string;
35
+ content: Uint8Array | string;
36
+ contentType?: string;
37
+ }>;
38
+ headers?: Record<string, string>;
39
+ }
40
+ interface TemplateData {
41
+ [key: string]: unknown;
42
+ }
43
+ interface EmailAdapter {
44
+ send(message: EmailMessage): Promise<{
45
+ messageId: string;
46
+ success: boolean;
47
+ }>;
48
+ sendTemplate(templateName: string, to: string | string[], data: TemplateData): Promise<{
49
+ messageId: string;
50
+ success: boolean;
51
+ }>;
52
+ }
53
+
54
+ /**
55
+ * In-memory email adapter used for tests and local development.
56
+ * It performs no network I/O and stays silent (no stdout) so it is
57
+ * safe to use inside library code and background workers.
58
+ */
59
+ declare class MockEmailAdapter implements EmailAdapter {
60
+ send(_message: EmailMessage): Promise<{
61
+ messageId: string;
62
+ success: boolean;
63
+ }>;
64
+ sendTemplate(name: string, to: string | string[], data: TemplateData): Promise<{
65
+ messageId: string;
66
+ success: boolean;
67
+ }>;
68
+ }
69
+
70
+ /**
71
+ * Builds the email adapter for the given provider. Providers are loaded
72
+ * lazily via dynamic `import()` so an app only pays for (and only needs
73
+ * installed) the SDK of the provider it actually uses.
74
+ */
75
+ declare function createEmailAdapter(config: EmailConfig): Promise<EmailAdapter>;
76
+
77
+ declare class SmtpEmailAdapter implements EmailAdapter {
78
+ private config;
79
+ private transporter;
80
+ constructor(config: EmailConfig);
81
+ send(message: EmailMessage): Promise<{
82
+ messageId: any;
83
+ success: boolean;
84
+ }>;
85
+ sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{
86
+ messageId: string;
87
+ success: boolean;
88
+ }>;
89
+ }
90
+
91
+ declare class SendGridEmailAdapter implements EmailAdapter {
92
+ private config;
93
+ constructor(config: EmailConfig);
94
+ send(message: EmailMessage): Promise<{
95
+ messageId: string;
96
+ success: boolean;
97
+ }>;
98
+ sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{
99
+ messageId: string;
100
+ success: boolean;
101
+ }>;
102
+ }
103
+
104
+ declare class MailgunEmailAdapter implements EmailAdapter {
105
+ private apiKey;
106
+ private domain;
107
+ private baseUrl;
108
+ private defaultFrom?;
109
+ constructor(config: EmailConfig);
110
+ send(message: EmailMessage): Promise<{
111
+ messageId: string;
112
+ success: boolean;
113
+ }>;
114
+ sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{
115
+ messageId: string;
116
+ success: boolean;
117
+ }>;
118
+ }
119
+
120
+ /**
121
+ * Minimal structural shape of the value returned by
122
+ * `@aws-sdk/client-sesv2`'s `SendEmailCommand` constructor. We only need
123
+ * an opaque object to hand back to `client.send`, so typing it as an
124
+ * unknown-bearing record keeps `tsc` happy without the SDK installed.
125
+ */
126
+ interface SesCommand {
127
+ readonly input: unknown;
128
+ }
129
+ /**
130
+ * Structural type for the SESv2 client. Declared locally so this module
131
+ * type-checks even when `@aws-sdk/client-sesv2` is not installed; the real
132
+ * SDK client satisfies this shape at runtime.
133
+ */
134
+ interface SesClient {
135
+ send(command: SesCommand): Promise<{
136
+ MessageId?: string;
137
+ }>;
138
+ }
139
+ /** Factory that builds a `SendEmailCommand` from its input object. */
140
+ type SesCommandFactory = (input: unknown) => SesCommand;
141
+ interface SesAdapterDeps {
142
+ /** Pre-built client (used by tests to avoid importing the real SDK). */
143
+ client?: SesClient;
144
+ /** Pre-built command factory (used by tests to avoid importing the real SDK). */
145
+ sendEmailCommand?: SesCommandFactory;
146
+ }
147
+ declare class SesEmailAdapter implements EmailAdapter {
148
+ private region?;
149
+ private defaultFrom?;
150
+ private injectedClient?;
151
+ private injectedCommand?;
152
+ constructor(config: EmailConfig, deps?: SesAdapterDeps);
153
+ private resolveSdk;
154
+ send(message: EmailMessage): Promise<{
155
+ messageId: string;
156
+ success: boolean;
157
+ }>;
158
+ sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{
159
+ messageId: string;
160
+ success: boolean;
161
+ }>;
162
+ }
163
+
164
+ export { type EmailAdapter, type EmailConfig, type EmailMessage, MailgunEmailAdapter, MockEmailAdapter, SendGridEmailAdapter, type SesAdapterDeps, type SesClient, type SesCommand, type SesCommandFactory, SesEmailAdapter, SmtpEmailAdapter, type TemplateData, createEmailAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,57 @@
1
+ import {
2
+ SmtpEmailAdapter
3
+ } from "./chunk-4Y2FQY2L.js";
4
+ import {
5
+ SendGridEmailAdapter
6
+ } from "./chunk-4256VN6L.js";
7
+ import {
8
+ MailgunEmailAdapter
9
+ } from "./chunk-P5HWUNVS.js";
10
+ import {
11
+ SesEmailAdapter
12
+ } from "./chunk-H6SSV4G4.js";
13
+
14
+ // src/mock.ts
15
+ var MockEmailAdapter = class {
16
+ async send(_message) {
17
+ return { messageId: `mock-${Date.now()}`, success: true };
18
+ }
19
+ async sendTemplate(name, to, data) {
20
+ return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });
21
+ }
22
+ };
23
+
24
+ // src/factory.ts
25
+ async function createEmailAdapter(config) {
26
+ switch (config.provider) {
27
+ case "mock":
28
+ return new MockEmailAdapter();
29
+ case "smtp": {
30
+ const { SmtpEmailAdapter: SmtpEmailAdapter2 } = await import("./smtp-GZ4DZMCQ.js");
31
+ return new SmtpEmailAdapter2(config);
32
+ }
33
+ case "sendgrid": {
34
+ const { SendGridEmailAdapter: SendGridEmailAdapter2 } = await import("./sendgrid-EYSREU2M.js");
35
+ return new SendGridEmailAdapter2(config);
36
+ }
37
+ case "mailgun": {
38
+ const { MailgunEmailAdapter: MailgunEmailAdapter2 } = await import("./mailgun-CKVSFBJE.js");
39
+ return new MailgunEmailAdapter2(config);
40
+ }
41
+ case "ses": {
42
+ const { SesEmailAdapter: SesEmailAdapter2 } = await import("./ses-KGJG5IB5.js");
43
+ return new SesEmailAdapter2(config);
44
+ }
45
+ default:
46
+ throw new Error(`Provider ${config.provider} not implemented`);
47
+ }
48
+ }
49
+ export {
50
+ MailgunEmailAdapter,
51
+ MockEmailAdapter,
52
+ SendGridEmailAdapter,
53
+ SesEmailAdapter,
54
+ SmtpEmailAdapter,
55
+ createEmailAdapter
56
+ };
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mock.ts","../src/factory.ts"],"sourcesContent":["import type { EmailAdapter, EmailMessage, TemplateData } from \"./types\";\n\n/**\n * In-memory email adapter used for tests and local development.\n * It performs no network I/O and stays silent (no stdout) so it is\n * safe to use inside library code and background workers.\n */\nexport class MockEmailAdapter implements EmailAdapter {\n async send(_message: EmailMessage) {\n return { messageId: `mock-${Date.now()}`, success: true };\n }\n\n async sendTemplate(name: string, to: string | string[], data: TemplateData) {\n return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });\n }\n}\n","import type { EmailAdapter, EmailConfig } from \"./types\";\nimport { MockEmailAdapter } from \"./mock\";\n\n/**\n * Builds the email adapter for the given provider. Providers are loaded\n * lazily via dynamic `import()` so an app only pays for (and only needs\n * installed) the SDK of the provider it actually uses.\n */\nexport async function createEmailAdapter(config: EmailConfig): Promise<EmailAdapter> {\n switch (config.provider) {\n case \"mock\":\n return new MockEmailAdapter();\n case \"smtp\": {\n const { SmtpEmailAdapter } = await import(\"./providers/smtp\");\n return new SmtpEmailAdapter(config);\n }\n case \"sendgrid\": {\n const { SendGridEmailAdapter } = await import(\"./providers/sendgrid\");\n return new SendGridEmailAdapter(config);\n }\n case \"mailgun\": {\n const { MailgunEmailAdapter } = await import(\"./providers/mailgun\");\n return new MailgunEmailAdapter(config);\n }\n case \"ses\": {\n const { SesEmailAdapter } = await import(\"./providers/ses\");\n return new SesEmailAdapter(config);\n }\n default:\n throw new Error(`Provider ${config.provider} not implemented`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAOO,IAAM,mBAAN,MAA+C;AAAA,EAClD,MAAM,KAAK,UAAwB;AAC/B,WAAO,EAAE,WAAW,QAAQ,KAAK,IAAI,CAAC,IAAI,SAAS,KAAK;AAAA,EAC5D;AAAA,EAEA,MAAM,aAAa,MAAc,IAAuB,MAAoB;AACxE,WAAO,KAAK,KAAK,EAAE,IAAI,SAAS,aAAa,IAAI,IAAI,MAAM,YAAY,IAAI,eAAe,KAAK,UAAU,IAAI,CAAC,GAAG,CAAC;AAAA,EACtH;AACJ;;;ACPA,eAAsB,mBAAmB,QAA4C;AACjF,UAAQ,OAAO,UAAU;AAAA,IACrB,KAAK;AACD,aAAO,IAAI,iBAAiB;AAAA,IAChC,KAAK,QAAQ;AACT,YAAM,EAAE,kBAAAA,kBAAiB,IAAI,MAAM,OAAO,oBAAkB;AAC5D,aAAO,IAAIA,kBAAiB,MAAM;AAAA,IACtC;AAAA,IACA,KAAK,YAAY;AACb,YAAM,EAAE,sBAAAC,sBAAqB,IAAI,MAAM,OAAO,wBAAsB;AACpE,aAAO,IAAIA,sBAAqB,MAAM;AAAA,IAC1C;AAAA,IACA,KAAK,WAAW;AACZ,YAAM,EAAE,qBAAAC,qBAAoB,IAAI,MAAM,OAAO,uBAAqB;AAClE,aAAO,IAAIA,qBAAoB,MAAM;AAAA,IACzC;AAAA,IACA,KAAK,OAAO;AACR,YAAM,EAAE,iBAAAC,iBAAgB,IAAI,MAAM,OAAO,mBAAiB;AAC1D,aAAO,IAAIA,iBAAgB,MAAM;AAAA,IACrC;AAAA,IACA;AACI,YAAM,IAAI,MAAM,YAAY,OAAO,QAAQ,kBAAkB;AAAA,EACrE;AACJ;","names":["SmtpEmailAdapter","SendGridEmailAdapter","MailgunEmailAdapter","SesEmailAdapter"]}
@@ -0,0 +1,7 @@
1
+ import {
2
+ MailgunEmailAdapter
3
+ } from "./chunk-P5HWUNVS.js";
4
+ export {
5
+ MailgunEmailAdapter
6
+ };
7
+ //# sourceMappingURL=mailgun-CKVSFBJE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,7 @@
1
+ import {
2
+ SendGridEmailAdapter
3
+ } from "./chunk-4256VN6L.js";
4
+ export {
5
+ SendGridEmailAdapter
6
+ };
7
+ //# sourceMappingURL=sendgrid-EYSREU2M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,7 @@
1
+ import {
2
+ SesEmailAdapter
3
+ } from "./chunk-H6SSV4G4.js";
4
+ export {
5
+ SesEmailAdapter
6
+ };
7
+ //# sourceMappingURL=ses-KGJG5IB5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,7 @@
1
+ import {
2
+ SmtpEmailAdapter
3
+ } from "./chunk-4Y2FQY2L.js";
4
+ export {
5
+ SmtpEmailAdapter
6
+ };
7
+ //# sourceMappingURL=smtp-GZ4DZMCQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@iskra-bun/mailer-kit",
3
+ "version": "0.1.0",
4
+ "description": "Envio de correo de Iskra, agnostico del transporte, con adaptadores de SMTP, SendGrid, Mailgun y SES.",
5
+ "keywords": [
6
+ "iskra",
7
+ "bun",
8
+ "email",
9
+ "smtp",
10
+ "sendgrid",
11
+ "mailgun",
12
+ "ses"
13
+ ],
14
+ "author": "Joan Lascano",
15
+ "license": "AGPL-3.0-or-later",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/fearful/iskra.git",
19
+ "directory": "packages/mailer-kit"
20
+ },
21
+ "homepage": "https://github.com/fearful/iskra/tree/main/packages/mailer-kit#readme",
22
+ "bugs": "https://github.com/fearful/iskra/issues",
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "source": "./src/index.ts",
30
+ "bun": "./src/index.ts",
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "README.md",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "test": "bun test",
47
+ "build": "tsup --config ../../tsup.config.ts"
48
+ },
49
+ "dependencies": {
50
+ "@iskra-bun/core": "0.1.1",
51
+ "@aws-sdk/client-sesv2": "^3.600.0",
52
+ "@sendgrid/mail": "^8.1.0",
53
+ "nodemailer": "^6.9.1"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "^1.3.5",
57
+ "@types/node": "^22.10.2",
58
+ "@types/nodemailer": "^7.0.4"
59
+ }
60
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { EmailAdapter, EmailConfig } from "./types";
2
+ import { MockEmailAdapter } from "./mock";
3
+
4
+ /**
5
+ * Builds the email adapter for the given provider. Providers are loaded
6
+ * lazily via dynamic `import()` so an app only pays for (and only needs
7
+ * installed) the SDK of the provider it actually uses.
8
+ */
9
+ export async function createEmailAdapter(config: EmailConfig): Promise<EmailAdapter> {
10
+ switch (config.provider) {
11
+ case "mock":
12
+ return new MockEmailAdapter();
13
+ case "smtp": {
14
+ const { SmtpEmailAdapter } = await import("./providers/smtp");
15
+ return new SmtpEmailAdapter(config);
16
+ }
17
+ case "sendgrid": {
18
+ const { SendGridEmailAdapter } = await import("./providers/sendgrid");
19
+ return new SendGridEmailAdapter(config);
20
+ }
21
+ case "mailgun": {
22
+ const { MailgunEmailAdapter } = await import("./providers/mailgun");
23
+ return new MailgunEmailAdapter(config);
24
+ }
25
+ case "ses": {
26
+ const { SesEmailAdapter } = await import("./providers/ses");
27
+ return new SesEmailAdapter(config);
28
+ }
29
+ default:
30
+ throw new Error(`Provider ${config.provider} not implemented`);
31
+ }
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type { EmailConfig, EmailMessage, TemplateData, EmailAdapter } from "./types";
2
+ export { MockEmailAdapter } from "./mock";
3
+ export { createEmailAdapter } from "./factory";
4
+ export { SmtpEmailAdapter } from "./providers/smtp";
5
+ export { SendGridEmailAdapter } from "./providers/sendgrid";
6
+ export { MailgunEmailAdapter } from "./providers/mailgun";
7
+ export { SesEmailAdapter } from "./providers/ses";
8
+ export type { SesClient, SesCommand, SesCommandFactory, SesAdapterDeps } from "./providers/ses";
package/src/mock.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { EmailAdapter, EmailMessage, TemplateData } from "./types";
2
+
3
+ /**
4
+ * In-memory email adapter used for tests and local development.
5
+ * It performs no network I/O and stays silent (no stdout) so it is
6
+ * safe to use inside library code and background workers.
7
+ */
8
+ export class MockEmailAdapter implements EmailAdapter {
9
+ async send(_message: EmailMessage) {
10
+ return { messageId: `mock-${Date.now()}`, success: true };
11
+ }
12
+
13
+ async sendTemplate(name: string, to: string | string[], data: TemplateData) {
14
+ return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });
15
+ }
16
+ }
@@ -0,0 +1,101 @@
1
+ import type { EmailAdapter, EmailMessage, EmailConfig, TemplateData } from "../types";
2
+
3
+ /**
4
+ * Outbound custom mail headers callers are permitted to set. Anything outside
5
+ * this set is rejected so a caller cannot spoof Reply-To / Sender / routing
6
+ * headers via the generic `headers` map.
7
+ */
8
+ const ALLOWED_HEADERS = new Set([
9
+ "reply-to",
10
+ "in-reply-to",
11
+ "references",
12
+ "list-unsubscribe",
13
+ "list-unsubscribe-post",
14
+ "list-id",
15
+ "x-mailgun-variables",
16
+ "x-mailgun-tag",
17
+ ]);
18
+
19
+ /**
20
+ * Truncate a header value at the first CR/LF. Anything after a line break is an
21
+ * injected header (or folded continuation) and must be dropped, not preserved.
22
+ */
23
+ const stripCrlf = (value: string): string => value.split(/[\r\n]/)[0] ?? "";
24
+
25
+ export class MailgunEmailAdapter implements EmailAdapter {
26
+ private apiKey: string;
27
+ private domain: string;
28
+ private baseUrl: string;
29
+ private defaultFrom?: { name?: string; email: string };
30
+
31
+ constructor(config: EmailConfig) {
32
+ if (!config.apiKey) throw new Error("Mailgun requires apiKey");
33
+ if (!config.domain) throw new Error("Mailgun requires domain");
34
+
35
+ this.apiKey = config.apiKey;
36
+ this.domain = config.domain;
37
+ this.baseUrl = config.baseUrl || "https://api.mailgun.net/v3";
38
+ this.defaultFrom = config.from;
39
+ }
40
+
41
+ async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {
42
+ const form = new FormData();
43
+
44
+ const from = message.from || this.defaultFrom;
45
+ if (from) {
46
+ form.append("from", from.name ? `${from.name} <${from.email}>` : from.email);
47
+ }
48
+
49
+ const to = Array.isArray(message.to) ? message.to.join(",") : message.to;
50
+ form.append("to", to);
51
+ form.append("subject", message.subject);
52
+
53
+ if (message.text) form.append("text", message.text);
54
+ if (message.html) form.append("html", message.html);
55
+ if (message.cc) form.append("cc", Array.isArray(message.cc) ? message.cc.join(",") : message.cc);
56
+ if (message.bcc) form.append("bcc", Array.isArray(message.bcc) ? message.bcc.join(",") : message.bcc);
57
+ if (message.replyTo) form.append("h:Reply-To", message.replyTo);
58
+
59
+ if (message.headers) {
60
+ for (const [key, value] of Object.entries(message.headers)) {
61
+ if (!ALLOWED_HEADERS.has(key.toLowerCase())) {
62
+ throw new Error(`Header "${key}" is not allowed`);
63
+ }
64
+ form.append(`h:${key}`, stripCrlf(value));
65
+ }
66
+ }
67
+
68
+ if (message.attachments) {
69
+ for (const att of message.attachments) {
70
+ const content = typeof att.content === "string"
71
+ ? new TextEncoder().encode(att.content)
72
+ : att.content;
73
+ const bytes = new Uint8Array(content);
74
+ const blob = new Blob([bytes], { type: att.contentType || "application/octet-stream" });
75
+ form.append("attachment", blob, att.filename);
76
+ }
77
+ }
78
+
79
+ const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {
80
+ method: "POST",
81
+ headers: {
82
+ Authorization: "Basic " + btoa(`api:${this.apiKey}`),
83
+ },
84
+ body: form,
85
+ });
86
+
87
+ if (!response.ok) {
88
+ const errorText = await response.text();
89
+ throw new Error(`Mailgun API error (${response.status}): ${errorText}`);
90
+ }
91
+
92
+ const result = await response.json() as { id: string; message: string };
93
+ return { messageId: result.id, success: true };
94
+ }
95
+
96
+ async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
97
+ // No template engine is implemented yet; fail loudly rather than
98
+ // silently sending a placeholder that looks like a real send.
99
+ throw new Error("sendTemplate not supported by mailgun");
100
+ }
101
+ }
@@ -0,0 +1,40 @@
1
+ import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../types";
2
+ import sgMail from "@sendgrid/mail";
3
+
4
+ export class SendGridEmailAdapter implements EmailAdapter {
5
+ constructor(private config: EmailConfig) {
6
+ if (!config.apiKey) throw new Error("SendGrid API Key required");
7
+ sgMail.setApiKey(config.apiKey);
8
+ }
9
+
10
+ async send(message: EmailMessage) {
11
+ const from = message.from || this.config.from;
12
+ if (!from) throw new Error("From address required");
13
+
14
+ const msg = {
15
+ to: message.to,
16
+ from: from.name ? { email: from.email, name: from.name } : from.email,
17
+ subject: message.subject,
18
+ text: message.text,
19
+ html: message.html,
20
+ cc: message.cc as any,
21
+ bcc: message.bcc as any,
22
+ replyTo: message.replyTo,
23
+ attachments: message.attachments?.map(a => ({
24
+ filename: a.filename,
25
+ content: typeof a.content === 'string' ? a.content : Buffer.from(a.content).toString("base64"),
26
+ type: a.contentType,
27
+ disposition: "attachment"
28
+ }))
29
+ } as any;
30
+
31
+ const [response] = await sgMail.send(msg);
32
+ return { messageId: response.headers["x-message-id"] as string, success: true };
33
+ }
34
+
35
+ async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
36
+ // No template engine is implemented yet; fail loudly rather than
37
+ // silently sending a placeholder body that looks like a real send.
38
+ throw new Error("sendTemplate not supported by sendgrid");
39
+ }
40
+ }
@@ -0,0 +1,106 @@
1
+ import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../types";
2
+
3
+ /**
4
+ * Minimal structural shape of the value returned by
5
+ * `@aws-sdk/client-sesv2`'s `SendEmailCommand` constructor. We only need
6
+ * an opaque object to hand back to `client.send`, so typing it as an
7
+ * unknown-bearing record keeps `tsc` happy without the SDK installed.
8
+ */
9
+ export interface SesCommand {
10
+ readonly input: unknown;
11
+ }
12
+
13
+ /**
14
+ * Structural type for the SESv2 client. Declared locally so this module
15
+ * type-checks even when `@aws-sdk/client-sesv2` is not installed; the real
16
+ * SDK client satisfies this shape at runtime.
17
+ */
18
+ export interface SesClient {
19
+ send(command: SesCommand): Promise<{ MessageId?: string }>;
20
+ }
21
+
22
+ /** Factory that builds a `SendEmailCommand` from its input object. */
23
+ export type SesCommandFactory = (input: unknown) => SesCommand;
24
+
25
+ export interface SesAdapterDeps {
26
+ /** Pre-built client (used by tests to avoid importing the real SDK). */
27
+ client?: SesClient;
28
+ /** Pre-built command factory (used by tests to avoid importing the real SDK). */
29
+ sendEmailCommand?: SesCommandFactory;
30
+ }
31
+
32
+ export class SesEmailAdapter implements EmailAdapter {
33
+ private region?: string;
34
+ private defaultFrom?: { name?: string; email: string };
35
+ private injectedClient?: SesClient;
36
+ private injectedCommand?: SesCommandFactory;
37
+
38
+ constructor(config: EmailConfig, deps: SesAdapterDeps = {}) {
39
+ // The effective `from` address is validated at send time (it can come
40
+ // from the message or the config), so construction stays permissive.
41
+ this.region = config.region;
42
+ this.defaultFrom = config.from;
43
+ this.injectedClient = deps.client;
44
+ this.injectedCommand = deps.sendEmailCommand;
45
+ }
46
+
47
+ private async resolveSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {
48
+ if (this.injectedClient && this.injectedCommand) {
49
+ return { client: this.injectedClient, command: this.injectedCommand };
50
+ }
51
+
52
+ // Lazy load only when actually sending so `tsc`/`bun test` stay green
53
+ // without `@aws-sdk/client-sesv2` installed. The specifier is held in a
54
+ // variable so TypeScript does not try to resolve the module at compile
55
+ // 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);
58
+ const SESv2Client = mod.SESv2Client as new (cfg: { region?: string }) => SesClient;
59
+ const SendEmailCommand = mod.SendEmailCommand as new (input: unknown) => SesCommand;
60
+
61
+ const client = this.injectedClient ?? new SESv2Client({ region: this.region });
62
+ const command: SesCommandFactory = this.injectedCommand ?? ((input) => new SendEmailCommand(input));
63
+
64
+ return { client, command };
65
+ }
66
+
67
+ async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {
68
+ const from = message.from || this.defaultFrom;
69
+ if (!from) throw new Error("From address required");
70
+
71
+ const { client, command } = await this.resolveSdk();
72
+
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;
76
+
77
+ 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" };
80
+
81
+ const input = {
82
+ FromEmailAddress: from.name ? `${from.name} <${from.email}>` : from.email,
83
+ Destination: {
84
+ ToAddresses: toAddresses,
85
+ ...(ccAddresses ? { CcAddresses: ccAddresses } : {}),
86
+ ...(bccAddresses ? { BccAddresses: bccAddresses } : {}),
87
+ },
88
+ ...(message.replyTo ? { ReplyToAddresses: [message.replyTo] } : {}),
89
+ Content: {
90
+ Simple: {
91
+ Subject: { Data: message.subject, Charset: "UTF-8" },
92
+ Body: body,
93
+ },
94
+ },
95
+ };
96
+
97
+ const result = await client.send(command(input));
98
+ return { messageId: result.MessageId ?? "", success: true };
99
+ }
100
+
101
+ async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
102
+ // No template engine is implemented yet; fail loudly rather than
103
+ // silently sending a placeholder body that looks like a real send.
104
+ throw new Error("sendTemplate not supported by ses");
105
+ }
106
+ }
@@ -0,0 +1,53 @@
1
+ import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../types";
2
+ import * as nodemailer from "nodemailer";
3
+
4
+ export class SmtpEmailAdapter implements EmailAdapter {
5
+ private transporter: nodemailer.Transporter;
6
+
7
+ constructor(private config: EmailConfig) {
8
+ if (!config.smtp) throw new Error("SMTP config required");
9
+ // Implicit TLS on port 465; STARTTLS enforced (requireTLS) elsewhere so
10
+ // credentials never transit in cleartext. An explicit `secure` wins.
11
+ const secure = config.smtp.secure ?? config.smtp.port === 465;
12
+ this.transporter = nodemailer.createTransport({
13
+ host: config.smtp.host,
14
+ port: config.smtp.port,
15
+ secure,
16
+ requireTLS: secure ? undefined : true,
17
+ tls: { rejectUnauthorized: true },
18
+ auth: {
19
+ user: config.smtp.username,
20
+ pass: config.smtp.password
21
+ }
22
+ });
23
+ }
24
+
25
+ async send(message: EmailMessage) {
26
+ const from = message.from || this.config.from;
27
+ if (!from) throw new Error("From address required");
28
+
29
+ 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,
32
+ subject: message.subject,
33
+ text: message.text,
34
+ 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 => ({
39
+ filename: a.filename,
40
+ content: typeof a.content === 'string' ? a.content : Buffer.from(a.content),
41
+ contentType: a.contentType
42
+ }))
43
+ });
44
+
45
+ return { messageId: (info as any).messageId, success: true };
46
+ }
47
+
48
+ async sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{ messageId: string; success: boolean }> {
49
+ // No template engine is implemented yet; fail loudly rather than
50
+ // silently sending a placeholder body that looks like a real send.
51
+ throw new Error("sendTemplate not supported by smtp");
52
+ }
53
+ }
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface EmailConfig {
2
+ provider: "smtp" | "sendgrid" | "mock" | "mailgun" | "ses";
3
+ smtp?: {
4
+ host: string;
5
+ port: number;
6
+ username: string;
7
+ password: string;
8
+ secure?: boolean;
9
+ };
10
+ apiKey?: string;
11
+ apiSecret?: string;
12
+ region?: string;
13
+ domain?: string;
14
+ baseUrl?: string;
15
+ from?: { name?: string; email: string };
16
+ templateDir?: string;
17
+ }
18
+
19
+ export interface EmailMessage {
20
+ to: string | string[];
21
+ from?: { name?: string; email: string };
22
+ subject: string;
23
+ text?: string;
24
+ html?: string;
25
+ cc?: string | string[];
26
+ bcc?: string | string[];
27
+ replyTo?: string;
28
+ attachments?: Array<{ filename: string; content: Uint8Array | string; contentType?: string }>;
29
+ headers?: Record<string, string>;
30
+ }
31
+
32
+ export interface TemplateData {
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ export interface EmailAdapter {
37
+ send(message: EmailMessage): Promise<{ messageId: string; success: boolean }>;
38
+ sendTemplate(templateName: string, to: string | string[], data: TemplateData): Promise<{ messageId: string; success: boolean }>;
39
+ }