@beignet/provider-mail-smtp 0.0.1

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,5 @@
1
+ # @beignet/provider-mail-smtp
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @beignet/provider-mail-smtp
2
+
3
+ SMTP-backed mail provider for Beignet, implemented with Nodemailer.
4
+
5
+ The provider installs the app-facing `ctx.ports.mailer` port and exposes
6
+ `ctx.ports.smtp.transporter` only as an escape hatch for Nodemailer-specific
7
+ features.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @beignet/provider-mail-smtp nodemailer
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ ```typescript
18
+ import { mailSmtpProvider } from "@beignet/provider-mail-smtp";
19
+ import { createServer } from "@beignet/core/server";
20
+
21
+ const server = await createServer({
22
+ ports: basePorts,
23
+ providers: [mailSmtpProvider],
24
+ createContext: ({ ports }) => ({ ports }),
25
+ routes,
26
+ });
27
+ ```
28
+
29
+ Required environment variables:
30
+
31
+ | Variable | Description |
32
+ | --- | --- |
33
+ | `MAIL_HOST` | SMTP server hostname |
34
+ | `MAIL_PORT` | SMTP server port. Port `465` uses SSL. |
35
+ | `MAIL_USER` | SMTP username |
36
+ | `MAIL_PASS` | SMTP password |
37
+ | `MAIL_FROM` | Default sender address |
38
+
39
+ ## Use in application code
40
+
41
+ ```typescript
42
+ await ctx.ports.mailer.send({
43
+ to: "user@example.com",
44
+ subject: "Welcome",
45
+ text: "Thanks for joining.",
46
+ });
47
+ ```
48
+
49
+ The same `MailerPort` works with Resend, memory fakes, and other adapters:
50
+
51
+ ```typescript
52
+ await ctx.ports.mailer.send({
53
+ from: { email: "support@example.com", name: "Support" },
54
+ to: ["user@example.com", "admin@example.com"],
55
+ cc: "audit@example.com",
56
+ replyTo: "support@example.com",
57
+ subject: "Account updated",
58
+ text: "Your account was updated.",
59
+ html: "<p>Your account was updated.</p>",
60
+ });
61
+ ```
62
+
63
+ ## Escape hatch
64
+
65
+ Use the Nodemailer transporter only when you need an SMTP-specific feature not
66
+ covered by `MailerPort`:
67
+
68
+ ```typescript
69
+ await ctx.ports.smtp.transporter.sendMail({
70
+ from: "sender@example.com",
71
+ to: "user@example.com",
72
+ subject: "Invoice",
73
+ text: "Attached.",
74
+ attachments: [
75
+ {
76
+ filename: "invoice.pdf",
77
+ path: "/path/to/invoice.pdf",
78
+ },
79
+ ],
80
+ });
81
+ ```
82
+
83
+ ## Devtools
84
+
85
+ When `ctx.ports.devtools` is installed, this provider records `mail.send`,
86
+ `mail.sent`, and `mail.failed` events under the `mail` watcher.
87
+
88
+ ## Errors
89
+
90
+ Delivery failures throw `MailDeliveryError` from `@beignet/core/mail`.
91
+ Startup configuration and connection problems throw during provider setup.
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @beignet/provider-mail-smtp
3
+ *
4
+ * SMTP-backed mail provider for Beignet.
5
+ */
6
+ import { type MailAddress, type MailerPort } from "@beignet/core/mail";
7
+ import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
8
+ import type { Transporter } from "nodemailer";
9
+ import { z } from "zod";
10
+ declare const MailConfigSchema: z.ZodObject<{
11
+ HOST: z.ZodString;
12
+ PORT: z.ZodCoercedNumber<unknown>;
13
+ USER: z.ZodString;
14
+ PASS: z.ZodString;
15
+ FROM: z.ZodString;
16
+ }, z.core.$strip>;
17
+ export type MailConfig = z.infer<typeof MailConfigSchema>;
18
+ export interface SmtpMailEscapeHatch {
19
+ transporter: Transporter;
20
+ }
21
+ export interface SmtpMailProviderPorts {
22
+ mailer: MailerPort;
23
+ smtp: SmtpMailEscapeHatch;
24
+ }
25
+ export interface CreateSmtpMailerOptions {
26
+ transporter: Transporter;
27
+ from: MailAddress;
28
+ instrumentation?: ProviderInstrumentationTarget;
29
+ }
30
+ export declare function createSmtpMailer({ transporter, from, instrumentation: instrumentationTarget, }: CreateSmtpMailerOptions): MailerPort;
31
+ export declare const mailSmtpProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
32
+ HOST: z.ZodString;
33
+ PORT: z.ZodCoercedNumber<unknown>;
34
+ USER: z.ZodString;
35
+ PASS: z.ZodString;
36
+ FROM: z.ZodString;
37
+ }, z.core.$strip>, {
38
+ mailer: MailerPort;
39
+ smtp: {
40
+ transporter: Transporter<import("nodemailer/lib/smtp-transport").SentMessageInfo, import("nodemailer/lib/smtp-transport").Options>;
41
+ };
42
+ }>;
43
+ export {};
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,UAAU,EAIhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAEV,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,gBAAgB;;;;;;iBAMpB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,mBAAmB,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAoDD,wBAAgB,gBAAgB,CAAC,EAC/B,WAAW,EACX,IAAI,EACJ,eAAe,EAAE,qBAAqB,GACvC,EAAE,uBAAuB,GAAG,UAAU,CAqEtC;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;EAwD3B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @beignet/provider-mail-smtp
3
+ *
4
+ * SMTP-backed mail provider for Beignet.
5
+ */
6
+ import { formatMailAddress, formatMailAddressList, MailDeliveryError, normalizeMailMessage, } from "@beignet/core/mail";
7
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
8
+ import nodemailer from "nodemailer";
9
+ import { z } from "zod";
10
+ const MailConfigSchema = z.object({
11
+ HOST: z.string(),
12
+ PORT: z.coerce.number().int().positive(),
13
+ USER: z.string(),
14
+ PASS: z.string(),
15
+ FROM: z.string().min(1),
16
+ });
17
+ function addOptional(target, key, value) {
18
+ if (value !== undefined) {
19
+ Object.assign(target, { [key]: value });
20
+ }
21
+ }
22
+ function createSmtpPayload(message, defaultFrom) {
23
+ const normalized = normalizeMailMessage(message, { defaultFrom });
24
+ if (!normalized.from) {
25
+ throw new MailDeliveryError({
26
+ provider: "smtp",
27
+ message: "Cannot send email without a from address.",
28
+ });
29
+ }
30
+ const payload = {
31
+ from: formatMailAddress(normalized.from),
32
+ to: formatMailAddressList(normalized.to),
33
+ subject: normalized.subject,
34
+ };
35
+ addOptional(payload, "text", normalized.text);
36
+ addOptional(payload, "html", normalized.html);
37
+ addOptional(payload, "cc", normalized.cc ? formatMailAddressList(normalized.cc) : undefined);
38
+ addOptional(payload, "bcc", normalized.bcc ? formatMailAddressList(normalized.bcc) : undefined);
39
+ addOptional(payload, "replyTo", normalized.replyTo ? formatMailAddressList(normalized.replyTo) : undefined);
40
+ addOptional(payload, "headers", normalized.headers);
41
+ return payload;
42
+ }
43
+ export function createSmtpMailer({ transporter, from, instrumentation: instrumentationTarget, }) {
44
+ const instrumentation = createProviderInstrumentation(instrumentationTarget, {
45
+ providerName: "mail-smtp",
46
+ watcher: "mail",
47
+ });
48
+ return {
49
+ async send(message) {
50
+ const payload = createSmtpPayload(message, from);
51
+ instrumentation.custom({
52
+ name: "mail.send",
53
+ label: "Mail send",
54
+ summary: `Sending "${message.subject}"`,
55
+ details: {
56
+ provider: "smtp",
57
+ subject: message.subject,
58
+ to: payload.to,
59
+ },
60
+ });
61
+ try {
62
+ const result = await transporter.sendMail(payload);
63
+ const id = typeof result === "object" &&
64
+ result !== null &&
65
+ "messageId" in result &&
66
+ typeof result.messageId === "string"
67
+ ? result.messageId
68
+ : undefined;
69
+ instrumentation.custom({
70
+ name: "mail.sent",
71
+ label: "Mail sent",
72
+ summary: `Sent "${message.subject}"`,
73
+ details: {
74
+ provider: "smtp",
75
+ subject: message.subject,
76
+ to: payload.to,
77
+ id,
78
+ },
79
+ });
80
+ return {
81
+ id,
82
+ provider: "smtp",
83
+ };
84
+ }
85
+ catch (error) {
86
+ instrumentation.custom({
87
+ name: "mail.failed",
88
+ label: "Mail failed",
89
+ summary: `Failed to send "${message.subject}"`,
90
+ details: {
91
+ provider: "smtp",
92
+ subject: message.subject,
93
+ to: payload.to,
94
+ error: error instanceof Error ? error.message : String(error),
95
+ },
96
+ });
97
+ throw new MailDeliveryError({
98
+ provider: "smtp",
99
+ message: error instanceof Error
100
+ ? error.message
101
+ : "SMTP failed to send email.",
102
+ cause: error,
103
+ });
104
+ }
105
+ },
106
+ };
107
+ }
108
+ export const mailSmtpProvider = createProvider({
109
+ name: "mail-smtp",
110
+ config: {
111
+ schema: MailConfigSchema,
112
+ envPrefix: "MAIL_",
113
+ },
114
+ async setup({ config, ports }) {
115
+ if (!config) {
116
+ throw new Error("[mailSmtpProvider] Missing Mail config. " +
117
+ "Please set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM environment variables.");
118
+ }
119
+ const transporter = nodemailer.createTransport({
120
+ host: config.HOST,
121
+ port: config.PORT,
122
+ secure: config.PORT === 465,
123
+ auth: {
124
+ user: config.USER,
125
+ pass: config.PASS,
126
+ },
127
+ });
128
+ try {
129
+ await transporter.verify();
130
+ }
131
+ catch (error) {
132
+ throw new Error(`[mailSmtpProvider] Failed to connect to mail server at ${config.HOST}:${config.PORT}: ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ const instrumentation = createProviderInstrumentation(ports, {
135
+ providerName: "mail-smtp",
136
+ watcher: "mail",
137
+ });
138
+ const mailer = createSmtpMailer({
139
+ transporter,
140
+ from: config.FROM,
141
+ instrumentation,
142
+ });
143
+ return {
144
+ ports: {
145
+ mailer,
146
+ smtp: { transporter },
147
+ },
148
+ stop() {
149
+ transporter.close();
150
+ },
151
+ };
152
+ },
153
+ });
154
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EAErB,iBAAiB,EAEjB,oBAAoB,GAGrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,6BAA6B,GAE9B,MAAM,yBAAyB,CAAC;AAKjC,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACxC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACxB,CAAC,CAAC;AAmBH,SAAS,WAAW,CAGlB,MAAS,EAAE,GAAM,EAAE,KAA+C;IAClE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CACxB,OAAwB,EACxB,WAAwB;IAExB,MAAM,UAAU,GAAG,oBAAoB,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;IAElE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,iBAAiB,CAAC;YAC1B,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,2CAA2C;SACrD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC;QACxC,EAAE,EAAE,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,UAAU,CAAC,OAAO;KACQ,CAAC;IAEtC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9C,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9C,WAAW,CACT,OAAO,EACP,IAAI,EACJ,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CACjE,CAAC;IACF,WAAW,CACT,OAAO,EACP,KAAK,EACL,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CACnE,CAAC;IACF,WAAW,CACT,OAAO,EACP,SAAS,EACT,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAC3E,CAAC;IACF,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IAEpD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAC/B,WAAW,EACX,IAAI,EACJ,eAAe,EAAE,qBAAqB,GACd;IACxB,MAAM,eAAe,GAAG,6BAA6B,CAAC,qBAAqB,EAAE;QAC3E,YAAY,EAAE,WAAW;QACzB,OAAO,EAAE,MAAM;KAChB,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACjD,eAAe,CAAC,MAAM,CAAC;gBACrB,IAAI,EAAE,WAAW;gBACjB,KAAK,EAAE,WAAW;gBAClB,OAAO,EAAE,YAAY,OAAO,CAAC,OAAO,GAAG;gBACvC,OAAO,EAAE;oBACP,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,EAAE,EAAE,OAAO,CAAC,EAAE;iBACf;aACF,CAAC,CAAC;YAEH,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACnD,MAAM,EAAE,GACN,OAAO,MAAM,KAAK,QAAQ;oBAC1B,MAAM,KAAK,IAAI;oBACf,WAAW,IAAI,MAAM;oBACrB,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;oBAClC,CAAC,CAAC,MAAM,CAAC,SAAS;oBAClB,CAAC,CAAC,SAAS,CAAC;gBAEhB,eAAe,CAAC,MAAM,CAAC;oBACrB,IAAI,EAAE,WAAW;oBACjB,KAAK,EAAE,WAAW;oBAClB,OAAO,EAAE,SAAS,OAAO,CAAC,OAAO,GAAG;oBACpC,OAAO,EAAE;wBACP,QAAQ,EAAE,MAAM;wBAChB,OAAO,EAAE,OAAO,CAAC,OAAO;wBACxB,EAAE,EAAE,OAAO,CAAC,EAAE;wBACd,EAAE;qBACH;iBACF,CAAC,CAAC;gBAEH,OAAO;oBACL,EAAE;oBACF,QAAQ,EAAE,MAAM;iBACjB,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,eAAe,CAAC,MAAM,CAAC;oBACrB,IAAI,EAAE,aAAa;oBACnB,KAAK,EAAE,aAAa;oBACpB,OAAO,EAAE,mBAAmB,OAAO,CAAC,OAAO,GAAG;oBAC9C,OAAO,EAAE;wBACP,QAAQ,EAAE,MAAM;wBAChB,OAAO,EAAE,OAAO,CAAC,OAAO;wBACxB,EAAE,EAAE,OAAO,CAAC,EAAE;wBACd,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;qBAC9D;iBACF,CAAC,CAAC;gBACH,MAAM,IAAI,iBAAiB,CAAC;oBAC1B,QAAQ,EAAE,MAAM;oBAChB,OAAO,EACL,KAAK,YAAY,KAAK;wBACpB,CAAC,CAAC,KAAK,CAAC,OAAO;wBACf,CAAC,CAAC,4BAA4B;oBAClC,KAAK,EAAE,KAAK;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,cAAc,CAAC;IAC7C,IAAI,EAAE,WAAW;IAEjB,MAAM,EAAE;QACN,MAAM,EAAE,gBAAgB;QACxB,SAAS,EAAE,OAAO;KACnB;IAED,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,0CAA0C;gBACxC,6FAA6F,CAChG,CAAC;QACJ,CAAC;QAED,MAAM,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;YAC7C,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM,EAAE,MAAM,CAAC,IAAI,KAAK,GAAG;YAC3B,IAAI,EAAE;gBACJ,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,IAAI,EAAE,MAAM,CAAC,IAAI;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,WAAW,CAAC,MAAM,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,0DAA0D,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,KAClF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACJ,CAAC;QAED,MAAM,eAAe,GAAG,6BAA6B,CAAC,KAAK,EAAE;YAC3D,YAAY,EAAE,WAAW;YACzB,OAAO,EAAE,MAAM;SAChB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,gBAAgB,CAAC;YAC9B,WAAW;YACX,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,eAAe;SAChB,CAAC,CAAC;QAEH,OAAO;YACL,KAAK,EAAE;gBACL,MAAM;gBACN,IAAI,EAAE,EAAE,WAAW,EAAE;aACU;YACjC,IAAI;gBACF,WAAW,CAAC,KAAK,EAAE,CAAC;YACtB,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@beignet/provider-mail-smtp",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "SMTP mail provider for Beignet - adds mailer port using nodemailer",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "beignet",
33
+ "mail",
34
+ "email",
35
+ "provider",
36
+ "smtp",
37
+ "nodemailer",
38
+ "ports"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/taylorbryant/beignet.git",
44
+ "directory": "packages/provider-mail-smtp"
45
+ },
46
+ "author": "Taylor Bryant",
47
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
48
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
49
+ "sideEffects": false,
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "nodemailer": "^7.0.7"
58
+ },
59
+ "dependencies": {
60
+ "zod": "^4.0.0",
61
+ "@beignet/core": "*"
62
+ },
63
+ "devDependencies": {
64
+ "@beignet/devtools": "*",
65
+ "@types/bun": "^1.3.13",
66
+ "@types/node": "^20.10.0",
67
+ "@types/nodemailer": "^6.4.0",
68
+ "nodemailer": "^7.0.7",
69
+ "typescript": "^5.3.0"
70
+ }
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @beignet/provider-mail-smtp
3
+ *
4
+ * SMTP-backed mail provider for Beignet.
5
+ */
6
+
7
+ import {
8
+ formatMailAddress,
9
+ formatMailAddressList,
10
+ type MailAddress,
11
+ MailDeliveryError,
12
+ type MailerPort,
13
+ normalizeMailMessage,
14
+ type SendMailOptions,
15
+ type SendMailResult,
16
+ } from "@beignet/core/mail";
17
+ import {
18
+ createProvider,
19
+ createProviderInstrumentation,
20
+ type ProviderInstrumentationTarget,
21
+ } from "@beignet/core/providers";
22
+ import type {
23
+ SendMailOptions as NodemailerSendMailOptions,
24
+ Transporter,
25
+ } from "nodemailer";
26
+ import nodemailer from "nodemailer";
27
+ import { z } from "zod";
28
+
29
+ const MailConfigSchema = z.object({
30
+ HOST: z.string(),
31
+ PORT: z.coerce.number().int().positive(),
32
+ USER: z.string(),
33
+ PASS: z.string(),
34
+ FROM: z.string().min(1),
35
+ });
36
+
37
+ export type MailConfig = z.infer<typeof MailConfigSchema>;
38
+
39
+ export interface SmtpMailEscapeHatch {
40
+ transporter: Transporter;
41
+ }
42
+
43
+ export interface SmtpMailProviderPorts {
44
+ mailer: MailerPort;
45
+ smtp: SmtpMailEscapeHatch;
46
+ }
47
+
48
+ export interface CreateSmtpMailerOptions {
49
+ transporter: Transporter;
50
+ from: MailAddress;
51
+ instrumentation?: ProviderInstrumentationTarget;
52
+ }
53
+
54
+ function addOptional<
55
+ T extends Record<string, unknown>,
56
+ K extends keyof NodemailerSendMailOptions,
57
+ >(target: T, key: K, value: NodemailerSendMailOptions[K] | undefined): void {
58
+ if (value !== undefined) {
59
+ Object.assign(target, { [key]: value });
60
+ }
61
+ }
62
+
63
+ function createSmtpPayload(
64
+ message: SendMailOptions,
65
+ defaultFrom: MailAddress,
66
+ ): NodemailerSendMailOptions {
67
+ const normalized = normalizeMailMessage(message, { defaultFrom });
68
+
69
+ if (!normalized.from) {
70
+ throw new MailDeliveryError({
71
+ provider: "smtp",
72
+ message: "Cannot send email without a from address.",
73
+ });
74
+ }
75
+
76
+ const payload = {
77
+ from: formatMailAddress(normalized.from),
78
+ to: formatMailAddressList(normalized.to),
79
+ subject: normalized.subject,
80
+ } satisfies NodemailerSendMailOptions;
81
+
82
+ addOptional(payload, "text", normalized.text);
83
+ addOptional(payload, "html", normalized.html);
84
+ addOptional(
85
+ payload,
86
+ "cc",
87
+ normalized.cc ? formatMailAddressList(normalized.cc) : undefined,
88
+ );
89
+ addOptional(
90
+ payload,
91
+ "bcc",
92
+ normalized.bcc ? formatMailAddressList(normalized.bcc) : undefined,
93
+ );
94
+ addOptional(
95
+ payload,
96
+ "replyTo",
97
+ normalized.replyTo ? formatMailAddressList(normalized.replyTo) : undefined,
98
+ );
99
+ addOptional(payload, "headers", normalized.headers);
100
+
101
+ return payload;
102
+ }
103
+
104
+ export function createSmtpMailer({
105
+ transporter,
106
+ from,
107
+ instrumentation: instrumentationTarget,
108
+ }: CreateSmtpMailerOptions): MailerPort {
109
+ const instrumentation = createProviderInstrumentation(instrumentationTarget, {
110
+ providerName: "mail-smtp",
111
+ watcher: "mail",
112
+ });
113
+
114
+ return {
115
+ async send(message): Promise<SendMailResult> {
116
+ const payload = createSmtpPayload(message, from);
117
+ instrumentation.custom({
118
+ name: "mail.send",
119
+ label: "Mail send",
120
+ summary: `Sending "${message.subject}"`,
121
+ details: {
122
+ provider: "smtp",
123
+ subject: message.subject,
124
+ to: payload.to,
125
+ },
126
+ });
127
+
128
+ try {
129
+ const result = await transporter.sendMail(payload);
130
+ const id =
131
+ typeof result === "object" &&
132
+ result !== null &&
133
+ "messageId" in result &&
134
+ typeof result.messageId === "string"
135
+ ? result.messageId
136
+ : undefined;
137
+
138
+ instrumentation.custom({
139
+ name: "mail.sent",
140
+ label: "Mail sent",
141
+ summary: `Sent "${message.subject}"`,
142
+ details: {
143
+ provider: "smtp",
144
+ subject: message.subject,
145
+ to: payload.to,
146
+ id,
147
+ },
148
+ });
149
+
150
+ return {
151
+ id,
152
+ provider: "smtp",
153
+ };
154
+ } catch (error) {
155
+ instrumentation.custom({
156
+ name: "mail.failed",
157
+ label: "Mail failed",
158
+ summary: `Failed to send "${message.subject}"`,
159
+ details: {
160
+ provider: "smtp",
161
+ subject: message.subject,
162
+ to: payload.to,
163
+ error: error instanceof Error ? error.message : String(error),
164
+ },
165
+ });
166
+ throw new MailDeliveryError({
167
+ provider: "smtp",
168
+ message:
169
+ error instanceof Error
170
+ ? error.message
171
+ : "SMTP failed to send email.",
172
+ cause: error,
173
+ });
174
+ }
175
+ },
176
+ };
177
+ }
178
+
179
+ export const mailSmtpProvider = createProvider({
180
+ name: "mail-smtp",
181
+
182
+ config: {
183
+ schema: MailConfigSchema,
184
+ envPrefix: "MAIL_",
185
+ },
186
+
187
+ async setup({ config, ports }) {
188
+ if (!config) {
189
+ throw new Error(
190
+ "[mailSmtpProvider] Missing Mail config. " +
191
+ "Please set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM environment variables.",
192
+ );
193
+ }
194
+
195
+ const transporter = nodemailer.createTransport({
196
+ host: config.HOST,
197
+ port: config.PORT,
198
+ secure: config.PORT === 465,
199
+ auth: {
200
+ user: config.USER,
201
+ pass: config.PASS,
202
+ },
203
+ });
204
+
205
+ try {
206
+ await transporter.verify();
207
+ } catch (error) {
208
+ throw new Error(
209
+ `[mailSmtpProvider] Failed to connect to mail server at ${config.HOST}:${config.PORT}: ${
210
+ error instanceof Error ? error.message : String(error)
211
+ }`,
212
+ );
213
+ }
214
+
215
+ const instrumentation = createProviderInstrumentation(ports, {
216
+ providerName: "mail-smtp",
217
+ watcher: "mail",
218
+ });
219
+ const mailer = createSmtpMailer({
220
+ transporter,
221
+ from: config.FROM,
222
+ instrumentation,
223
+ });
224
+
225
+ return {
226
+ ports: {
227
+ mailer,
228
+ smtp: { transporter },
229
+ } satisfies SmtpMailProviderPorts,
230
+ stop() {
231
+ transporter.close();
232
+ },
233
+ };
234
+ },
235
+ });