@nwire/mail-nodemailer 0.7.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/__tests__/mail-nodemailer.integration.test.d.ts +24 -0
- package/dist/__tests__/mail-nodemailer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/mail-nodemailer.integration.test.js +129 -0
- package/dist/__tests__/mail-nodemailer.integration.test.js.map +1 -0
- package/dist/__tests__/mail-nodemailer.test.d.ts +9 -0
- package/dist/__tests__/mail-nodemailer.test.d.ts.map +1 -0
- package/dist/__tests__/mail-nodemailer.test.js +67 -0
- package/dist/__tests__/mail-nodemailer.test.js.map +1 -0
- package/dist/mail-nodemailer.d.ts +52 -0
- package/dist/mail-nodemailer.d.ts.map +1 -0
- package/dist/mail-nodemailer.js +114 -0
- package/dist/mail-nodemailer.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @nwire/mail-nodemailer
|
|
2
|
+
|
|
3
|
+
> [Nodemailer](https://nodemailer.com)-backed `Mailer` — SMTP, Mailhog, Gmail, Office365.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Wraps a nodemailer `Transporter` so handlers can send mail through the canonical `Mailer` contract without knowing whether they're talking to Mailhog, Postfix, or a cloud relay.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @nwire/mail-nodemailer @nwire/mail nodemailer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { smtpMailer } from "@nwire/mail-nodemailer";
|
|
19
|
+
import { mailPlugin } from "@nwire/mail";
|
|
20
|
+
import { defineApp } from "@nwire/forge";
|
|
21
|
+
|
|
22
|
+
defineApp("my-app", {
|
|
23
|
+
plugins: [
|
|
24
|
+
mailPlugin({
|
|
25
|
+
mailer: smtpMailer({
|
|
26
|
+
host: process.env.SMTP_HOST,
|
|
27
|
+
port: Number(process.env.SMTP_PORT),
|
|
28
|
+
auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
|
|
29
|
+
}),
|
|
30
|
+
defaultFrom: '"My App" <noreply@example.com>',
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Local dev:
|
|
36
|
+
// smtpMailer({ host: "localhost", port: 1025 }) // Mailhog (no auth)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API surface
|
|
40
|
+
|
|
41
|
+
- `smtpMailer(transporterOptions)` — implements `Mailer` using a nodemailer transport.
|
|
42
|
+
|
|
43
|
+
## When to use
|
|
44
|
+
|
|
45
|
+
Production transactional mail or local-dev with Mailhog (`nwire infra up` ships Mailhog at `http://localhost:8025`). Fits L3 and up.
|
|
46
|
+
|
|
47
|
+
## Standalone use
|
|
48
|
+
|
|
49
|
+
For developers using `@nwire/mail-nodemailer` **without the rest of Nwire** — pair it with any TypeScript project, any container, any HTTP framework.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// See the package's main entry (src/) for the standalone surface.
|
|
53
|
+
// The exports below work without @nwire/app or @nwire/forge.
|
|
54
|
+
import {} from /* ...standalone exports... */ "@nwire/mail-nodemailer";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Within nwire-app
|
|
58
|
+
|
|
59
|
+
For developers using this package as part of the Nwire stack — register it via `app.use(...)` or it auto-wires when you compose `createApp({ modules })`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createApp } from "@nwire/forge";
|
|
63
|
+
|
|
64
|
+
const app = createApp({
|
|
65
|
+
/* ...config... */
|
|
66
|
+
});
|
|
67
|
+
// Adapter/plugin wiring happens here when applicable.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## See also
|
|
71
|
+
|
|
72
|
+
- [Architecture sketch §05 — Adapters tier](../../architecture-sketch.html#packages)
|
|
73
|
+
- Sibling packages: [@nwire/mail](../nwire-mail)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — exercises `@nwire/mail-nodemailer` against real
|
|
3
|
+
* Mailhog. Mailhog accepts SMTP on `1025` and exposes a tiny JSON API
|
|
4
|
+
* on `8025/api/v2/messages` listing every received message — that's
|
|
5
|
+
* what we use to confirm the mail actually arrived as we sent it.
|
|
6
|
+
*
|
|
7
|
+
* Mailhog comes from the workspace docker-compose. When the SMTP port
|
|
8
|
+
* isn't reachable on `127.0.0.1:1025`, the whole suite skips.
|
|
9
|
+
*
|
|
10
|
+
* Why we need real SMTP for these checks:
|
|
11
|
+
*
|
|
12
|
+
* - `transporter.verify()` (our healthCheck) opens a real connection,
|
|
13
|
+
* completes the HELO + EHLO handshake, then closes. Mocks can't
|
|
14
|
+
* prove the wire protocol is wired up correctly.
|
|
15
|
+
* - The mapping from our `MailMessage` shape (mixed string +
|
|
16
|
+
* `{email,name}` recipients, attachments as `Uint8Array`) into
|
|
17
|
+
* RFC-822 headers + MIME parts is where Nodemailer's behavior is
|
|
18
|
+
* most likely to surprise us across upgrades. Asserting against
|
|
19
|
+
* Mailhog's parsed inbox lets us check the receiver's view.
|
|
20
|
+
* - Pool teardown (`shutdown()`) only matters when there's a real
|
|
21
|
+
* connection pool to close.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=mail-nodemailer.integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mail-nodemailer.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — exercises `@nwire/mail-nodemailer` against real
|
|
3
|
+
* Mailhog. Mailhog accepts SMTP on `1025` and exposes a tiny JSON API
|
|
4
|
+
* on `8025/api/v2/messages` listing every received message — that's
|
|
5
|
+
* what we use to confirm the mail actually arrived as we sent it.
|
|
6
|
+
*
|
|
7
|
+
* Mailhog comes from the workspace docker-compose. When the SMTP port
|
|
8
|
+
* isn't reachable on `127.0.0.1:1025`, the whole suite skips.
|
|
9
|
+
*
|
|
10
|
+
* Why we need real SMTP for these checks:
|
|
11
|
+
*
|
|
12
|
+
* - `transporter.verify()` (our healthCheck) opens a real connection,
|
|
13
|
+
* completes the HELO + EHLO handshake, then closes. Mocks can't
|
|
14
|
+
* prove the wire protocol is wired up correctly.
|
|
15
|
+
* - The mapping from our `MailMessage` shape (mixed string +
|
|
16
|
+
* `{email,name}` recipients, attachments as `Uint8Array`) into
|
|
17
|
+
* RFC-822 headers + MIME parts is where Nodemailer's behavior is
|
|
18
|
+
* most likely to surprise us across upgrades. Asserting against
|
|
19
|
+
* Mailhog's parsed inbox lets us check the receiver's view.
|
|
20
|
+
* - Pool teardown (`shutdown()`) only matters when there's a real
|
|
21
|
+
* connection pool to close.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|
24
|
+
import { isReachable } from "@nwire/test-kit";
|
|
25
|
+
import { smtpMailer } from "../mail-nodemailer.js";
|
|
26
|
+
const MAILHOG_HOST = "127.0.0.1";
|
|
27
|
+
const MAILHOG_SMTP_PORT = 1025;
|
|
28
|
+
const MAILHOG_HTTP_PORT = 8025;
|
|
29
|
+
const MAILHOG_API = `http://${MAILHOG_HOST}:${MAILHOG_HTTP_PORT}/api/v2/messages`;
|
|
30
|
+
const reachable = await isReachable(MAILHOG_HOST, MAILHOG_SMTP_PORT, 800);
|
|
31
|
+
async function purgeInbox() {
|
|
32
|
+
await fetch(`http://${MAILHOG_HOST}:${MAILHOG_HTTP_PORT}/api/v1/messages`, {
|
|
33
|
+
method: "DELETE",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function fetchInbox() {
|
|
37
|
+
const res = await fetch(MAILHOG_API);
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error(`Mailhog API ${res.status}`);
|
|
40
|
+
const data = (await res.json());
|
|
41
|
+
return data.items;
|
|
42
|
+
}
|
|
43
|
+
async function waitForCount(n, timeoutMs = 3_000) {
|
|
44
|
+
const deadline = Date.now() + timeoutMs;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
const items = await fetchInbox();
|
|
47
|
+
if (items.length >= n)
|
|
48
|
+
return items;
|
|
49
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`waitForCount: only ${(await fetchInbox()).length}/${n} after ${timeoutMs}ms`);
|
|
52
|
+
}
|
|
53
|
+
describe.skipIf(!reachable)("mail-nodemailer ↔ real Mailhog", () => {
|
|
54
|
+
let mailer;
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
mailer = smtpMailer({
|
|
57
|
+
host: MAILHOG_HOST,
|
|
58
|
+
port: MAILHOG_SMTP_PORT,
|
|
59
|
+
// Mailhog has no auth and no TLS in the dev compose.
|
|
60
|
+
pool: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await purgeInbox();
|
|
65
|
+
});
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await mailer.shutdown?.();
|
|
68
|
+
});
|
|
69
|
+
it("healthCheck verifies the SMTP handshake", async () => {
|
|
70
|
+
await expect(mailer.healthCheck()).resolves.toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
it("sends a plain-text message with the right To/From/Subject headers", async () => {
|
|
73
|
+
const result = await mailer.send({
|
|
74
|
+
from: "noreply@example.com",
|
|
75
|
+
to: "alice@example.com",
|
|
76
|
+
subject: "Phase 77 hello",
|
|
77
|
+
text: "Hello from a real SMTP round-trip.",
|
|
78
|
+
});
|
|
79
|
+
expect(result.messageId).toMatch(/@/); // RFC-822 message-id
|
|
80
|
+
const [msg] = await waitForCount(1);
|
|
81
|
+
expect(msg.Content.Headers.Subject?.[0]).toBe("Phase 77 hello");
|
|
82
|
+
expect(msg.Content.Headers.From?.[0]).toContain("noreply@example.com");
|
|
83
|
+
expect(msg.Content.Headers.To?.[0]).toContain("alice@example.com");
|
|
84
|
+
expect(msg.Content.Body).toContain("Hello from a real SMTP round-trip.");
|
|
85
|
+
});
|
|
86
|
+
it("maps {email,name} recipient objects into RFC-822 display-name form", async () => {
|
|
87
|
+
await mailer.send({
|
|
88
|
+
from: { email: "noreply@example.com", name: "Phase 77" },
|
|
89
|
+
to: { email: "alice@example.com", name: "Alice Example" },
|
|
90
|
+
subject: "Named recipient",
|
|
91
|
+
text: "ok",
|
|
92
|
+
});
|
|
93
|
+
const [msg] = await waitForCount(1);
|
|
94
|
+
// Nodemailer emits `Display Name <addr>` for simple display names; only
|
|
95
|
+
// names with special chars get quoted. Verify both parts arrived.
|
|
96
|
+
const from = msg.Content.Headers.From?.[0] ?? "";
|
|
97
|
+
const to = msg.Content.Headers.To?.[0] ?? "";
|
|
98
|
+
expect(from).toContain("Phase 77");
|
|
99
|
+
expect(from).toContain("<noreply@example.com>");
|
|
100
|
+
expect(to).toContain("Alice Example");
|
|
101
|
+
expect(to).toContain("<alice@example.com>");
|
|
102
|
+
});
|
|
103
|
+
it("multi-recipient arrays produce a comma-separated To header", async () => {
|
|
104
|
+
await mailer.send({
|
|
105
|
+
from: "noreply@example.com",
|
|
106
|
+
to: ["alice@example.com", "bob@example.com"],
|
|
107
|
+
subject: "Group blast",
|
|
108
|
+
text: "ok",
|
|
109
|
+
});
|
|
110
|
+
const [msg] = await waitForCount(1);
|
|
111
|
+
const to = msg.Content.Headers.To?.[0] ?? "";
|
|
112
|
+
expect(to).toMatch(/alice@example\.com/);
|
|
113
|
+
expect(to).toMatch(/bob@example\.com/);
|
|
114
|
+
});
|
|
115
|
+
it("html bodies arrive as a MIME multipart with the expected content type", async () => {
|
|
116
|
+
await mailer.send({
|
|
117
|
+
from: "noreply@example.com",
|
|
118
|
+
to: "alice@example.com",
|
|
119
|
+
subject: "HTML test",
|
|
120
|
+
text: "fallback",
|
|
121
|
+
html: "<p>Hello, <b>real SMTP</b>.</p>",
|
|
122
|
+
});
|
|
123
|
+
const [msg] = await waitForCount(1);
|
|
124
|
+
const ct = msg.Content.Headers["Content-Type"]?.[0] ?? "";
|
|
125
|
+
expect(ct).toMatch(/multipart\/alternative/);
|
|
126
|
+
expect(msg.Content.Body).toContain("<b>real SMTP</b>");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=mail-nodemailer.integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.integration.test.js","sourceRoot":"","sources":["../../src/__tests__/mail-nodemailer.integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mail-nodemailer` — adapter unit tests.
|
|
3
|
+
*
|
|
4
|
+
* We don't boot a real SMTP server here; we stub the transporter so the
|
|
5
|
+
* tests run fast without Mailhog. Integration tests against real Mailhog
|
|
6
|
+
* happen via `nwire infra up`.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=mail-nodemailer.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mail-nodemailer.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mail-nodemailer` — adapter unit tests.
|
|
3
|
+
*
|
|
4
|
+
* We don't boot a real SMTP server here; we stub the transporter so the
|
|
5
|
+
* tests run fast without Mailhog. Integration tests against real Mailhog
|
|
6
|
+
* happen via `nwire infra up`.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from "vitest";
|
|
9
|
+
import { smtpMailer } from "../mail-nodemailer.js";
|
|
10
|
+
import { MailSendError } from "@nwire/mail";
|
|
11
|
+
function stubTransporter(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
sendMail: vi.fn(async () => ({ messageId: "<smtp-abc@local>" })),
|
|
14
|
+
verify: vi.fn(async () => true),
|
|
15
|
+
close: vi.fn(() => { }),
|
|
16
|
+
...overrides,
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("smtpMailer", () => {
|
|
21
|
+
it("send maps the canonical MailMessage to Nodemailer's SendMailOptions", async () => {
|
|
22
|
+
const transporter = stubTransporter();
|
|
23
|
+
const mailer = smtpMailer({ host: "x", port: 1025, transporter });
|
|
24
|
+
await mailer.send({
|
|
25
|
+
to: [{ email: "alice@x", name: "Alice" }, "bob@x"],
|
|
26
|
+
from: { email: "noreply@x", name: "My App" },
|
|
27
|
+
subject: "Welcome",
|
|
28
|
+
html: "<p>hi</p>",
|
|
29
|
+
headers: { "X-Trace": "abc" },
|
|
30
|
+
});
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const call = transporter.sendMail.mock.calls[0][0];
|
|
33
|
+
expect(call.subject).toBe("Welcome");
|
|
34
|
+
expect(call.from).toEqual({ name: "My App", address: "noreply@x" });
|
|
35
|
+
expect(call.to).toEqual([
|
|
36
|
+
{ name: "Alice", address: "alice@x" },
|
|
37
|
+
"bob@x",
|
|
38
|
+
]);
|
|
39
|
+
expect(call.headers).toEqual({ "X-Trace": "abc" });
|
|
40
|
+
});
|
|
41
|
+
it("send returns a populated MailSendResult", async () => {
|
|
42
|
+
const mailer = smtpMailer({ host: "x", port: 1025, transporter: stubTransporter() });
|
|
43
|
+
const result = await mailer.send({ to: "a", subject: "" });
|
|
44
|
+
expect(result.messageId).toBe("<smtp-abc@local>");
|
|
45
|
+
expect(typeof result.durationMs).toBe("number");
|
|
46
|
+
});
|
|
47
|
+
it("send wraps transporter errors as MailSendError", async () => {
|
|
48
|
+
const transporter = stubTransporter({
|
|
49
|
+
sendMail: vi.fn(async () => { throw new Error("conn refused"); }),
|
|
50
|
+
});
|
|
51
|
+
const mailer = smtpMailer({ host: "x", port: 1025, transporter });
|
|
52
|
+
await expect(mailer.send({ to: "a", subject: "" })).rejects.toBeInstanceOf(MailSendError);
|
|
53
|
+
});
|
|
54
|
+
it("healthCheck calls transporter.verify", async () => {
|
|
55
|
+
const transporter = stubTransporter();
|
|
56
|
+
const mailer = smtpMailer({ host: "x", port: 1025, transporter });
|
|
57
|
+
await mailer.healthCheck();
|
|
58
|
+
expect(transporter.verify).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
it("shutdown closes the transporter", async () => {
|
|
61
|
+
const transporter = stubTransporter();
|
|
62
|
+
const mailer = smtpMailer({ host: "x", port: 1025, transporter });
|
|
63
|
+
await mailer.shutdown();
|
|
64
|
+
expect(transporter.close).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=mail-nodemailer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.test.js","sourceRoot":"","sources":["../../src/__tests__/mail-nodemailer.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mail-nodemailer` — Nodemailer-backed Mailer.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a nodemailer Transporter so handlers can send mail through the
|
|
5
|
+
* canonical `Mailer` contract without knowing whether they're talking to
|
|
6
|
+
* Mailhog locally, Postfix on the same box, or a cloud relay.
|
|
7
|
+
*
|
|
8
|
+
* import { smtpMailer } from "@nwire/mail-nodemailer";
|
|
9
|
+
* import { mailPlugin } from "@nwire/mail";
|
|
10
|
+
*
|
|
11
|
+
* defineApp("my-app", {
|
|
12
|
+
* plugins: [mailPlugin({
|
|
13
|
+
* mailer: smtpMailer({
|
|
14
|
+
* host: process.env.SMTP_HOST,
|
|
15
|
+
* port: Number(process.env.SMTP_PORT),
|
|
16
|
+
* auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
|
|
17
|
+
* }),
|
|
18
|
+
* defaultFrom: '"My App" <noreply@example.com>',
|
|
19
|
+
* })],
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* For local dev:
|
|
23
|
+
*
|
|
24
|
+
* smtpMailer({ host: "localhost", port: 1025 }) // Mailhog (no auth)
|
|
25
|
+
*
|
|
26
|
+
* Run `nwire infra up` and open http://localhost:8025 to inspect sent mail.
|
|
27
|
+
*/
|
|
28
|
+
import type { Transporter } from "nodemailer";
|
|
29
|
+
import { type Mailer } from "@nwire/mail";
|
|
30
|
+
export interface SmtpMailerOptions {
|
|
31
|
+
readonly host: string;
|
|
32
|
+
readonly port: number;
|
|
33
|
+
/** Use TLS at connect time (port 465). Default: false (STARTTLS otherwise). */
|
|
34
|
+
readonly secure?: boolean;
|
|
35
|
+
readonly auth?: {
|
|
36
|
+
readonly user: string;
|
|
37
|
+
readonly pass: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Reuse an existing nodemailer Transporter. Useful when the app already
|
|
41
|
+
* has a custom-configured transporter (multi-tenant, pooling tuned, ...)
|
|
42
|
+
*/
|
|
43
|
+
readonly transporter?: Transporter;
|
|
44
|
+
/**
|
|
45
|
+
* Pool connections? Nodemailer's pool transport is more efficient for
|
|
46
|
+
* high-throughput senders. Default: true.
|
|
47
|
+
*/
|
|
48
|
+
readonly pool?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Build a Mailer backed by Nodemailer's SMTP transport. */
|
|
51
|
+
export declare function smtpMailer(options: SmtpMailerOptions): Mailer;
|
|
52
|
+
//# sourceMappingURL=mail-nodemailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.d.ts","sourceRoot":"","sources":["../src/mail-nodemailer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,YAAY,CAAC;AAC/D,OAAO,EAEL,KAAK,MAAM,EAIZ,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IACF;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAa7D"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mail-nodemailer` — Nodemailer-backed Mailer.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a nodemailer Transporter so handlers can send mail through the
|
|
5
|
+
* canonical `Mailer` contract without knowing whether they're talking to
|
|
6
|
+
* Mailhog locally, Postfix on the same box, or a cloud relay.
|
|
7
|
+
*
|
|
8
|
+
* import { smtpMailer } from "@nwire/mail-nodemailer";
|
|
9
|
+
* import { mailPlugin } from "@nwire/mail";
|
|
10
|
+
*
|
|
11
|
+
* defineApp("my-app", {
|
|
12
|
+
* plugins: [mailPlugin({
|
|
13
|
+
* mailer: smtpMailer({
|
|
14
|
+
* host: process.env.SMTP_HOST,
|
|
15
|
+
* port: Number(process.env.SMTP_PORT),
|
|
16
|
+
* auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
|
|
17
|
+
* }),
|
|
18
|
+
* defaultFrom: '"My App" <noreply@example.com>',
|
|
19
|
+
* })],
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* For local dev:
|
|
23
|
+
*
|
|
24
|
+
* smtpMailer({ host: "localhost", port: 1025 }) // Mailhog (no auth)
|
|
25
|
+
*
|
|
26
|
+
* Run `nwire infra up` and open http://localhost:8025 to inspect sent mail.
|
|
27
|
+
*/
|
|
28
|
+
import nodemailer from "nodemailer";
|
|
29
|
+
import { MailSendError, } from "@nwire/mail";
|
|
30
|
+
/** Build a Mailer backed by Nodemailer's SMTP transport. */
|
|
31
|
+
export function smtpMailer(options) {
|
|
32
|
+
// nodemailer's `createTransport` is union-typed — its SMTPTransportOptions
|
|
33
|
+
// overload accepts `host`, but TS can't always pick it without help.
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
const transporter = options.transporter ?? nodemailer.createTransport({
|
|
36
|
+
host: options.host,
|
|
37
|
+
port: options.port,
|
|
38
|
+
secure: options.secure ?? false,
|
|
39
|
+
auth: options.auth,
|
|
40
|
+
pool: options.pool ?? true,
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
});
|
|
43
|
+
return new NodemailerMailer(transporter);
|
|
44
|
+
}
|
|
45
|
+
class NodemailerMailer {
|
|
46
|
+
transporter;
|
|
47
|
+
constructor(transporter) {
|
|
48
|
+
this.transporter = transporter;
|
|
49
|
+
}
|
|
50
|
+
async send(message) {
|
|
51
|
+
const startedAt = performance.now();
|
|
52
|
+
try {
|
|
53
|
+
const info = await this.transporter.sendMail(toNodemailer(message));
|
|
54
|
+
return {
|
|
55
|
+
messageId: info.messageId ?? "",
|
|
56
|
+
durationMs: performance.now() - startedAt,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new MailSendError(`SMTP send failed: ${err?.message ?? String(err)}`, err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async healthCheck() {
|
|
64
|
+
// verify() resolves true on success; reject otherwise. Cheap, exact.
|
|
65
|
+
await this.transporter.verify();
|
|
66
|
+
}
|
|
67
|
+
async shutdown() {
|
|
68
|
+
this.transporter.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ─── Mapping ────────────────────────────────────────────────────────
|
|
72
|
+
function toNodemailer(message) {
|
|
73
|
+
return {
|
|
74
|
+
from: toAddressField(message.from),
|
|
75
|
+
to: toRecipientField(message.to),
|
|
76
|
+
cc: toRecipientField(message.cc),
|
|
77
|
+
bcc: toRecipientField(message.bcc),
|
|
78
|
+
replyTo: toAddressField(message.replyTo),
|
|
79
|
+
subject: message.subject,
|
|
80
|
+
text: message.text,
|
|
81
|
+
html: message.html,
|
|
82
|
+
headers: message.headers,
|
|
83
|
+
attachments: message.attachments?.map((a) => ({
|
|
84
|
+
filename: a.filename,
|
|
85
|
+
// Nodemailer's `content` accepts string | Buffer | Readable; for
|
|
86
|
+
// Uint8Array we copy into a Buffer to satisfy the typing.
|
|
87
|
+
content: a.content instanceof Uint8Array && !Buffer.isBuffer(a.content)
|
|
88
|
+
? Buffer.from(a.content)
|
|
89
|
+
: a.content,
|
|
90
|
+
path: a.path,
|
|
91
|
+
contentType: a.contentType,
|
|
92
|
+
cid: a.cid,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function toAddressField(addr) {
|
|
97
|
+
if (!addr)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (typeof addr === "string")
|
|
100
|
+
return addr;
|
|
101
|
+
return { name: addr.name ?? "", address: addr.email };
|
|
102
|
+
}
|
|
103
|
+
function toRecipientField(rec) {
|
|
104
|
+
if (!rec)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (typeof rec === "string")
|
|
107
|
+
return rec;
|
|
108
|
+
if (Array.isArray(rec)) {
|
|
109
|
+
return rec.map((r) => typeof r === "string" ? r : { name: r.name ?? "", address: r.email });
|
|
110
|
+
}
|
|
111
|
+
const obj = rec;
|
|
112
|
+
return { name: obj.name ?? "", address: obj.email };
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=mail-nodemailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-nodemailer.js","sourceRoot":"","sources":["../src/mail-nodemailer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/mail-nodemailer",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Nwire — Nodemailer-backed mail adapter. Implements the Mailer contract via nodemailer's transporter (SMTP/Mailhog/Gmail/Office365/...). Health-checked with transporter.verify(); graceful shutdown closes the pool.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adapter",
|
|
7
|
+
"mail",
|
|
8
|
+
"mailhog",
|
|
9
|
+
"nodemailer",
|
|
10
|
+
"nwire",
|
|
11
|
+
"smtp"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/mail-nodemailer.js",
|
|
19
|
+
"types": "./dist/mail-nodemailer.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/mail-nodemailer.js",
|
|
23
|
+
"types": "./dist/mail-nodemailer.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"nodemailer": "^6.9.16",
|
|
31
|
+
"@nwire/mail": "0.7.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.19.9",
|
|
35
|
+
"@types/nodemailer": "^6.4.17",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.1.6",
|
|
38
|
+
"@nwire/test-kit": "0.7.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|