@xacos/mail 1.0.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/MailManager.d.ts +10 -0
  4. package/dist/MailManager.d.ts.map +1 -0
  5. package/dist/MailManager.js +69 -0
  6. package/dist/MailManager.js.map +1 -0
  7. package/dist/Mailer.d.ts +19 -0
  8. package/dist/Mailer.d.ts.map +1 -0
  9. package/dist/Mailer.js +3 -0
  10. package/dist/Mailer.js.map +1 -0
  11. package/dist/drivers/LogMailer.d.ts +7 -0
  12. package/dist/drivers/LogMailer.d.ts.map +1 -0
  13. package/dist/drivers/LogMailer.js +28 -0
  14. package/dist/drivers/LogMailer.js.map +1 -0
  15. package/dist/drivers/MailgunMailer.d.ts +14 -0
  16. package/dist/drivers/MailgunMailer.d.ts.map +1 -0
  17. package/dist/drivers/MailgunMailer.js +43 -0
  18. package/dist/drivers/MailgunMailer.js.map +1 -0
  19. package/dist/drivers/PostmarkMailer.d.ts +12 -0
  20. package/dist/drivers/PostmarkMailer.d.ts.map +1 -0
  21. package/dist/drivers/PostmarkMailer.js +41 -0
  22. package/dist/drivers/PostmarkMailer.js.map +1 -0
  23. package/dist/drivers/ResendMailer.d.ts +12 -0
  24. package/dist/drivers/ResendMailer.d.ts.map +1 -0
  25. package/dist/drivers/ResendMailer.js +44 -0
  26. package/dist/drivers/ResendMailer.js.map +1 -0
  27. package/dist/drivers/SesMailer.d.ts +14 -0
  28. package/dist/drivers/SesMailer.d.ts.map +1 -0
  29. package/dist/drivers/SesMailer.js +52 -0
  30. package/dist/drivers/SesMailer.js.map +1 -0
  31. package/dist/drivers/SmtpMailer.d.ts +15 -0
  32. package/dist/drivers/SmtpMailer.d.ts.map +1 -0
  33. package/dist/drivers/SmtpMailer.js +46 -0
  34. package/dist/drivers/SmtpMailer.js.map +1 -0
  35. package/dist/drivers/drivers.test.d.ts +2 -0
  36. package/dist/drivers/drivers.test.d.ts.map +1 -0
  37. package/dist/drivers/drivers.test.js +30 -0
  38. package/dist/drivers/drivers.test.js.map +1 -0
  39. package/dist/index.d.ts +10 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +10 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/index.test.d.ts +2 -0
  44. package/dist/index.test.d.ts.map +1 -0
  45. package/dist/index.test.js +18 -0
  46. package/dist/index.test.js.map +1 -0
  47. package/dist/utils/interpolate.d.ts +6 -0
  48. package/dist/utils/interpolate.d.ts.map +1 -0
  49. package/dist/utils/interpolate.js +10 -0
  50. package/dist/utils/interpolate.js.map +1 -0
  51. package/package.json +76 -0
  52. package/src/MailManager.ts +76 -0
  53. package/src/Mailer.ts +22 -0
  54. package/src/drivers/LogMailer.ts +29 -0
  55. package/src/drivers/MailgunMailer.ts +56 -0
  56. package/src/drivers/PostmarkMailer.ts +53 -0
  57. package/src/drivers/ResendMailer.ts +56 -0
  58. package/src/drivers/SesMailer.ts +64 -0
  59. package/src/drivers/SmtpMailer.ts +59 -0
  60. package/src/drivers/drivers.test.ts +34 -0
  61. package/src/index.test.ts +23 -0
  62. package/src/index.ts +11 -0
  63. package/src/utils/interpolate.ts +10 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it, spyOn } from "bun:test";
2
+ import { LogMailer } from "./index";
3
+ describe("LogMailer", () => {
4
+ it("renders templates with data", async () => {
5
+ const mailer = new LogMailer();
6
+ const logSpy = spyOn(console, "log");
7
+ await mailer.send({
8
+ to: "test@example.com",
9
+ subject: "Hello {{ name }}",
10
+ text: "Welcome to {{ app }}",
11
+ data: { name: "Alice", app: "XAOCS" }
12
+ });
13
+ expect(logSpy).toHaveBeenCalledWith("Subject:", "Hello Alice");
14
+ expect(logSpy).toHaveBeenCalledWith("Text: ", "Welcome to XAOCS");
15
+ logSpy.mockRestore();
16
+ });
17
+ });
18
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;QAC3C,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAErC,MAAM,MAAM,CAAC,IAAI,CAAC;YAChB,EAAE,EAAE,kBAAkB;YACtB,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EAAE,sBAAsB;YAC5B,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;SACtC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;QAGpE,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Simple string interpolation utility for XAOCS templates.
3
+ * Replaces {{ variable }} with values from the data object.
4
+ */
5
+ export declare function interpolate(template: string, data: Record<string, any>): string;
6
+ //# sourceMappingURL=interpolate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.d.ts","sourceRoot":"","sources":["../../src/utils/interpolate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAI/E"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Simple string interpolation utility for XAOCS templates.
3
+ * Replaces {{ variable }} with values from the data object.
4
+ */
5
+ export function interpolate(template, data) {
6
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
7
+ return data[key]?.toString() ?? match;
8
+ });
9
+ }
10
+ //# sourceMappingURL=interpolate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.js","sourceRoot":"","sources":["../../src/utils/interpolate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,IAAyB;IACrE,OAAO,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,IAAI,KAAK,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@xacos/mail",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "@aws-sdk/client-ses": "^3.1048.0",
15
+ "@xacos/config": "workspace:*",
16
+ "@xacos/shared": "workspace:*",
17
+ "form-data": "^4.0.5",
18
+ "mailgun.js": "^13.0.1",
19
+ "nodemailer": "^8.0.7",
20
+ "postmark": "^4.0.7",
21
+ "resend": "^6.12.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.15.3",
25
+ "@types/nodemailer": "^8.0.0",
26
+ "bun-types": "^1.3.12",
27
+ "typescript": "^5.7.3"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsc --watch",
32
+ "test": "bun test src",
33
+ "type-check": "tsc --noEmit"
34
+ },
35
+ "license": "MIT",
36
+ "author": "XAOCS Team",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/zoherr/xaocs.git",
40
+ "directory": "packages/mail"
41
+ },
42
+ "homepage": "https://xaocs.dev",
43
+ "bugs": {
44
+ "url": "https://github.com/zoherr/xaocs/issues"
45
+ },
46
+ "keywords": [
47
+ "xaocs",
48
+ "xacos",
49
+ "framework",
50
+ "typescript",
51
+ "fullstack",
52
+ "fastify",
53
+ "react",
54
+ "email",
55
+ "smtp",
56
+ "ses",
57
+ "mailgun",
58
+ "resend",
59
+ "postmark"
60
+ ],
61
+ "files": [
62
+ "dist",
63
+ "src",
64
+ "README.md",
65
+ "LICENSE"
66
+ ],
67
+ "publishConfig": {
68
+ "access": "public",
69
+ "registry": "https://registry.npmjs.org",
70
+ "provenance": false
71
+ },
72
+ "engines": {
73
+ "bun": ">=1.0.0",
74
+ "node": ">=18.0.0"
75
+ }
76
+ }
@@ -0,0 +1,76 @@
1
+ import type { XacosConfig } from '@xacos/config';
2
+ import type { Mailer, MailMessage, MailResult } from './Mailer';
3
+ import { LogMailer } from './drivers/LogMailer';
4
+ import { SmtpMailer } from './drivers/SmtpMailer';
5
+ import { SesMailer } from './drivers/SesMailer';
6
+ import { MailgunMailer } from './drivers/MailgunMailer';
7
+ import { PostmarkMailer } from './drivers/PostmarkMailer';
8
+ import { ResendMailer } from './drivers/ResendMailer';
9
+
10
+
11
+ export class MailManager {
12
+ private mailer: Mailer | null = null;
13
+
14
+ constructor(private config: XacosConfig['mail'] = {}) {}
15
+
16
+ getMailer(): Mailer {
17
+ if (this.mailer) return this.mailer;
18
+
19
+ const config = this.config ?? {};
20
+ const driver = config.driver ?? 'log';
21
+
22
+ switch (driver) {
23
+ case 'smtp':
24
+ this.mailer = new SmtpMailer({
25
+ host: config.host ?? 'localhost',
26
+ port: config.port ?? 587,
27
+ user: config.user ?? '',
28
+ pass: config.pass ?? '',
29
+ from: config.from ?? 'noreply@xacos.dev',
30
+ });
31
+ break;
32
+ case 'ses':
33
+ if (!config.ses) throw new Error('[XAOCS Mail] SES config missing');
34
+ this.mailer = new SesMailer({
35
+ region: config.ses.region ?? 'us-east-1',
36
+ key: config.ses.key ?? '',
37
+ secret: config.ses.secret ?? '',
38
+ from: config.from ?? 'noreply@xacos.dev',
39
+ });
40
+ break;
41
+ case 'mailgun':
42
+ this.mailer = new MailgunMailer({
43
+ apiKey: process.env['MAILGUN_API_KEY'] ?? '',
44
+ domain: process.env['MAILGUN_DOMAIN'] ?? '',
45
+ from: config.from ?? 'noreply@xacos.dev',
46
+ host: process.env['MAILGUN_HOST'] ?? undefined,
47
+ });
48
+ break;
49
+
50
+ case 'postmark':
51
+ this.mailer = new PostmarkMailer({
52
+ serverToken: process.env['POSTMARK_SERVER_TOKEN'] ?? '',
53
+ from: config.from ?? 'noreply@xacos.dev',
54
+ });
55
+ break;
56
+ case 'resend':
57
+ this.mailer = new ResendMailer({
58
+ apiKey: process.env['RESEND_API_KEY'] ?? '',
59
+ from: config.from ?? 'noreply@xacos.dev',
60
+ });
61
+ break;
62
+
63
+ case 'log':
64
+ default:
65
+ this.mailer = new LogMailer(config.from);
66
+ break;
67
+ }
68
+
69
+ return this.mailer;
70
+ }
71
+
72
+ async send(message: MailMessage): Promise<MailResult> {
73
+ return this.getMailer().send(message);
74
+ }
75
+ }
76
+
package/src/Mailer.ts ADDED
@@ -0,0 +1,22 @@
1
+ export interface MailMessage {
2
+ from?: string;
3
+ to: string | string[];
4
+ subject: string;
5
+ template?: string;
6
+ data?: Record<string, any>;
7
+ text?: string;
8
+ html?: string;
9
+ }
10
+
11
+ export interface MailResult {
12
+ success: boolean;
13
+ messageId?: string | undefined;
14
+ error?: string | undefined;
15
+ provider: string;
16
+ }
17
+
18
+
19
+ export abstract class Mailer {
20
+ abstract send(message: MailMessage): Promise<MailResult>;
21
+ }
22
+
@@ -0,0 +1,29 @@
1
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
2
+ import { interpolate } from '../utils/interpolate';
3
+
4
+ export class LogMailer implements Mailer {
5
+ constructor(private from: string = 'noreply@xacos.dev') {
6
+ }
7
+
8
+ async send(message: MailMessage): Promise<MailResult> {
9
+ const subject = interpolate(message.subject, message.data ?? {});
10
+ const text = message.text ? interpolate(message.text, message.data ?? {}) : undefined;
11
+ const html = message.template
12
+ ? interpolate(message.template, message.data ?? {})
13
+ : (message.html ? interpolate(message.html, message.data ?? {}) : undefined);
14
+
15
+ console.log('\n--- [MAIL LOG] ---');
16
+ console.log('From: ', message.from ?? this.from);
17
+ console.log('To: ', message.to);
18
+ console.log('Subject:', subject);
19
+ if (text) console.log('Text: ', text);
20
+ if (html) console.log('HTML: ', '[Rendered HTML Content]');
21
+ console.log('------------------\n');
22
+
23
+ return {
24
+ success: true,
25
+ provider: 'log',
26
+ };
27
+ }
28
+ }
29
+
@@ -0,0 +1,56 @@
1
+ import Mailgun from 'mailgun.js';
2
+ import FormData from 'form-data';
3
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
4
+ import { interpolate } from '../utils/interpolate';
5
+
6
+ export interface MailgunConfig {
7
+ apiKey: string;
8
+ domain: string;
9
+ from: string;
10
+ host?: string | undefined; // default: 'api.mailgun.net', use 'api.eu.mailgun.net' for EU
11
+ }
12
+
13
+ export class MailgunMailer implements Mailer {
14
+ private client: ReturnType<InstanceType<typeof Mailgun>['client']>;
15
+ private config: MailgunConfig;
16
+
17
+ constructor(config: MailgunConfig) {
18
+ this.config = config;
19
+ const mg = new Mailgun(FormData);
20
+ this.client = mg.client({
21
+ username: 'api',
22
+ key: config.apiKey,
23
+ url: config.host ?? 'https://api.mailgun.net',
24
+ });
25
+ }
26
+
27
+ async send(message: MailMessage): Promise<MailResult> {
28
+ const html = message.template
29
+ ? interpolate(message.template, message.data ?? {})
30
+ : message.html ?? '';
31
+
32
+ try {
33
+ const response = await this.client.messages.create(this.config.domain, {
34
+ from: message.from ?? this.config.from,
35
+ to: Array.isArray(message.to) ? message.to : [message.to],
36
+ subject: message.subject,
37
+ html,
38
+ text: message.text,
39
+ } as any);
40
+
41
+
42
+ return {
43
+ success: true,
44
+ messageId: response.id,
45
+ provider: 'mailgun',
46
+ };
47
+ } catch (error) {
48
+ return {
49
+ success: false,
50
+ error: (error as Error).message,
51
+ provider: 'mailgun',
52
+ };
53
+ }
54
+ }
55
+ }
56
+
@@ -0,0 +1,53 @@
1
+ import { ServerClient } from 'postmark';
2
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
3
+ import { interpolate } from '../utils/interpolate';
4
+
5
+ export interface PostmarkConfig {
6
+ serverToken: string;
7
+ from: string;
8
+ }
9
+
10
+ export class PostmarkMailer implements Mailer {
11
+ private client: ServerClient;
12
+ private config: PostmarkConfig;
13
+
14
+ constructor(config: PostmarkConfig) {
15
+ this.config = config;
16
+ this.client = new ServerClient(config.serverToken);
17
+ }
18
+
19
+ async send(message: MailMessage): Promise<MailResult> {
20
+ const htmlBody = message.template
21
+ ? interpolate(message.template, message.data ?? {})
22
+ : message.html ?? '';
23
+
24
+ try {
25
+ const messageData: any = {
26
+ From: message.from ?? this.config.from,
27
+ To: Array.isArray(message.to) ? message.to.join(',') : message.to,
28
+ Subject: message.subject,
29
+ MessageStream: 'outbound',
30
+ };
31
+
32
+ if (htmlBody) messageData.HtmlBody = htmlBody;
33
+ if (message.text) messageData.TextBody = message.text;
34
+
35
+ const response = await this.client.sendEmail(messageData);
36
+
37
+
38
+
39
+ return {
40
+ success: true,
41
+ messageId: response.MessageID,
42
+ provider: 'postmark',
43
+ };
44
+ } catch (error) {
45
+ return {
46
+ success: false,
47
+ error: (error as Error).message,
48
+ provider: 'postmark',
49
+ };
50
+ }
51
+ }
52
+ }
53
+
@@ -0,0 +1,56 @@
1
+ import { Resend } from 'resend';
2
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
3
+ import { interpolate } from '../utils/interpolate';
4
+
5
+ export interface ResendConfig {
6
+ apiKey: string;
7
+ from: string;
8
+ }
9
+
10
+ export class ResendMailer implements Mailer {
11
+ private client: Resend;
12
+ private config: ResendConfig;
13
+
14
+ constructor(config: ResendConfig) {
15
+ this.config = config;
16
+ this.client = new Resend(config.apiKey);
17
+ }
18
+
19
+ async send(message: MailMessage): Promise<MailResult> {
20
+ const html = message.template
21
+ ? interpolate(message.template, message.data ?? {})
22
+ : message.html ?? '';
23
+
24
+ try {
25
+ const response = await this.client.emails.send({
26
+ from: message.from ?? this.config.from,
27
+ to: Array.isArray(message.to) ? message.to : [message.to],
28
+ subject: message.subject,
29
+ html,
30
+ text: message.text,
31
+ } as any);
32
+
33
+
34
+ if (response.error) {
35
+ return {
36
+ success: false,
37
+ error: response.error.message,
38
+ provider: 'resend',
39
+ };
40
+ }
41
+
42
+ return {
43
+ success: true,
44
+ messageId: response.data?.id,
45
+ provider: 'resend',
46
+ };
47
+ } catch (error) {
48
+ return {
49
+ success: false,
50
+ error: (error as Error).message,
51
+ provider: 'resend',
52
+ };
53
+ }
54
+ }
55
+ }
56
+
@@ -0,0 +1,64 @@
1
+ import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
2
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
3
+ import { interpolate } from '../utils/interpolate';
4
+
5
+ export interface SesConfig {
6
+ region: string;
7
+ key: string;
8
+ secret: string;
9
+ from: string;
10
+ }
11
+
12
+ export class SesMailer implements Mailer {
13
+ private client: SESClient;
14
+ private defaultFrom: string;
15
+
16
+ constructor(config: SesConfig) {
17
+ this.defaultFrom = config.from;
18
+ this.client = new SESClient({
19
+ region: config.region,
20
+ credentials: {
21
+ accessKeyId: config.key,
22
+ secretAccessKey: config.secret,
23
+ },
24
+ });
25
+ }
26
+
27
+ async send(message: MailMessage): Promise<MailResult> {
28
+ const subject = interpolate(message.subject, message.data ?? {});
29
+ const text = message.text ? interpolate(message.text, message.data ?? {}) : undefined;
30
+ const html = message.template
31
+ ? interpolate(message.template, message.data ?? {})
32
+ : (message.html ? interpolate(message.html, message.data ?? {}) : undefined);
33
+
34
+ const command = new SendEmailCommand({
35
+ Source: message.from ?? this.defaultFrom,
36
+ Destination: {
37
+ ToAddresses: Array.isArray(message.to) ? message.to : [message.to],
38
+ },
39
+ Message: {
40
+ Subject: { Data: subject },
41
+ Body: {
42
+ Text: text ? { Data: text } : undefined,
43
+ Html: html ? { Data: html } : undefined,
44
+ },
45
+ },
46
+ });
47
+
48
+ try {
49
+ const response = await this.client.send(command);
50
+ return {
51
+ success: true,
52
+ messageId: response.MessageId,
53
+ provider: 'ses',
54
+ };
55
+ } catch (error) {
56
+ return {
57
+ success: false,
58
+ error: (error as Error).message,
59
+ provider: 'ses',
60
+ };
61
+ }
62
+ }
63
+ }
64
+
@@ -0,0 +1,59 @@
1
+ import nodemailer from 'nodemailer';
2
+ import type { Mailer, MailMessage, MailResult } from '../Mailer';
3
+ import { interpolate } from '../utils/interpolate';
4
+
5
+ export interface SmtpConfig {
6
+ host: string;
7
+ port: number;
8
+ user: string;
9
+ pass: string;
10
+ from: string;
11
+ }
12
+
13
+ export class SmtpMailer implements Mailer {
14
+ private transporter: nodemailer.Transporter;
15
+ private defaultFrom: string;
16
+
17
+ constructor(config: SmtpConfig) {
18
+ this.defaultFrom = config.from;
19
+ this.transporter = nodemailer.createTransport({
20
+ host: config.host,
21
+ port: config.port,
22
+ auth: {
23
+ user: config.user,
24
+ pass: config.pass,
25
+ },
26
+ });
27
+ }
28
+
29
+ async send(message: MailMessage): Promise<MailResult> {
30
+ const subject = interpolate(message.subject, message.data ?? {});
31
+ const text = message.text ? interpolate(message.text, message.data ?? {}) : undefined;
32
+ const html = message.template
33
+ ? interpolate(message.template, message.data ?? {})
34
+ : (message.html ? interpolate(message.html, message.data ?? {}) : undefined);
35
+
36
+ try {
37
+ const response = await this.transporter.sendMail({
38
+ from: message.from ?? this.defaultFrom,
39
+ to: Array.isArray(message.to) ? message.to.join(', ') : message.to,
40
+ subject,
41
+ text,
42
+ html,
43
+ });
44
+
45
+ return {
46
+ success: true,
47
+ messageId: response.messageId,
48
+ provider: 'smtp',
49
+ };
50
+ } catch (error) {
51
+ return {
52
+ success: false,
53
+ error: (error as Error).message,
54
+ provider: 'smtp',
55
+ };
56
+ }
57
+ }
58
+ }
59
+
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { MailgunMailer } from "./MailgunMailer";
3
+ import { PostmarkMailer } from "./PostmarkMailer";
4
+ import { ResendMailer } from "./ResendMailer";
5
+
6
+ describe("MailgunMailer", () => {
7
+ it("constructs without throwing", () => {
8
+ expect(() => new MailgunMailer({
9
+ apiKey: "key-test",
10
+ domain: "test.example.com",
11
+ from: "test@example.com",
12
+ })).not.toThrow();
13
+ });
14
+ });
15
+
16
+ describe("PostmarkMailer", () => {
17
+ it("constructs without throwing", () => {
18
+ expect(() => new PostmarkMailer({
19
+ serverToken: "token-test",
20
+ from: "test@example.com",
21
+ })).not.toThrow();
22
+ });
23
+ });
24
+
25
+ describe("ResendMailer", () => {
26
+ it("constructs without throwing", () => {
27
+ expect(() => new ResendMailer({
28
+ apiKey: "re_test",
29
+ from: "test@example.com",
30
+ })).not.toThrow();
31
+ });
32
+ });
33
+
34
+
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, spyOn } from "bun:test";
2
+ import { LogMailer } from "./index";
3
+
4
+ describe("LogMailer", () => {
5
+ it("renders templates with data", async () => {
6
+ const mailer = new LogMailer();
7
+ const logSpy = spyOn(console, "log");
8
+
9
+ await mailer.send({
10
+ to: "test@example.com",
11
+ subject: "Hello {{ name }}",
12
+ text: "Welcome to {{ app }}",
13
+ data: { name: "Alice", app: "XAOCS" }
14
+ });
15
+
16
+ expect(logSpy).toHaveBeenCalledWith("Subject:", "Hello Alice");
17
+ expect(logSpy).toHaveBeenCalledWith("Text: ", "Welcome to XAOCS");
18
+
19
+
20
+ logSpy.mockRestore();
21
+ });
22
+ });
23
+
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from './Mailer';
2
+ export * from './MailManager';
3
+ export * from './drivers/LogMailer';
4
+ export * from './drivers/SmtpMailer';
5
+ export * from './drivers/SesMailer';
6
+ export * from './drivers/MailgunMailer';
7
+ export * from './drivers/PostmarkMailer';
8
+ export * from './drivers/ResendMailer';
9
+
10
+ export * from './utils/interpolate';
11
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Simple string interpolation utility for XAOCS templates.
3
+ * Replaces {{ variable }} with values from the data object.
4
+ */
5
+ export function interpolate(template: string, data: Record<string, any>): string {
6
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
7
+ return data[key]?.toString() ?? match;
8
+ });
9
+ }
10
+