@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 +5 -0
- package/README.md +95 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +235 -0
package/CHANGELOG.md
ADDED
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
});
|