@owlmeans/mailer-smtp 0.1.18-rc.8 → 0.1.18

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/README.md CHANGED
@@ -7,13 +7,13 @@ SMTP transport for the OwlMeans `MailerService` contract, built on [nodemailer](
7
7
  - `makeSmtpMailerService(alias?)` — `MailerService` implementation reading `cfg.smtp`
8
8
  - `SmtpSettings` / `SmtpConfig` — the config block; numbers and booleans also accept their string form, because config values mounted from files always arrive as text
9
9
  - `verify()` — open a session and authenticate without sending, for health checks and tests
10
- - `toTransportOptions()` / `toMailOptions()` — the pure translations into nodemailer's shapes
10
+ - `makeSmtpSettingsModel(smtp).toTransportOptions()` / `.toMailOptions(message)` — the pure translations into nodemailer's shapes; `assertSmtpSettings()` validates the block
11
11
  - Works with any SMTP relay; Mailgun, SES and Postmark all expose one
12
12
 
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- bun add @owlmeans/mailer-smtp
16
+ bun add @owlmeans/mailer-smtp@^0.1.18
17
17
  ```
18
18
 
19
19
  ## Usage
@@ -65,7 +65,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
65
65
  your project's skill store (`.agents/skills/`):
66
66
 
67
67
  ```sh
68
- npx @owlmeans/agent-skills
68
+ npx @owlmeans/agent-skills@^0.1.18
69
69
  ```
70
70
 
71
71
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
- "schemaVersion": 2,
2
+ "schemaVersion": 3,
3
3
  "package": "@owlmeans/mailer-smtp",
4
- "version": "0.1.18-rc.7",
5
- "generatedAt": "2026-08-24T21:14:00.095Z",
4
+ "version": "0.1.18",
5
+ "generatedAt": "2026-10-28T00:31:34.717Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,6 +8,9 @@ metadata:
8
8
 
9
9
  # Using `@owlmeans/mailer-smtp`
10
10
 
11
+ **Install:** `"@owlmeans/mailer-smtp": "^0.1.18"` in `dependencies` — it depends on
12
+ `nodemailer` itself, so a consumer declares nothing extra
13
+
11
14
  SMTP transport implementing `@owlmeans/mailer`'s `MailerService`, built on `nodemailer` (the
12
15
  zero-dependency de-facto standard for Node SMTP). Reads `ctx.cfg.smtp`. Works against any relay —
13
16
  Mailgun, SES, Postmark all expose SMTP.
@@ -21,7 +24,7 @@ Mailgun, SES, Postmark all expose SMTP.
21
24
  | `SmtpSettings` / `SmtpConfig` | interface | `cfg.smtp` block |
22
25
  | `SMTP_MAILER` | const | `'smtp-mailer'` |
23
26
  | `SMTP_DEFAULT_PORT` | const | `465` |
24
- | `toTransportOptions` / `toMailOptions` | fn | Pure translations into nodemailer's shapes; test these, not the socket |
27
+ | `makeSmtpSettingsModel(smtp).toTransportOptions()` / `.toMailOptions(message)` | model | Pure translations into nodemailer's shapes; test these, not the socket |
25
28
 
26
29
  ## Config shape
27
30
 
@@ -59,16 +62,20 @@ context.registerService(makeSmtpMailerService(MAILER_SERVICE))
59
62
  ```
60
63
 
61
64
  Register under `MAILER_SERVICE` so platform code — `OtpService` above all — resolves the mailer
62
- without knowing the transport. Select between transports on config, not on a build flag:
65
+ without knowing the transport. A local-only console transport is an explicit mode; it is never a
66
+ production fallback:
63
67
 
64
68
  ```ts
65
- context.registerService(
66
- cfg.smtp?.host != null && cfg.smtp.host !== ''
67
- ? makeSmtpMailerService(MAILER_SERVICE)
68
- : makeConsoleMailerService(MAILER_SERVICE)
69
- )
69
+ if (cfg.iamMailer.mode !== 'smtp') throw new SyntaxError('production requires smtp mode')
70
+ context.registerService(makeSmtpMailerService(MAILER_SERVICE, {
71
+ authenticated: true,
72
+ verifyOnInit: true,
73
+ }))
70
74
  ```
71
75
 
76
+ Authenticated SMTP requires a host, from address, user and password; `verifyOnInit` makes an
77
+ unusable relay fail context startup rather than exposing a later login code through a console log.
78
+
72
79
  ## Ports and TLS
73
80
 
74
81
  | Port | Mode | `secure` |
@@ -84,19 +91,30 @@ context.registerService(
84
91
  - The `From` domain must be verified with the relay; the local part is free-form.
85
92
  - Deliberately **unpooled** — a pooled transport holds its socket and keeps a short-lived process
86
93
  alive. Add pooling only with a matching lifecycle.
87
- - Errors are rethrown prefixed with the service alias and the server's own reply
88
- (`code`/`responseCode`/`response`). The password never reaches a message or a log.
94
+ - Relay errors are rethrown prefixed with the service alias and the server's own reply
95
+ (`code`/`responseCode`/`response`). The password never reaches a message or a log. A missing
96
+ `cfg.smtp.host` is the other failure and it is a `SyntaxError`, raised before any socket.
89
97
  - `verify()` authenticates without submitting a message — right for health checks; not proof that
90
98
  the relay accepts *a message* from your sender.
91
- - Never register this in unit tests — use `makeConsoleMailerService`. `toMailOptions` plus
92
- nodemailer's own `jsonTransport` cover envelope assertions without a socket.
99
+ - Production mail selection must be explicit and use `authenticated: true, verifyOnInit: true`.
100
+ Console mail is for local fixtures/development and must never be selected because SMTP settings
101
+ were absent or malformed.
102
+ - `toMailOptions` maps `MailMessage.attachments` to nodemailer's `attachments` as
103
+ `{ filename, content: Buffer, contentType? }` — the bytes already decoded by
104
+ `mailAttachmentHelper.bytesOf`, so no `encoding` is left for nodemailer to guess; absent or empty
105
+ = no `attachments` key.
106
+ - Never register this in unit tests — use `makeConsoleMailerService`.
107
+ `makeSmtpSettingsModel(smtp).toMailOptions` plus nodemailer's own `jsonTransport` cover envelope
108
+ assertions without a socket.
93
109
  - **Never authenticate with a deliberately wrong password against a live relay.** Repeated failed
94
110
  logins trip the provider's brute-force protection and lock the shared credential for every
95
111
  environment using it — Mailgun then answers `535 Authentication failed` to the correct password
96
112
  too, and the credential has to be reset in its dashboard. Provoke transport errors with an
97
113
  unreachable socket (`host: '127.0.0.1', port: '1'`) instead.
98
- - The live spec (`tests/send.spec.ts`) is gated on the SMTP block in `/.env.example` and **delivers
99
- real mail** when the gate is open. Empty variables = skip, never a failure.
114
+ - The live spec (`tests/send.spec.ts`) is gated by `gateHelper.smtpGate()` from
115
+ `@owlmeans/test-integration` on `SMTP_HOST` / `SMTP_USER` / `SMTP_PASSWORD` / `SMTP_FROM` /
116
+ `SMTP_TEST_TO` (with `SMTP_PORT` and `SMTP_SECURE` optional), and **delivers real mail** when the
117
+ gate is open. Empty variables = skip with a printed reason, never a failure.
100
118
  - Rollup-bundling for a container image works with the default `preferBuiltins: true` node-resolve
101
119
  setup; nodemailer's dynamic requires do not need to be externalized.
102
120
 
@@ -105,10 +123,14 @@ context.registerService(
105
123
  - [nodemailer](https://www.npmjs.com/package/nodemailer) — v9.x, MIT-0, **zero dependencies**, the
106
124
  de-facto standard (~10.8k dependents). Ships no types; `@types/nodemailer` (v8.x) supplies them.
107
125
  CJS — import the default (`import nodemailer from 'nodemailer'`), not named members, so both Bun
108
- and Rollup's commonjs interop resolve it. Verified working under Bun and inside a Rollup CJS
109
- bundle (2026-08).
126
+ and Rollup's commonjs interop resolve it; it works under Bun and inside a Rollup CJS bundle.
110
127
  - [Mailgun — Send via SMTP](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/send-smtp)
111
128
  — hosts `smtp.mailgun.org` / `smtp.eu.mailgun.org`; port 465 requires TLS, ports 25/587/2525 start
112
129
  plain and upgrade via STARTTLS. Credentials are **per sending domain**, managed under that domain's
113
130
  SMTP settings — not the account login. `X-Mailgun-Drop-Message: yes` submits in test mode (accepted,
114
131
  never delivered); other `X-Mailgun-*` headers cover tagging, DKIM, tracking and required TLS.
132
+
133
+ ## Related
134
+
135
+ - [[mailer]] — the `MailerService` contract and the console transport
136
+ - [[server-mailer-mailgun]] — the same contract over Mailgun's HTTP API instead of SMTP
@@ -0,0 +1,4 @@
1
+ import type { SmtpMailerOptions, SmtpSettings } from './types.js';
2
+ /** Validate the settings required by an authenticated production relay. */
3
+ export declare const assertSmtpSettings: (smtp: SmtpSettings | undefined, alias: string, opts?: SmtpMailerOptions) => SmtpSettings;
4
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEjE,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,SACvB,YAAY,GAAG,SAAS,SAAS,MAAM,SAAQ,iBAAiB,KACrE,YASF,CAAA"}
@@ -0,0 +1,10 @@
1
+ /** Validate the settings required by an authenticated production relay. */
2
+ export const assertSmtpSettings = (smtp, alias, opts = {}) => {
3
+ const required = opts.authenticated === true ? ['host', 'from', 'user', 'pass'] : ['host'];
4
+ const missing = required.filter(key => smtp?.[key] == null || String(smtp[key]).trim() === '');
5
+ if (missing.length > 0) {
6
+ throw new SyntaxError(`${alias}: cfg.smtp.${missing.join(', cfg.smtp.')} is not configured`);
7
+ }
8
+ return smtp;
9
+ };
10
+ //# sourceMappingURL=assert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.js","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,IAA8B,EAAE,KAAa,EAAE,IAAI,GAAsB,EAAE,EAC7D,EAAE;IAChB,MAAM,QAAQ,GACZ,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;IAC3E,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC9F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,cAAc,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAA;IAC9F,CAAC;IAED,OAAO,IAAoB,CAAA;AAC7B,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export type * from './types.js';
2
+ export type * from './settings/types.js';
2
3
  export * from './consts.js';
3
4
  export * from './service.js';
5
+ export * from './settings.js';
6
+ export * from './assert.js';
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,mBAAmB,qBAAqB,CAAA;AACxC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
package/build/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './consts.js';
2
2
  export * from './service.js';
3
+ export * from './settings.js';
4
+ export * from './assert.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
@@ -1,17 +1,4 @@
1
- import type { MailMessage } from '@owlmeans/mailer';
2
- import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js';
3
- import type Mail from 'nodemailer/lib/mailer/index.js';
4
- import type { SmtpMailerService, SmtpSettings } from './types.js';
5
- /**
6
- * Translate the settings block into nodemailer's transport options.
7
- *
8
- * Deliberately unpooled: a pooled transport holds its socket open and would keep a
9
- * short-lived process alive, and the traffic this serves — login codes — is far below
10
- * the volume that makes pooling worth that.
11
- */
12
- export declare const toTransportOptions: (smtp: SmtpSettings) => SMTPTransport.Options;
13
- /** Translate a provider-agnostic message into nodemailer's envelope, applying the config defaults. */
14
- export declare const toMailOptions: (smtp: SmtpSettings, message: MailMessage) => Mail.Options;
1
+ import type { SmtpMailerOptions, SmtpMailerService } from './types.js';
15
2
  /**
16
3
  * SMTP transport for the `MailerService` contract. Reads `cfg.smtp` and keeps one
17
4
  * nodemailer transporter per service instance.
@@ -19,5 +6,5 @@ export declare const toMailOptions: (smtp: SmtpSettings, message: MailMessage) =
19
6
  * Register it under `MAILER_SERVICE` so callers — the email-OTP plugin above all —
20
7
  * resolve it without knowing which transport is in play.
21
8
  */
22
- export declare const makeSmtpMailerService: (alias?: string) => SmtpMailerService;
9
+ export declare const makeSmtpMailerService: (alias?: string, opts?: SmtpMailerOptions) => SmtpMailerService;
23
10
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAInD,OAAO,KAAK,aAAa,MAAM,wCAAwC,CAAA;AACvE,OAAO,KAAK,IAAI,MAAM,gCAAgC,CAAA;AAEtD,OAAO,KAAK,EAAc,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAgB7E;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,SAAU,YAAY,KAAG,aAAa,CAAC,OASpE,CAAA;AAEF,sGAAsG;AACtG,eAAO,MAAM,aAAa,SAAU,YAAY,WAAW,WAAW,KAAG,IAAI,CAAC,OAa7E,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,sBAA0B,iBAsD3D,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAc,iBAAiB,EAAE,iBAAiB,EAAgB,MAAM,YAAY,CAAA;AAEhG;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,0BACL,iBAAiB,KAC3C,iBAsDF,CAAA"}
package/build/service.js CHANGED
@@ -1,50 +1,8 @@
1
1
  import { createService } from '@owlmeans/context';
2
2
  import nodemailer from 'nodemailer';
3
- import { SMTP_DEFAULT_PORT, SMTP_MAILER } from './consts.js';
4
- const toNumber = (value, def) => {
5
- if (value == null || value === '')
6
- return def;
7
- const num = typeof value === 'number' ? value : Number(value);
8
- return Number.isFinite(num) ? num : def;
9
- };
10
- const toBoolean = (value, def) => {
11
- if (value == null || value === '')
12
- return def;
13
- if (typeof value === 'boolean')
14
- return value;
15
- return ['1', 'on', 'true', 'yes'].includes(value.trim().toLowerCase());
16
- };
17
- /**
18
- * Translate the settings block into nodemailer's transport options.
19
- *
20
- * Deliberately unpooled: a pooled transport holds its socket open and would keep a
21
- * short-lived process alive, and the traffic this serves — login codes — is far below
22
- * the volume that makes pooling worth that.
23
- */
24
- export const toTransportOptions = (smtp) => ({
25
- host: smtp.host,
26
- port: toNumber(smtp.port, SMTP_DEFAULT_PORT),
27
- secure: toBoolean(smtp.secure, true),
28
- tls: { rejectUnauthorized: toBoolean(smtp.rejectUnauthorized, true) },
29
- ...(smtp.timeout != null ? { connectionTimeout: toNumber(smtp.timeout, 0) } : {}),
30
- ...(smtp.user != null && smtp.user !== ''
31
- ? { auth: { user: smtp.user, pass: smtp.pass ?? '' } }
32
- : {}),
33
- });
34
- /** Translate a provider-agnostic message into nodemailer's envelope, applying the config defaults. */
35
- export const toMailOptions = (smtp, message) => {
36
- const headers = { ...smtp.headers, ...message.headers };
37
- const replyTo = message.replyTo ?? smtp.replyTo;
38
- return {
39
- from: message.from ?? smtp.from,
40
- to: message.to,
41
- subject: message.subject,
42
- ...(message.text != null ? { text: message.text } : {}),
43
- ...(message.html != null ? { html: message.html } : {}),
44
- ...(replyTo != null ? { replyTo } : {}),
45
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
46
- };
47
- };
3
+ import { SMTP_MAILER } from './consts.js';
4
+ import { assertSmtpSettings } from './assert.js';
5
+ import { makeSmtpSettingsModel } from './settings.js';
48
6
  /**
49
7
  * SMTP transport for the `MailerService` contract. Reads `cfg.smtp` and keeps one
50
8
  * nodemailer transporter per service instance.
@@ -52,17 +10,13 @@ export const toMailOptions = (smtp, message) => {
52
10
  * Register it under `MAILER_SERVICE` so callers — the email-OTP plugin above all —
53
11
  * resolve it without knowing which transport is in play.
54
12
  */
55
- export const makeSmtpMailerService = (alias = SMTP_MAILER) => {
13
+ export const makeSmtpMailerService = (alias = SMTP_MAILER, opts = {}) => {
56
14
  let transport = null;
57
15
  const settings = () => {
58
16
  const ctx = service.assertCtx(alias);
59
- const smtp = ctx.cfg.smtp;
60
- if (smtp?.host == null || smtp.host === '') {
61
- throw new SyntaxError(`${alias}: cfg.smtp.host is not configured`);
62
- }
63
- return smtp;
17
+ return assertSmtpSettings(ctx.cfg.smtp, alias, opts);
64
18
  };
65
- const transporter = () => transport ??= nodemailer.createTransport(toTransportOptions(settings()));
19
+ const transporter = () => transport ??= nodemailer.createTransport(makeSmtpSettingsModel(settings()).toTransportOptions());
66
20
  // Credentials never reach the message: nodemailer reports the server's own reply only.
67
21
  const fail = (action, error) => {
68
22
  const err = error;
@@ -72,7 +26,7 @@ export const makeSmtpMailerService = (alias = SMTP_MAILER) => {
72
26
  };
73
27
  const service = createService(alias, {
74
28
  send: async (message) => {
75
- const options = toMailOptions(settings(), message);
29
+ const options = makeSmtpSettingsModel(settings()).toMailOptions(message);
76
30
  try {
77
31
  await transporter().sendMail(options);
78
32
  }
@@ -93,6 +47,13 @@ export const makeSmtpMailerService = (alias = SMTP_MAILER) => {
93
47
  transport?.close();
94
48
  transport = null;
95
49
  },
50
+ }, current => async () => {
51
+ // `configure()` resolves mounted config files before services initialize, so this validates
52
+ // the resolved values and, when requested, verifies the relay without sending a message.
53
+ settings();
54
+ if (opts.verifyOnInit === true)
55
+ await current.verify();
56
+ current.initialized = true;
96
57
  });
97
58
  return service;
98
59
  };
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,UAAU,MAAM,YAAY,CAAA;AAEnC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAGrD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,KAAK,GAAG,WAAW,EAAE,IAAI,GAAsB,EAAE,EAC9B,EAAE;IACrB,IAAI,SAAS,GAA4C,IAAI,CAAA;IAE7D,MAAM,QAAQ,GAAG,GAAiB,EAAE;QAClC,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAwC,KAAK,CAAC,CAAA;QAC3E,OAAO,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;IACtD,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,GAAqC,EAAE,CACzD,SAAS,KAAK,UAAU,CAAC,eAAe,CAAC,qBAAqB,CAAC,QAAQ,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAElG,uFAAuF;IACvF,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,KAAc,EAAS,EAAE;QACrD,MAAM,GAAG,GAAG,KAAsF,CAAA;QAClG,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC;aACrE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAExD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC9E,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,aAAa,CAAoB,KAAK,EAAE;QACtD,IAAI,EAAE,KAAK,EAAE,OAAoB,EAAiB,EAAE;YAClD,MAAM,OAAO,GAAG,qBAAqB,CAAC,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;YAExE,IAAI,CAAC;gBACH,MAAM,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YACvC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,qBAAqB,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;YAChD,CAAC;QACH,CAAC;QAED,MAAM,EAAE,KAAK,IAAmB,EAAE;YAChC,IAAI,CAAC;gBACH,MAAM,WAAW,EAAE,CAAC,MAAM,EAAE,CAAA;YAC9B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAA;YACrD,CAAC;YAED,OAAO,IAAI,CAAA;QACb,CAAC;QAED,KAAK,EAAE,KAAK,IAAmB,EAAE;YAC/B,SAAS,EAAE,KAAK,EAAE,CAAA;YAClB,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC;KACF,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE;QACvB,4FAA4F;QAC5F,yFAAyF;QACzF,QAAQ,EAAE,CAAA;QACV,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI;YAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAA;QACtD,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { MailMessage } from '@owlmeans/mailer';
2
+ import type { SendMailOptions, SMTPTransportOptions } from 'nodemailer';
3
+ /** One `cfg.smtp` block, translated into what nodemailer takes. */
4
+ export interface SmtpSettingsModel {
5
+ /**
6
+ * Translate the settings block into nodemailer's transport options.
7
+ *
8
+ * Deliberately unpooled: a pooled transport holds its socket open and would keep a
9
+ * short-lived process alive, and the traffic this serves — login codes — is far below
10
+ * the volume that makes pooling worth that.
11
+ */
12
+ toTransportOptions: () => SMTPTransportOptions;
13
+ /**
14
+ * Translate a provider-agnostic message into nodemailer's envelope, applying the config defaults.
15
+ * Attachments are handed over as decoded bytes with their name and declared MIME type.
16
+ */
17
+ toMailOptions: (message: MailMessage) => SendMailOptions;
18
+ }
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/settings/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEvE,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;IAC9C;;;OAGG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,eAAe,CAAA;CACzD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/settings/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { SmtpSettings } from './types.js';
2
+ import type { SmtpSettingsModel } from './settings/types.js';
3
+ export declare const makeSmtpSettingsModel: (smtp: SmtpSettings) => SmtpSettingsModel;
4
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgB5D,eAAO,MAAM,qBAAqB,SAAU,YAAY,KAAG,iBAyC1D,CAAA"}
@@ -0,0 +1,53 @@
1
+ import { mailAttachmentHelper } from '@owlmeans/mailer';
2
+ import { SMTP_DEFAULT_PORT } from './consts.js';
3
+ const toNumber = (value, def) => {
4
+ if (value == null || value === '')
5
+ return def;
6
+ const num = typeof value === 'number' ? value : Number(value);
7
+ return Number.isFinite(num) ? num : def;
8
+ };
9
+ const toBoolean = (value, def) => {
10
+ if (value == null || value === '')
11
+ return def;
12
+ if (typeof value === 'boolean')
13
+ return value;
14
+ return ['1', 'on', 'true', 'yes'].includes(value.trim().toLowerCase());
15
+ };
16
+ export const makeSmtpSettingsModel = (smtp) => {
17
+ const toTransportOptions = () => ({
18
+ host: smtp.host,
19
+ port: toNumber(smtp.port, SMTP_DEFAULT_PORT),
20
+ secure: toBoolean(smtp.secure, true),
21
+ tls: { rejectUnauthorized: toBoolean(smtp.rejectUnauthorized, true) },
22
+ ...(smtp.timeout != null ? { connectionTimeout: toNumber(smtp.timeout, 0) } : {}),
23
+ ...(smtp.user != null && smtp.user !== ''
24
+ ? { auth: { user: smtp.user, pass: smtp.pass ?? '' } }
25
+ : {}),
26
+ });
27
+ /** One attachment as nodemailer takes it: the decoded bytes, so no encoding is left to guess. */
28
+ const toAttachment = (attachment) => {
29
+ const bytes = mailAttachmentHelper.bytesOf(attachment);
30
+ return {
31
+ filename: attachment.filename,
32
+ content: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
33
+ ...(attachment.contentType != null ? { contentType: attachment.contentType } : {}),
34
+ };
35
+ };
36
+ const toMailOptions = (message) => {
37
+ const headers = { ...smtp.headers, ...message.headers };
38
+ const replyTo = message.replyTo ?? smtp.replyTo;
39
+ const attachments = message.attachments ?? [];
40
+ return {
41
+ from: message.from ?? smtp.from,
42
+ to: message.to,
43
+ subject: message.subject,
44
+ ...(message.text != null ? { text: message.text } : {}),
45
+ ...(message.html != null ? { html: message.html } : {}),
46
+ ...(replyTo != null ? { replyTo } : {}),
47
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
48
+ ...(attachments.length > 0 ? { attachments: attachments.map(toAttachment) } : {}),
49
+ };
50
+ };
51
+ return { toTransportOptions, toMailOptions };
52
+ };
53
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -36,4 +36,10 @@ export interface SmtpMailerService extends MailerService {
36
36
  /** Release a pooled connection. A no-op for the default, non-pooled transport. */
37
37
  close: () => Promise<void>;
38
38
  }
39
+ export interface SmtpMailerOptions {
40
+ /** Require a default sender and username/password authentication. */
41
+ authenticated?: boolean;
42
+ /** Authenticate with the relay while the context initializes. */
43
+ verifyOnInit?: boolean;
44
+ }
39
45
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACrC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,IAAI,EAAE,YAAY,CAAA;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,oGAAoG;IACpG,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,kFAAkF;IAClF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACrC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,IAAI,EAAE,YAAY,CAAA;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,oGAAoG;IACpG,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,kFAAkF;IAClF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/mailer-smtp",
3
- "version": "0.1.18-rc.8",
3
+ "version": "0.1.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,18 +22,17 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/context": "^0.1.18-rc.6",
26
- "@owlmeans/mailer": "^0.1.18-rc.6",
27
- "@owlmeans/server-context": "^0.1.18-rc.8",
28
- "nodemailer": "^9.0.5"
25
+ "@owlmeans/context": "^0.1.18",
26
+ "@owlmeans/mailer": "^0.1.18",
27
+ "@owlmeans/server-context": "^0.1.18",
28
+ "nodemailer": "^10.0.14"
29
29
  },
30
30
  "devDependencies": {
31
- "@owlmeans/dep-config": "workspace:*",
32
- "@owlmeans/test": "^0.1.18-rc.6",
33
- "@owlmeans/test-integration": "^0.1.18-rc.6",
34
- "@types/bun": "^1.4.0",
35
- "@types/node": "^26.1.0",
36
- "@types/nodemailer": "^8.0.1",
31
+ "@owlmeans/dep-config": "^0.1.18",
32
+ "@owlmeans/test": "^0.1.18",
33
+ "@owlmeans/test-integration": "^0.1.18",
34
+ "@types/bun": "^1.4.2",
35
+ "@types/node": "^26.6.4",
37
36
  "nodemon": "^3.1.14",
38
37
  "typescript": "^7.0.2"
39
38
  },
package/src/assert.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { SmtpMailerOptions, SmtpSettings } from './types.js'
2
+
3
+ /** Validate the settings required by an authenticated production relay. */
4
+ export const assertSmtpSettings = (
5
+ smtp: SmtpSettings | undefined, alias: string, opts: SmtpMailerOptions = {}
6
+ ): SmtpSettings => {
7
+ const required: Array<keyof Pick<SmtpSettings, 'host' | 'from' | 'user' | 'pass'>> =
8
+ opts.authenticated === true ? ['host', 'from', 'user', 'pass'] : ['host']
9
+ const missing = required.filter(key => smtp?.[key] == null || String(smtp[key]).trim() === '')
10
+ if (missing.length > 0) {
11
+ throw new SyntaxError(`${alias}: cfg.smtp.${missing.join(', cfg.smtp.')} is not configured`)
12
+ }
13
+
14
+ return smtp as SmtpSettings
15
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export type * from './types.js'
2
+ export type * from './settings/types.js'
2
3
  export * from './consts.js'
3
4
  export * from './service.js'
5
+ export * from './settings.js'
6
+ export * from './assert.js'
package/src/service.ts CHANGED
@@ -2,59 +2,11 @@ import { createService } from '@owlmeans/context'
2
2
  import type { MailMessage } from '@owlmeans/mailer'
3
3
  import type { ServerContext } from '@owlmeans/server-context'
4
4
  import nodemailer from 'nodemailer'
5
- import type { Transporter } from 'nodemailer'
6
- import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js'
7
- import type Mail from 'nodemailer/lib/mailer/index.js'
8
- import { SMTP_DEFAULT_PORT, SMTP_MAILER } from './consts.js'
9
- import type { SmtpConfig, SmtpMailerService, SmtpSettings } from './types.js'
10
-
11
- const toNumber = (value: number | string | undefined, def: number): number => {
12
- if (value == null || value === '') return def
13
- const num = typeof value === 'number' ? value : Number(value)
14
-
15
- return Number.isFinite(num) ? num : def
16
- }
17
-
18
- const toBoolean = (value: boolean | string | undefined, def: boolean): boolean => {
19
- if (value == null || value === '') return def
20
- if (typeof value === 'boolean') return value
21
-
22
- return ['1', 'on', 'true', 'yes'].includes(value.trim().toLowerCase())
23
- }
24
-
25
- /**
26
- * Translate the settings block into nodemailer's transport options.
27
- *
28
- * Deliberately unpooled: a pooled transport holds its socket open and would keep a
29
- * short-lived process alive, and the traffic this serves — login codes — is far below
30
- * the volume that makes pooling worth that.
31
- */
32
- export const toTransportOptions = (smtp: SmtpSettings): SMTPTransport.Options => ({
33
- host: smtp.host,
34
- port: toNumber(smtp.port, SMTP_DEFAULT_PORT),
35
- secure: toBoolean(smtp.secure, true),
36
- tls: { rejectUnauthorized: toBoolean(smtp.rejectUnauthorized, true) },
37
- ...(smtp.timeout != null ? { connectionTimeout: toNumber(smtp.timeout, 0) } : {}),
38
- ...(smtp.user != null && smtp.user !== ''
39
- ? { auth: { user: smtp.user, pass: smtp.pass ?? '' } }
40
- : {}),
41
- })
42
-
43
- /** Translate a provider-agnostic message into nodemailer's envelope, applying the config defaults. */
44
- export const toMailOptions = (smtp: SmtpSettings, message: MailMessage): Mail.Options => {
45
- const headers = { ...smtp.headers, ...message.headers }
46
- const replyTo = message.replyTo ?? smtp.replyTo
47
-
48
- return {
49
- from: message.from ?? smtp.from,
50
- to: message.to,
51
- subject: message.subject,
52
- ...(message.text != null ? { text: message.text } : {}),
53
- ...(message.html != null ? { html: message.html } : {}),
54
- ...(replyTo != null ? { replyTo } : {}),
55
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
56
- }
57
- }
5
+ import type { SMTPSentMessageInfo, Transporter } from 'nodemailer'
6
+ import { SMTP_MAILER } from './consts.js'
7
+ import { assertSmtpSettings } from './assert.js'
8
+ import { makeSmtpSettingsModel } from './settings.js'
9
+ import type { SmtpConfig, SmtpMailerOptions, SmtpMailerService, SmtpSettings } from './types.js'
58
10
 
59
11
  /**
60
12
  * SMTP transport for the `MailerService` contract. Reads `cfg.smtp` and keeps one
@@ -63,22 +15,18 @@ export const toMailOptions = (smtp: SmtpSettings, message: MailMessage): Mail.Op
63
15
  * Register it under `MAILER_SERVICE` so callers — the email-OTP plugin above all —
64
16
  * resolve it without knowing which transport is in play.
65
17
  */
66
- export const makeSmtpMailerService = (alias = SMTP_MAILER): SmtpMailerService => {
67
- let transport: Transporter<SMTPTransport.SentMessageInfo> | null = null
18
+ export const makeSmtpMailerService = (
19
+ alias = SMTP_MAILER, opts: SmtpMailerOptions = {}
20
+ ): SmtpMailerService => {
21
+ let transport: Transporter<SMTPSentMessageInfo> | null = null
68
22
 
69
23
  const settings = (): SmtpSettings => {
70
24
  const ctx = service.assertCtx<SmtpConfig, ServerContext<SmtpConfig>>(alias)
71
- const smtp = ctx.cfg.smtp
72
-
73
- if (smtp?.host == null || smtp.host === '') {
74
- throw new SyntaxError(`${alias}: cfg.smtp.host is not configured`)
75
- }
76
-
77
- return smtp
25
+ return assertSmtpSettings(ctx.cfg.smtp, alias, opts)
78
26
  }
79
27
 
80
- const transporter = (): Transporter<SMTPTransport.SentMessageInfo> =>
81
- transport ??= nodemailer.createTransport(toTransportOptions(settings()))
28
+ const transporter = (): Transporter<SMTPSentMessageInfo> =>
29
+ transport ??= nodemailer.createTransport(makeSmtpSettingsModel(settings()).toTransportOptions())
82
30
 
83
31
  // Credentials never reach the message: nodemailer reports the server's own reply only.
84
32
  const fail = (action: string, error: unknown): never => {
@@ -91,7 +39,7 @@ export const makeSmtpMailerService = (alias = SMTP_MAILER): SmtpMailerService =>
91
39
 
92
40
  const service = createService<SmtpMailerService>(alias, {
93
41
  send: async (message: MailMessage): Promise<void> => {
94
- const options = toMailOptions(settings(), message)
42
+ const options = makeSmtpSettingsModel(settings()).toMailOptions(message)
95
43
 
96
44
  try {
97
45
  await transporter().sendMail(options)
@@ -114,6 +62,12 @@ export const makeSmtpMailerService = (alias = SMTP_MAILER): SmtpMailerService =>
114
62
  transport?.close()
115
63
  transport = null
116
64
  },
65
+ }, current => async () => {
66
+ // `configure()` resolves mounted config files before services initialize, so this validates
67
+ // the resolved values and, when requested, verifies the relay without sending a message.
68
+ settings()
69
+ if (opts.verifyOnInit === true) await current.verify()
70
+ current.initialized = true
117
71
  })
118
72
 
119
73
  return service
@@ -0,0 +1,19 @@
1
+ import type { MailMessage } from '@owlmeans/mailer'
2
+ import type { SendMailOptions, SMTPTransportOptions } from 'nodemailer'
3
+
4
+ /** One `cfg.smtp` block, translated into what nodemailer takes. */
5
+ export interface SmtpSettingsModel {
6
+ /**
7
+ * Translate the settings block into nodemailer's transport options.
8
+ *
9
+ * Deliberately unpooled: a pooled transport holds its socket open and would keep a
10
+ * short-lived process alive, and the traffic this serves — login codes — is far below
11
+ * the volume that makes pooling worth that.
12
+ */
13
+ toTransportOptions: () => SMTPTransportOptions
14
+ /**
15
+ * Translate a provider-agnostic message into nodemailer's envelope, applying the config defaults.
16
+ * Attachments are handed over as decoded bytes with their name and declared MIME type.
17
+ */
18
+ toMailOptions: (message: MailMessage) => SendMailOptions
19
+ }
@@ -0,0 +1,63 @@
1
+ import { mailAttachmentHelper } from '@owlmeans/mailer'
2
+ import type { MailAttachment, MailMessage } from '@owlmeans/mailer'
3
+ import type { SendMailOptions, SMTPTransportOptions } from 'nodemailer'
4
+ import { SMTP_DEFAULT_PORT } from './consts.js'
5
+ import type { SmtpSettings } from './types.js'
6
+ import type { SmtpSettingsModel } from './settings/types.js'
7
+
8
+ const toNumber = (value: number | string | undefined, def: number): number => {
9
+ if (value == null || value === '') return def
10
+ const num = typeof value === 'number' ? value : Number(value)
11
+
12
+ return Number.isFinite(num) ? num : def
13
+ }
14
+
15
+ const toBoolean = (value: boolean | string | undefined, def: boolean): boolean => {
16
+ if (value == null || value === '') return def
17
+ if (typeof value === 'boolean') return value
18
+
19
+ return ['1', 'on', 'true', 'yes'].includes(value.trim().toLowerCase())
20
+ }
21
+
22
+ export const makeSmtpSettingsModel = (smtp: SmtpSettings): SmtpSettingsModel => {
23
+ const toTransportOptions = (): SMTPTransportOptions => ({
24
+ host: smtp.host,
25
+ port: toNumber(smtp.port, SMTP_DEFAULT_PORT),
26
+ secure: toBoolean(smtp.secure, true),
27
+ tls: { rejectUnauthorized: toBoolean(smtp.rejectUnauthorized, true) },
28
+ ...(smtp.timeout != null ? { connectionTimeout: toNumber(smtp.timeout, 0) } : {}),
29
+ ...(smtp.user != null && smtp.user !== ''
30
+ ? { auth: { user: smtp.user, pass: smtp.pass ?? '' } }
31
+ : {}),
32
+ })
33
+
34
+ /** One attachment as nodemailer takes it: the decoded bytes, so no encoding is left to guess. */
35
+ const toAttachment = (attachment: MailAttachment): NonNullable<SendMailOptions['attachments']>[number] => {
36
+ const bytes = mailAttachmentHelper.bytesOf(attachment)
37
+
38
+ return {
39
+ filename: attachment.filename,
40
+ content: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
41
+ ...(attachment.contentType != null ? { contentType: attachment.contentType } : {}),
42
+ }
43
+ }
44
+
45
+ const toMailOptions = (message: MailMessage): SendMailOptions => {
46
+ const headers = { ...smtp.headers, ...message.headers }
47
+ const replyTo = message.replyTo ?? smtp.replyTo
48
+ const attachments = message.attachments ?? []
49
+
50
+ return {
51
+ from: message.from ?? smtp.from,
52
+ to: message.to,
53
+ subject: message.subject,
54
+ ...(message.text != null ? { text: message.text } : {}),
55
+ ...(message.html != null ? { html: message.html } : {}),
56
+ ...(replyTo != null ? { replyTo } : {}),
57
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
58
+ ...(attachments.length > 0 ? { attachments: attachments.map(toAttachment) } : {}),
59
+ }
60
+ }
61
+
62
+ return { toTransportOptions, toMailOptions }
63
+ }
package/src/types.ts CHANGED
@@ -39,3 +39,10 @@ export interface SmtpMailerService extends MailerService {
39
39
  /** Release a pooled connection. A no-op for the default, non-pooled transport. */
40
40
  close: () => Promise<void>
41
41
  }
42
+
43
+ export interface SmtpMailerOptions {
44
+ /** Require a default sender and username/password authentication. */
45
+ authenticated?: boolean
46
+ /** Authenticate with the relay while the context initializes. */
47
+ verifyOnInit?: boolean
48
+ }
package/tests/context.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { smtpGate } from '@owlmeans/test-integration'
1
+
2
2
  import type { IntegrationGate, SmtpEnv } from '@owlmeans/test-integration'
3
3
  import { config, makeServerContext } from '@owlmeans/server-context'
4
4
  import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
5
5
  import { MAILER_SERVICE } from '@owlmeans/mailer'
6
6
  import { makeSmtpMailerService } from '@owlmeans/mailer-smtp'
7
7
  import type { SmtpConfig, SmtpMailerService, SmtpSettings } from '@owlmeans/mailer-smtp'
8
+ import { gateHelper } from '@owlmeans/test-integration'
8
9
 
9
10
  /**
10
11
  * Live relay gate. Every variable empty means the delivery specs self-skip with a
11
12
  * printed reason — never a failure. When it is open these specs send REAL mail, so
12
13
  * `SMTP_TEST_TO` is required rather than optional.
13
14
  */
14
- export const gate: IntegrationGate<SmtpEnv> = smtpGate()
15
+ export const gate: IntegrationGate<SmtpEnv> = gateHelper.smtpGate()
15
16
 
16
17
  /** Never logged, never asserted on — only handed to the transport. */
17
18
  export const settings = (): SmtpSettings => ({
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import nodemailer from 'nodemailer'
3
- import { SMTP_DEFAULT_PORT, toMailOptions, toTransportOptions } from '@owlmeans/mailer-smtp'
3
+ import {
4
+ assertSmtpSettings, makeSmtpSettingsModel, SMTP_DEFAULT_PORT,
5
+ } from '@owlmeans/mailer-smtp'
4
6
  import type { SmtpSettings } from '@owlmeans/mailer-smtp'
5
7
 
6
8
  const base: SmtpSettings = {
@@ -15,8 +17,16 @@ const base: SmtpSettings = {
15
17
  * and let nodemailer itself build the envelope through its bundled `jsonTransport`.
16
18
  */
17
19
  describe('@owlmeans/mailer-smtp — transport options', () => {
20
+ test('authenticated mode requires host, from, user, and password', () => {
21
+ expect(() => assertSmtpSettings(base, 'mailer', { authenticated: true })).not.toThrow()
22
+ for (const field of ['host', 'from', 'user', 'pass'] as const) {
23
+ expect(() => assertSmtpSettings({ ...base, [field]: '' }, 'mailer', { authenticated: true }))
24
+ .toThrow(`cfg.smtp.${field}`)
25
+ }
26
+ })
27
+
18
28
  test('defaults to implicit TLS on 465 with certificate verification', () => {
19
- const options = toTransportOptions(base)
29
+ const options = makeSmtpSettingsModel(base).toTransportOptions()
20
30
 
21
31
  expect(options.port).toBe(SMTP_DEFAULT_PORT)
22
32
  expect(options.secure).toBe(true)
@@ -25,9 +35,9 @@ describe('@owlmeans/mailer-smtp — transport options', () => {
25
35
  })
26
36
 
27
37
  test('coerces the string forms a ConfigMap file delivers', () => {
28
- const options = toTransportOptions({
38
+ const options = makeSmtpSettingsModel({
29
39
  ...base, port: '2525', secure: 'false', rejectUnauthorized: '0', timeout: '5000',
30
- })
40
+ }).toTransportOptions()
31
41
 
32
42
  expect(options.port).toBe(2525)
33
43
  expect(options.secure).toBe(false)
@@ -36,12 +46,12 @@ describe('@owlmeans/mailer-smtp — transport options', () => {
36
46
  })
37
47
 
38
48
  test('falls back to the default port when the value is unusable', () => {
39
- expect(toTransportOptions({ ...base, port: '' }).port).toBe(SMTP_DEFAULT_PORT)
40
- expect(toTransportOptions({ ...base, port: 'nonsense' }).port).toBe(SMTP_DEFAULT_PORT)
49
+ expect(makeSmtpSettingsModel({ ...base, port: '' }).toTransportOptions().port).toBe(SMTP_DEFAULT_PORT)
50
+ expect(makeSmtpSettingsModel({ ...base, port: 'nonsense' }).toTransportOptions().port).toBe(SMTP_DEFAULT_PORT)
41
51
  })
42
52
 
43
53
  test('omits auth for an unauthenticated relay', () => {
44
- expect(toTransportOptions({ ...base, user: '' }).auth).toBeUndefined()
54
+ expect(makeSmtpSettingsModel({ ...base, user: '' }).toTransportOptions().auth).toBeUndefined()
45
55
  })
46
56
  })
47
57
 
@@ -49,7 +59,7 @@ describe('@owlmeans/mailer-smtp — message mapping', () => {
49
59
  const message = { to: 'user@example.com', subject: 'Your login code', text: '106341' }
50
60
 
51
61
  test('applies the configured sender and reply-to', () => {
52
- const options = toMailOptions({ ...base, replyTo: 'support@example.org' }, message)
62
+ const options = makeSmtpSettingsModel({ ...base, replyTo: 'support@example.org' }).toMailOptions(message)
53
63
 
54
64
  expect(options.from).toBe(base.from)
55
65
  expect(options.replyTo).toBe('support@example.org')
@@ -57,8 +67,7 @@ describe('@owlmeans/mailer-smtp — message mapping', () => {
57
67
  })
58
68
 
59
69
  test('lets a message override the sender and reply-to', () => {
60
- const options = toMailOptions(
61
- { ...base, replyTo: 'support@example.org' },
70
+ const options = makeSmtpSettingsModel({ ...base, replyTo: 'support@example.org' }).toMailOptions(
62
71
  { ...message, from: 'Other <other@example.org>', replyTo: 'nobody@example.org' }
63
72
  )
64
73
 
@@ -67,8 +76,7 @@ describe('@owlmeans/mailer-smtp — message mapping', () => {
67
76
  })
68
77
 
69
78
  test('merges headers with the message winning', () => {
70
- const options = toMailOptions(
71
- { ...base, headers: { 'X-Origin': 'config', 'X-Kept': 'yes' } },
79
+ const options = makeSmtpSettingsModel({ ...base, headers: { 'X-Origin': 'config', 'X-Kept': 'yes' } }).toMailOptions(
72
80
  { ...message, headers: { 'X-Origin': 'message' } }
73
81
  )
74
82
 
@@ -77,8 +85,7 @@ describe('@owlmeans/mailer-smtp — message mapping', () => {
77
85
 
78
86
  test('nodemailer builds the envelope we described', async () => {
79
87
  const transport = nodemailer.createTransport({ jsonTransport: true })
80
- const info = await transport.sendMail(toMailOptions(
81
- { ...base, headers: { 'X-OwlMeans-Test': 'mapping' } },
88
+ const info = await transport.sendMail(makeSmtpSettingsModel({ ...base, headers: { 'X-OwlMeans-Test': 'mapping' } }).toMailOptions(
82
89
  { ...message, html: '<p>106341</p>' }
83
90
  ))
84
91
 
@@ -91,4 +98,28 @@ describe('@owlmeans/mailer-smtp — message mapping', () => {
91
98
  expect(built.html).toBe('<p>106341</p>')
92
99
  expect(built.headers['X-OwlMeans-Test']).toBe('mapping')
93
100
  })
101
+
102
+ test('attachments travel as decoded bytes under their own names and types', async () => {
103
+ const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
104
+ const options = makeSmtpSettingsModel(base).toMailOptions({
105
+ ...message,
106
+ attachments: [
107
+ { filename: 'shot.png', content: png, contentType: 'image/png' },
108
+ { filename: 'notes.txt', content: 'aGVsbG8=', encoding: 'base64', contentType: 'text/plain' },
109
+ ],
110
+ })
111
+
112
+ expect(options.attachments).toHaveLength(2)
113
+ expect(makeSmtpSettingsModel(base).toMailOptions(message).attachments).toBeUndefined()
114
+
115
+ const transport = nodemailer.createTransport({ jsonTransport: true })
116
+ const info = await transport.sendMail(options)
117
+ const built = JSON.parse(info.message as unknown as string)
118
+
119
+ expect(built.attachments.map((a: { filename: string }) => a.filename)).toEqual(['shot.png', 'notes.txt'])
120
+ expect(built.attachments[0].contentType).toBe('image/png')
121
+ expect(Buffer.from(built.attachments[0].content, 'base64')).toEqual(Buffer.from(png))
122
+ expect(built.attachments[1].contentType).toBe('text/plain')
123
+ expect(Buffer.from(built.attachments[1].content, 'base64').toString('utf8')).toBe('hello')
124
+ })
94
125
  })
package/build/.gitkeep DELETED
File without changes