@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 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
+ }