@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 +3 -3
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/mailer-smtp/SKILL.md +37 -15
- package/build/assert.d.ts +4 -0
- package/build/assert.d.ts.map +1 -0
- package/build/assert.js +10 -0
- package/build/assert.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/service.d.ts +2 -15
- package/build/service.d.ts.map +1 -1
- package/build/service.js +14 -53
- package/build/service.js.map +1 -1
- package/build/settings/types.d.ts +19 -0
- package/build/settings/types.d.ts.map +1 -0
- package/build/settings/types.js +2 -0
- package/build/settings/types.js.map +1 -0
- package/build/settings.d.ts +4 -0
- package/build/settings.d.ts.map +1 -0
- package/build/settings.js +53 -0
- package/build/settings.js.map +1 -0
- package/build/types.d.ts +6 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +10 -11
- package/src/assert.ts +15 -0
- package/src/index.ts +3 -0
- package/src/service.ts +19 -65
- package/src/settings/types.ts +19 -0
- package/src/settings.ts +63 -0
- package/src/types.ts +7 -0
- package/tests/context.ts +3 -2
- package/tests/message.spec.ts +45 -14
- package/build/.gitkeep +0 -0
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()` /
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/mailer-smtp",
|
|
4
|
-
"version": "0.1.18
|
|
5
|
-
"generatedAt": "2026-
|
|
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` /
|
|
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.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
92
|
-
|
|
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
|
|
99
|
-
|
|
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
|
|
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"}
|
package/build/assert.js
ADDED
|
@@ -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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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"}
|
package/build/service.d.ts
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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 {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
};
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/settings/types.ts"],"names":[],"mappings":""}
|
|
@@ -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
|
package/build/types.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
"@owlmeans/mailer": "^0.1.18
|
|
27
|
-
"@owlmeans/server-context": "^0.1.18
|
|
28
|
-
"nodemailer": "^
|
|
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": "
|
|
32
|
-
"@owlmeans/test": "^0.1.18
|
|
33
|
-
"@owlmeans/test-integration": "^0.1.18
|
|
34
|
-
"@types/bun": "^1.4.
|
|
35
|
-
"@types/node": "^26.
|
|
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
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
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
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 = (
|
|
67
|
-
|
|
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
|
-
|
|
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<
|
|
81
|
-
transport ??= nodemailer.createTransport(
|
|
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 =
|
|
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
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -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
|
-
|
|
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 => ({
|
package/tests/message.spec.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import nodemailer from 'nodemailer'
|
|
3
|
-
import {
|
|
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 =
|
|
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 =
|
|
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(
|
|
40
|
-
expect(
|
|
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(
|
|
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 =
|
|
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
|