sently 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +44 -0
- package/dist/{chunk-8sm0vz0n.js → chunk-7fqv71z1.js} +10 -4
- package/dist/chunk-7fqv71z1.js.map +10 -0
- package/dist/{chunk-bvxkmq94.js → chunk-f4c9ttmr.js} +34 -5
- package/dist/chunk-f4c9ttmr.js.map +11 -0
- package/dist/{chunk-bfrvnqhq.js → chunk-mp5c9bfd.js} +3 -3
- package/dist/{chunk-sbydk09g.js → chunk-tymfm441.js} +5 -3
- package/dist/{chunk-sbydk09g.js.map → chunk-tymfm441.js.map} +3 -3
- package/dist/{chunk-j6qw8ms6.js → chunk-x3szga4k.js} +21 -7
- package/dist/chunk-x3szga4k.js.map +11 -0
- package/dist/core/address.d.ts +30 -0
- package/dist/core/smtp.js +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/detect.js +5 -5
- package/dist/pool/pool.js +5 -5
- package/dist/transports/brevo.js +1 -1
- package/dist/transports/mailgun.js +1 -1
- package/dist/transports/postmark.js +1 -1
- package/dist/transports/preview.js +2 -2
- package/dist/transports/resend.js +1 -1
- package/dist/transports/resolve-attachments.d.ts +10 -2
- package/dist/transports/retry.js +1 -1
- package/dist/transports/sendgrid.js +1 -1
- package/dist/transports/ses.js +4 -4
- package/dist/transports/ses.js.map +2 -2
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-8sm0vz0n.js.map +0 -10
- package/dist/chunk-bvxkmq94.js.map +0 -11
- package/dist/chunk-j6qw8ms6.js.map +0 -11
- /package/dist/{chunk-bfrvnqhq.js.map → chunk-mp5c9bfd.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.4] — 2026-05-30
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- Fixed SigV4 date format: slice(0,15) not slice(0,16) — all real
|
|
8
|
+
SES requests were producing malformed x-amz-date headers
|
|
9
|
+
- Added requireTLS guard before SMTP AUTH (default: true when auth
|
|
10
|
+
is set) — prevents credential exposure on STARTTLS-stripping attacks
|
|
11
|
+
- Hardened email address validation against header and SMTP command
|
|
12
|
+
injection, enforced centrally in `parseAddresses()` (and re-asserted
|
|
13
|
+
at render time in `toMIMEHeader()`):
|
|
14
|
+
- Rejects CR, LF, NUL, all other C0 control characters (0x00–0x1F),
|
|
15
|
+
DEL (0x7F), and the Unicode line/paragraph separators U+2028/U+2029
|
|
16
|
+
- Fails closed: hostile input throws a clear error with the offending
|
|
17
|
+
code point instead of being accepted
|
|
18
|
+
- No repair or normalization of malicious input — addresses are never
|
|
19
|
+
transformed (e.g. CR/LF stripped) and then accepted
|
|
20
|
+
- Protects the display name as well as the address; an ASCII display
|
|
21
|
+
name such as `"Foo\r\nBcc: ..."` can no longer inject a header
|
|
22
|
+
- Enforced consistently across every address field (From, To, Cc, Bcc,
|
|
23
|
+
Reply-To) and every transport (SMTP, SES, Mailgun, Postmark, Resend,
|
|
24
|
+
SendGrid, Brevo), since all of them route through `parseAddresses()`
|
|
25
|
+
- Sanitized attachment filenames and custom attachment headers
|
|
26
|
+
against MIME header injection
|
|
27
|
+
- Fixed basePath startsWith sibling-directory bypass in
|
|
28
|
+
resolve-attachments (now appends path separator before comparison)
|
|
29
|
+
- Added CRLF guard on EHLO domain for consistency
|
|
30
|
+
|
|
31
|
+
## [0.4.3] — 2026-05-30
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `llms.txt` for LLM/agent discovery (install, quick example, subpath
|
|
36
|
+
exports, and when-to-use guidance)
|
|
37
|
+
- README: Nodemailer comparison table, 30-second tour, error handling,
|
|
38
|
+
and TypeScript sections
|
|
39
|
+
- `CLAUDE.md` repository map for agents (core entry, adapters,
|
|
40
|
+
transports, tests, build)
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- README positioning, HTTP transport reference table, plugin docs
|
|
45
|
+
reorder, and tree-shaking callout
|
|
46
|
+
- `package.json` description and npm keywords for discoverability
|
|
47
|
+
|
|
3
48
|
## [0.4.2] — 2026-05-30
|
|
4
49
|
|
|
5
50
|
### Fixed
|
package/README.md
CHANGED
|
@@ -214,6 +214,8 @@ await mailer.verify(); // test connection + auth
|
|
|
214
214
|
|
|
215
215
|
**AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
|
|
216
216
|
|
|
217
|
+
**`requireTLS` (default `true` when `auth` is set):** sently refuses to send credentials over an unencrypted connection. If the link is not secured by direct TLS (`secure: true`) or a successful `STARTTLS` upgrade, authentication throws an `SMTPError` instead of leaking credentials — this defends against STARTTLS-stripping MITM attacks. Set `requireTLS: false` only if you fully trust the network (not recommended).
|
|
218
|
+
|
|
217
219
|
#### DKIM signing
|
|
218
220
|
|
|
219
221
|
```typescript
|
|
@@ -506,6 +508,48 @@ Import error classes from their transport subpath — not from `sently` core. Ea
|
|
|
506
508
|
|
|
507
509
|
---
|
|
508
510
|
|
|
511
|
+
## Security
|
|
512
|
+
|
|
513
|
+
sently is built to be secure by default — protections are enforced at the library's core chokepoints, so they apply to every transport and every address field without any extra configuration.
|
|
514
|
+
|
|
515
|
+
### Email header & SMTP command injection
|
|
516
|
+
|
|
517
|
+
All addresses **and** display names are validated centrally in `parseAddresses()` (and re-asserted when rendering headers), before any normalization:
|
|
518
|
+
|
|
519
|
+
- Rejects CR, LF, NUL, every other C0 control (`0x00`–`0x1F`), DEL (`0x7F`), and the Unicode line/paragraph separators `U+2028`/`U+2029`.
|
|
520
|
+
- **Fails closed:** hostile input throws a clear error (with the offending code point) — it is never stripped, repaired, and then accepted.
|
|
521
|
+
- Protects the **display name** too, so an ASCII name like `"Foo\r\nBcc: attacker@evil.com"` can no longer inject a header.
|
|
522
|
+
- Enforced consistently across **From, To, Cc, Bcc, and Reply-To**, and across **every transport** (SMTP, SES, Mailgun, Postmark, Resend, SendGrid, Brevo).
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
await mailer.send({
|
|
526
|
+
from: "you@example.com",
|
|
527
|
+
to: { address: "victim@x.com\r\nBcc: attacker@evil.com" },
|
|
528
|
+
subject: "Hi",
|
|
529
|
+
text: "...",
|
|
530
|
+
});
|
|
531
|
+
// → throws: Email address contains a forbidden control character (0x0d)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
MIME attachment filenames and custom attachment headers are likewise sanitized against header injection.
|
|
535
|
+
|
|
536
|
+
### Credential protection
|
|
537
|
+
|
|
538
|
+
- **`requireTLS`** (default `true` when `auth` is set) refuses to authenticate over a cleartext connection, defeating STARTTLS-stripping downgrade attacks.
|
|
539
|
+
- **OAuth2 / XOAUTH2** and DKIM signing are built in via Web Crypto — no plaintext secrets in transit beyond what the protocol requires.
|
|
540
|
+
|
|
541
|
+
### Attachments
|
|
542
|
+
|
|
543
|
+
> ⚠️ `attachment.path` reads files from disk. Never pass user-controlled paths without validation.
|
|
544
|
+
|
|
545
|
+
`resolveAttachments()` accepts an opt-in `basePath` that confines reads to an allowed directory and rejects path-traversal (including sibling-directory prefix tricks like `/var/data-secret` vs `/var/data`). Note: `basePath` does not dereference symlinks — use `fs.realpath()` first if symlink traversal is a concern.
|
|
546
|
+
|
|
547
|
+
### Supply chain
|
|
548
|
+
|
|
549
|
+
**Zero runtime dependencies** — there is no transitive dependency tree to audit or to be compromised.
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
509
553
|
## Tree-Shaking
|
|
510
554
|
|
|
511
555
|
Each import path is a separate build entry point:
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-ym3zzv8b.js";
|
|
4
4
|
import {
|
|
5
5
|
buildMIME
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-x3szga4k.js";
|
|
7
7
|
import {
|
|
8
8
|
SMTPError,
|
|
9
9
|
accumulateResponse,
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
parseEHLO,
|
|
17
17
|
parseResponse,
|
|
18
18
|
selectAuthMethod
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-tymfm441.js";
|
|
20
20
|
import {
|
|
21
21
|
resolveAttachments
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-f4c9ttmr.js";
|
|
23
23
|
import {
|
|
24
24
|
__require
|
|
25
25
|
} from "./chunk-v0bahtg2.js";
|
|
@@ -87,6 +87,7 @@ function resolveSMTPConfig(config) {
|
|
|
87
87
|
port: config.port ?? (secure ? 465 : 587),
|
|
88
88
|
secure,
|
|
89
89
|
...config.auth !== undefined ? { auth: config.auth } : {},
|
|
90
|
+
...config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {},
|
|
90
91
|
...config.dkim !== undefined ? { dkim: config.dkim } : {},
|
|
91
92
|
...config.tls !== undefined ? { tls: config.tls } : {},
|
|
92
93
|
...config.connectionTimeout !== undefined ? { connectionTimeout: config.connectionTimeout } : {},
|
|
@@ -109,6 +110,11 @@ async function openSMTPSession(adapter, config) {
|
|
|
109
110
|
capabilities = await ehlo(adapter, config.host);
|
|
110
111
|
}
|
|
111
112
|
if (config.auth) {
|
|
113
|
+
const tlsRequired = config.requireTLS ?? true;
|
|
114
|
+
const encrypted = adapter.secure || config.secure;
|
|
115
|
+
if (tlsRequired && !encrypted) {
|
|
116
|
+
throw new SMTPError("Refusing to authenticate over unencrypted connection. " + "Set requireTLS: false to disable this check (not recommended).", 0, "AUTH", "");
|
|
117
|
+
}
|
|
112
118
|
await authenticate(adapter, config.auth, capabilities);
|
|
113
119
|
}
|
|
114
120
|
}
|
|
@@ -242,4 +248,4 @@ async function resolveMX(domain) {
|
|
|
242
248
|
}
|
|
243
249
|
export { SMTPTransport, resolveSMTPConfig, openSMTPSession, deliverSMTPMessage, closeSMTPSession, readSMTPResponse };
|
|
244
250
|
|
|
245
|
-
//# debugId=
|
|
251
|
+
//# debugId=736A9106E3164FC464756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/smtp.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n private readonly config: ResolvedSMTPConfig;\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n host: string;\n port: number;\n secure: boolean;\n auth?: SMTPConfig[\"auth\"];\n requireTLS?: boolean;\n tls?: SMTPConfig[\"tls\"];\n dkim?: SMTPConfig[\"dkim\"];\n connectionTimeout?: number;\n greetingTimeout?: number;\n socketTimeout?: number;\n direct?: boolean;\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n const tlsRequired = config.requireTLS ?? true;\n const encrypted = adapter.secure || config.secure;\n if (tlsRequired && !encrypted) {\n throw new SMTPError(\n \"Refusing to authenticate over unencrypted connection. \" +\n \"Set requireTLS: false to disable this check (not recommended).\",\n 0,\n \"AUTH\",\n \"\",\n );\n }\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "736A9106E3164FC464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -6,13 +6,34 @@ import {
|
|
|
6
6
|
} from "./chunk-v0bahtg2.js";
|
|
7
7
|
|
|
8
8
|
// src/core/address.ts
|
|
9
|
+
function findForbiddenChar(value) {
|
|
10
|
+
for (let i = 0;i < value.length; i++) {
|
|
11
|
+
const code = value.charCodeAt(i);
|
|
12
|
+
if (code <= 31 || code === 127 || code === 8232 || code === 8233) {
|
|
13
|
+
return code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return -1;
|
|
17
|
+
}
|
|
18
|
+
function assertSafeAddress(value, label = "address") {
|
|
19
|
+
const code = findForbiddenChar(value);
|
|
20
|
+
if (code !== -1) {
|
|
21
|
+
const hex = code.toString(16).padStart(2, "0");
|
|
22
|
+
throw new Error(`Email ${label} contains a forbidden control character (0x${hex}). ` + "CR, LF, NUL, and other control characters are not allowed.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
function parseAddresses(input) {
|
|
10
26
|
if (Array.isArray(input)) {
|
|
11
27
|
return input.flatMap((item) => parseAddresses(item));
|
|
12
28
|
}
|
|
13
29
|
if (typeof input === "object") {
|
|
30
|
+
assertSafeAddress(input.address, "address");
|
|
31
|
+
if (input.name !== undefined) {
|
|
32
|
+
assertSafeAddress(input.name, "display name");
|
|
33
|
+
}
|
|
14
34
|
return [{ ...input }];
|
|
15
35
|
}
|
|
36
|
+
assertSafeAddress(input, "address");
|
|
16
37
|
const trimmed = input.trim();
|
|
17
38
|
if (!trimmed) {
|
|
18
39
|
return [];
|
|
@@ -20,7 +41,9 @@ function parseAddresses(input) {
|
|
|
20
41
|
return splitAddressList(trimmed).map(parseSingleAddress);
|
|
21
42
|
}
|
|
22
43
|
function toMIMEHeader(address) {
|
|
44
|
+
assertSafeAddress(address.address, "address");
|
|
23
45
|
if (address.name) {
|
|
46
|
+
assertSafeAddress(address.name, "display name");
|
|
24
47
|
const name = encodeHeader(address.name);
|
|
25
48
|
return `${name} <${address.address}>`;
|
|
26
49
|
}
|
|
@@ -29,6 +52,11 @@ function toMIMEHeader(address) {
|
|
|
29
52
|
function extractEmails(input) {
|
|
30
53
|
return parseAddresses(input).map((addr) => addr.address);
|
|
31
54
|
}
|
|
55
|
+
function isValidEmail(email) {
|
|
56
|
+
if (findForbiddenChar(email) !== -1)
|
|
57
|
+
return false;
|
|
58
|
+
return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email);
|
|
59
|
+
}
|
|
32
60
|
function splitAddressList(input) {
|
|
33
61
|
const parts = [];
|
|
34
62
|
let current = "";
|
|
@@ -99,11 +127,12 @@ async function resolveAttachments(attachments, options) {
|
|
|
99
127
|
throw new Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead");
|
|
100
128
|
}
|
|
101
129
|
if (options?.basePath) {
|
|
102
|
-
const { resolve } = await import("node:path");
|
|
130
|
+
const { resolve, sep } = await import("node:path");
|
|
103
131
|
const resolvedPath = resolve(attachment.path);
|
|
104
132
|
const resolvedBase = resolve(options.basePath);
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
const isWithin = resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);
|
|
134
|
+
if (!isWithin) {
|
|
135
|
+
throw new Error(`[sently] Attachment path "${resolvedPath}" escapes basePath "${resolvedBase}". Use absolute paths within the allowed directory.`);
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
const data = await fs.readFile(attachment.path);
|
|
@@ -120,6 +149,6 @@ async function resolveAttachments(attachments, options) {
|
|
|
120
149
|
return resolved;
|
|
121
150
|
}
|
|
122
151
|
|
|
123
|
-
export { parseAddresses, toMIMEHeader, extractEmails, resolveAttachments };
|
|
152
|
+
export { assertSafeAddress, parseAddresses, toMIMEHeader, extractEmails, isValidEmail, resolveAttachments };
|
|
124
153
|
|
|
125
|
-
//# debugId=
|
|
154
|
+
//# debugId=419CD2BE8A029CA164756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/address.ts", "../src/transports/resolve-attachments.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// src/core/address.ts\nimport { encodeHeader } from \"./base64.js\";\nimport type { Address, AddressInput } from \"./types.js\";\n\n/**\n * Find the first forbidden control character in a string.\n *\n * Forbidden characters must never appear in an email address or display name,\n * because each enables a distinct attack:\n * - CR (0x0D) / LF (0x0A): email header injection and SMTP command injection\n * - NUL (0x00): C-string truncation / parser confusion in downstream agents\n * - other C0 controls (0x01–0x1F), DEL (0x7F): header/parser confusion\n * - U+2028 / U+2029: line separators that some parsers treat as newlines\n *\n * @returns The char code of the first forbidden character, or -1 if none.\n */\nfunction findForbiddenChar(value: string): number {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n if (code <= 0x1f || code === 0x7f || code === 0x2028 || code === 0x2029) {\n return code;\n }\n }\n return -1;\n}\n\n/**\n * Assert that a raw address or display-name value contains no forbidden\n * control characters. Throws immediately (fail closed) — the library never\n * attempts to strip or rewrite hostile input into an accepted value.\n *\n * The check is intentionally performed on the RAW input, before any trimming\n * or normalization, so hostile values are rejected rather than repaired.\n *\n * @param value - The raw, untransformed string to validate.\n * @param label - Field label used in the error message (e.g. \"address\").\n * @throws {Error} If the value contains any forbidden control character.\n */\nexport function assertSafeAddress(value: string, label = \"address\"): void {\n const code = findForbiddenChar(value);\n if (code !== -1) {\n const hex = code.toString(16).padStart(2, \"0\");\n throw new Error(\n `Email ${label} contains a forbidden control character (0x${hex}). ` +\n \"CR, LF, NUL, and other control characters are not allowed.\",\n );\n }\n}\n\n/**\n * Normalize any AddressInput form into Address[].\n *\n * Every address and display name is validated against control-character\n * injection before any transformation. This is the single chokepoint shared\n * by all transports and address fields (From, To, Cc, Bcc, Reply-To), so the\n * protection is uniform and secure by default.\n *\n * @throws {Error} If any address or name contains a forbidden control character.\n */\nexport function parseAddresses(input: AddressInput): Address[] {\n if (Array.isArray(input)) {\n return input.flatMap((item) => parseAddresses(item));\n }\n\n if (typeof input === \"object\") {\n assertSafeAddress(input.address, \"address\");\n if (input.name !== undefined) {\n assertSafeAddress(input.name, \"display name\");\n }\n return [{ ...input }];\n }\n\n // Validate the raw input string before splitting or trimming so injected\n // newlines cannot hide inside a multi-address list.\n assertSafeAddress(input, \"address\");\n\n const trimmed = input.trim();\n if (!trimmed) {\n return [];\n }\n\n return splitAddressList(trimmed).map(parseSingleAddress);\n}\n\n/**\n * Format an Address for SMTP envelope commands (MAIL FROM / RCPT TO).\n */\nexport function toEnvelope(address: Address): string {\n return address.address;\n}\n\n/**\n * Format an Address for use in a MIME header (From, To, CC, etc.).\n *\n * Re-validates the address and name at render time so a header can never be\n * emitted with an embedded control character, even if the {@link Address} was\n * constructed without going through {@link parseAddresses}.\n *\n * @throws {Error} If the address or name contains a forbidden control character.\n */\nexport function toMIMEHeader(address: Address): string {\n assertSafeAddress(address.address, \"address\");\n if (address.name) {\n assertSafeAddress(address.name, \"display name\");\n const name = encodeHeader(address.name);\n return `${name} <${address.address}>`;\n }\n return address.address;\n}\n\n/**\n * Extract plain email strings from any AddressInput.\n */\nexport function extractEmails(input: AddressInput): string[] {\n return parseAddresses(input).map((addr) => addr.address);\n}\n\n/**\n * Basic email format validation (format only, no DNS lookup).\n *\n * Rejects any control character (including CR, LF, tab, and NUL) before\n * applying the structural check, so a \"valid\" result is always safe to place\n * into a header or SMTP command.\n */\nexport function isValidEmail(email: string): boolean {\n if (findForbiddenChar(email) !== -1) return false;\n return /^[^\\s@<>]+@[^\\s@<>]+\\.[^\\s@<>]+$/.test(email);\n}\n\nfunction splitAddressList(input: string): string[] {\n const parts: string[] = [];\n let current = \"\";\n let inQuotes = false;\n let inAngle = false;\n\n for (let i = 0; i < input.length; i++) {\n const char = input[i] ?? \"\";\n if (char === '\"' && input[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n current += char;\n continue;\n }\n if (char === \"<\" && !inQuotes) {\n inAngle = true;\n current += char;\n continue;\n }\n if (char === \">\" && !inQuotes) {\n inAngle = false;\n current += char;\n continue;\n }\n if (char === \",\" && !inQuotes && !inAngle) {\n if (current.trim()) {\n parts.push(current.trim());\n }\n current = \"\";\n continue;\n }\n current += char;\n }\n\n if (current.trim()) {\n parts.push(current.trim());\n }\n\n return parts;\n}\n\nfunction parseSingleAddress(input: string): Address {\n const trimmed = input.trim();\n\n const angleMatch = trimmed.match(/^(?:\"([^\"]*)\"|([^<]*?))\\s*<([^>]+)>$/);\n if (angleMatch) {\n const name = (angleMatch[1] ?? angleMatch[2] ?? \"\").trim();\n const address = (angleMatch[3] ?? \"\").trim();\n if (name) {\n return { name, address };\n }\n return { address };\n }\n\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n return { address: trimmed.slice(1, -1) };\n }\n\n return { address: trimmed };\n}\n",
|
|
6
|
+
"import type { Attachment } from \"../core/types.js\";\n\n/** Options for {@link resolveAttachments}. */\nexport interface ResolveAttachmentsOptions {\n /**\n * If set, attachment paths must resolve within this directory.\n * Prevents path traversal via `..` segments and sibling-directory\n * prefix matches. Opt-in only.\n *\n * Note: this check uses `node:path` `resolve()`, which does NOT\n * dereference symlinks. A symlink located inside `basePath` that\n * points outside of it will pass this check. If symlink traversal is\n * a concern, resolve paths with `fs.realpath()` before passing them in.\n */\n basePath?: string;\n}\n\n/**\n * Resolve attachment.path to in-memory Uint8Array content.\n * @throws When attachment.path is used on runtimes without node:fs/promises\n */\nexport async function resolveAttachments(\n attachments: Attachment[] | undefined,\n options?: ResolveAttachmentsOptions,\n): Promise<Attachment[]> {\n const list = attachments ?? [];\n const resolved: Attachment[] = [];\n\n for (const attachment of list) {\n if (attachment.content instanceof Uint8Array) {\n resolved.push(attachment);\n continue;\n }\n\n if (attachment.path) {\n let fs: typeof import(\"node:fs/promises\");\n try {\n fs = await import(\"node:fs/promises\");\n } catch {\n throw new Error(\n \"attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead\",\n );\n }\n\n if (options?.basePath) {\n const { resolve, sep } = await import(\"node:path\");\n const resolvedPath = resolve(attachment.path);\n const resolvedBase = resolve(options.basePath);\n // startsWith alone is vulnerable: \"/var/data-secret\" passes \"/var/data\".\n // Require an exact match or a trailing path separator.\n const isWithin =\n resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);\n if (!isWithin) {\n throw new Error(\n `[sently] Attachment path \"${resolvedPath}\" escapes basePath \"${resolvedBase}\". ` +\n \"Use absolute paths within the allowed directory.\",\n );\n }\n }\n\n const data = await fs.readFile(attachment.path);\n const { path: _path, ...rest } = attachment;\n resolved.push({ ...rest, content: new Uint8Array(data) });\n continue;\n }\n\n if (typeof attachment.content === \"string\") {\n resolved.push(attachment);\n continue;\n }\n\n resolved.push(attachment);\n }\n\n return resolved;\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "419CD2BE8A029CA164756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
deliverSMTPMessage,
|
|
4
4
|
openSMTPSession,
|
|
5
5
|
resolveSMTPConfig
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7fqv71z1.js";
|
|
7
7
|
import {
|
|
8
8
|
buildMIME
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-x3szga4k.js";
|
|
10
10
|
import {
|
|
11
11
|
resolveAttachments
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-f4c9ttmr.js";
|
|
13
13
|
|
|
14
14
|
// src/pool/connection.ts
|
|
15
15
|
async function createPooledConnection(options) {
|
|
@@ -257,9 +257,11 @@ class SMTPError extends Error {
|
|
|
257
257
|
function encodeCommand(cmd) {
|
|
258
258
|
let line;
|
|
259
259
|
switch (cmd.type) {
|
|
260
|
-
case "EHLO":
|
|
261
|
-
|
|
260
|
+
case "EHLO": {
|
|
261
|
+
const safeDomain = cmd.domain.replace(/[\r\n]/g, "");
|
|
262
|
+
line = `EHLO ${safeDomain}`;
|
|
262
263
|
break;
|
|
264
|
+
}
|
|
263
265
|
case "STARTTLS":
|
|
264
266
|
line = "STARTTLS";
|
|
265
267
|
break;
|
|
@@ -400,4 +402,4 @@ function encodeLine(line) {
|
|
|
400
402
|
|
|
401
403
|
export { computeCRAMMD5, SMTPError, encodeCommand, parseResponse, accumulateResponse, selectAuthMethod, parseEHLO, assertResponse, encodeAuthLoginPass, encodeAuthLoginUser, encodeAuthCramResponse, encodeLine };
|
|
402
404
|
|
|
403
|
-
//# debugId=
|
|
405
|
+
//# debugId=5BEE29900FA6AC3664756E2164756E21
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/core/cram-md5.ts", "../src/core/smtp.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * @module\n * Pure-JS HMAC-MD5 implementation for SMTP CRAM-MD5 authentication.\n * Web Crypto does not support MD5, so this is implemented in pure TypeScript\n * with no external dependencies.\n *\n * @example\n * ```ts\n * import { computeCRAMMD5 } from \"sently/core/cram-md5\";\n * const response = await computeCRAMMD5(\"<challenge>\", \"user\", \"pass\");\n * ```\n */\nimport { decodeBase64, encodeBase64, encodeUtf8 } from \"./base64.js\";\n\n/** MD5 block size in bytes (HMAC block size per RFC 2104). */\nconst BLOCK_SIZE = 64;\n\n/** Coerce to unsigned 32-bit integer. */\nfunction u32(x: number): number {\n return x >>> 0;\n}\n\n/**\n * Compute an MD5 hash of the given data (RFC 1321).\n */\nexport function md5(data: Uint8Array): Uint8Array {\n const padded = padMessage(data);\n\n let a0 = 0x67452301;\n let b0 = 0xefcdab89;\n let c0 = 0x98badcfe;\n let d0 = 0x10325476;\n\n for (let i = 0; i < padded.length; i += 64) {\n const block = padded.subarray(i, i + 64);\n const m = new Uint32Array(16);\n for (let j = 0; j < 16; j++) {\n const o = j * 4;\n m[j] = u32(\n (block[o] ?? 0) |\n ((block[o + 1] ?? 0) << 8) |\n ((block[o + 2] ?? 0) << 16) |\n ((block[o + 3] ?? 0) << 24),\n );\n }\n\n let a = a0;\n let b = b0;\n let c = c0;\n let d = d0;\n\n for (let k = 0; k < 64; k++) {\n let f: number;\n let g: number;\n if (k < 16) {\n f = u32((b & c) | (~b & d));\n g = k;\n } else if (k < 32) {\n f = u32((b & d) | (c & ~d));\n g = u32((5 * k + 1) % 16);\n } else if (k < 48) {\n f = u32(b ^ c ^ d);\n g = u32((3 * k + 5) % 16);\n } else {\n f = u32(c ^ (b | ~d));\n g = u32((7 * k) % 16);\n }\n\n const temp = d;\n d = c;\n c = b;\n b = u32(b + leftRotate(u32(a + f + u32((K[k] ?? 0) + (m[g] ?? 0))), S[k] ?? 0));\n a = temp;\n }\n\n a0 = u32(a0 + a);\n b0 = u32(b0 + b);\n c0 = u32(c0 + c);\n d0 = u32(d0 + d);\n }\n\n const out = new Uint8Array(16);\n const view = new DataView(out.buffer);\n view.setUint32(0, a0, true);\n view.setUint32(4, b0, true);\n view.setUint32(8, c0, true);\n view.setUint32(12, d0, true);\n return out;\n}\n\nconst S = [\n 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,\n 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6,\n 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,\n];\n\nconst K = new Uint32Array([\n 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,\n 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,\n 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,\n 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,\n 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,\n 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,\n 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,\n 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,\n]);\n\nfunction leftRotate(value: number, shift: number): number {\n return u32((value << shift) | (value >>> (32 - shift)));\n}\n\nfunction padMessage(data: Uint8Array): Uint8Array {\n const bitLen = data.length * 8;\n const padLen = (56 - ((data.length + 1) % 64) + 64) % 64;\n const totalLen = data.length + 1 + padLen + 8;\n const padded = new Uint8Array(totalLen);\n padded.set(data);\n padded[data.length] = 0x80;\n\n const view = new DataView(padded.buffer);\n view.setUint32(totalLen - 8, bitLen >>> 0, true);\n view.setUint32(totalLen - 4, Math.floor(bitLen / 0x100000000), true);\n return padded;\n}\n\n/**\n * Compute HMAC-MD5(key, data) per RFC 2104.\n */\nexport function hmacMD5(key: Uint8Array, data: Uint8Array): Uint8Array {\n let k = key;\n if (k.length > BLOCK_SIZE) {\n k = md5(k);\n }\n const paddedKey = new Uint8Array(BLOCK_SIZE);\n paddedKey.set(k);\n\n const ipad = new Uint8Array(BLOCK_SIZE);\n const opad = new Uint8Array(BLOCK_SIZE);\n for (let i = 0; i < BLOCK_SIZE; i++) {\n ipad[i] = (paddedKey[i] ?? 0) ^ 0x36;\n opad[i] = (paddedKey[i] ?? 0) ^ 0x5c;\n }\n\n const inner = new Uint8Array(ipad.length + data.length);\n inner.set(ipad);\n inner.set(data, ipad.length);\n const innerHash = md5(inner);\n\n const outer = new Uint8Array(opad.length + innerHash.length);\n outer.set(opad);\n outer.set(innerHash, opad.length);\n return md5(outer);\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * Compute the CRAM-MD5 response string for SMTP authentication.\n *\n * @param challenge - base64-encoded challenge from server\n * @param user - SMTP username\n * @param pass - SMTP password\n * @returns base64-encoded CRAM-MD5 response\n */\nexport async function computeCRAMMD5(\n challenge: string,\n user: string,\n pass: string,\n): Promise<string> {\n const challengeBytes = decodeBase64(challenge.trim());\n const passBytes = encodeUtf8(pass);\n const digest = hmacMD5(passBytes, challengeBytes);\n const hex = bytesToHex(digest);\n return encodeBase64(`${user} ${hex}`).replace(/\\r\\n/g, \"\");\n}\n",
|
|
6
|
-
"// src/core/smtp.ts\nimport { encodeBase64, encodeUtf8 } from \"./base64.js\";\n\nexport { computeCRAMMD5 } from \"./cram-md5.js\";\n\n/** SMTP command to send to the server. */\nexport type SMTPCommand =\n | { type: \"EHLO\"; domain: string }\n | { type: \"STARTTLS\" }\n | { type: \"AUTH_LOGIN\"; user: string; pass: string }\n | { type: \"AUTH_PLAIN\"; user: string; pass: string }\n | { type: \"AUTH_CRAM_MD5_INIT\" }\n | { type: \"AUTH_CRAM_MD5_RESPONSE\"; response: string }\n | { type: \"AUTH_XOAUTH2\"; xoauth2String: string }\n | { type: \"MAIL_FROM\"; address: string }\n | { type: \"RCPT_TO\"; address: string }\n | { type: \"DATA\" }\n | { type: \"DATA_BODY\"; content: Uint8Array }\n | { type: \"QUIT\" }\n | { type: \"RSET\" }\n | { type: \"NOOP\" };\n\n/** Parsed SMTP server response. */\nexport interface SMTPResponse {\n code: number;\n message: string;\n isSuccess: boolean;\n isReady: boolean;\n isError: boolean;\n}\n\n/** SMTP protocol error with server response details. */\nexport class SMTPError extends Error {\n /** Creates an SMTP protocol error. */\n constructor(\n message: string,\n public readonly code: number,\n public readonly command: string,\n public readonly response: string,\n ) {\n super(message);\n this.name = \"SMTPError\";\n }\n}\n\n/**\n * Encode an SMTPCommand into a Uint8Array for sending over the socket.\n */\nexport function encodeCommand(cmd: SMTPCommand): Uint8Array {\n let line: string;\n\n switch (cmd.type) {\n case \"EHLO\"
|
|
6
|
+
"// src/core/smtp.ts\nimport { encodeBase64, encodeUtf8 } from \"./base64.js\";\n\nexport { computeCRAMMD5 } from \"./cram-md5.js\";\n\n/** SMTP command to send to the server. */\nexport type SMTPCommand =\n | { type: \"EHLO\"; domain: string }\n | { type: \"STARTTLS\" }\n | { type: \"AUTH_LOGIN\"; user: string; pass: string }\n | { type: \"AUTH_PLAIN\"; user: string; pass: string }\n | { type: \"AUTH_CRAM_MD5_INIT\" }\n | { type: \"AUTH_CRAM_MD5_RESPONSE\"; response: string }\n | { type: \"AUTH_XOAUTH2\"; xoauth2String: string }\n | { type: \"MAIL_FROM\"; address: string }\n | { type: \"RCPT_TO\"; address: string }\n | { type: \"DATA\" }\n | { type: \"DATA_BODY\"; content: Uint8Array }\n | { type: \"QUIT\" }\n | { type: \"RSET\" }\n | { type: \"NOOP\" };\n\n/** Parsed SMTP server response. */\nexport interface SMTPResponse {\n code: number;\n message: string;\n isSuccess: boolean;\n isReady: boolean;\n isError: boolean;\n}\n\n/** SMTP protocol error with server response details. */\nexport class SMTPError extends Error {\n /** Creates an SMTP protocol error. */\n constructor(\n message: string,\n public readonly code: number,\n public readonly command: string,\n public readonly response: string,\n ) {\n super(message);\n this.name = \"SMTPError\";\n }\n}\n\n/**\n * Encode an SMTPCommand into a Uint8Array for sending over the socket.\n */\nexport function encodeCommand(cmd: SMTPCommand): Uint8Array {\n let line: string;\n\n switch (cmd.type) {\n case \"EHLO\": {\n const safeDomain = cmd.domain.replace(/[\\r\\n]/g, \"\");\n line = `EHLO ${safeDomain}`;\n break;\n }\n case \"STARTTLS\":\n line = \"STARTTLS\";\n break;\n case \"AUTH_LOGIN\":\n line = \"AUTH LOGIN\";\n break;\n case \"AUTH_PLAIN\":\n line = `AUTH PLAIN ${encodeBase64(`\\0${cmd.user}\\0${cmd.pass}`).replace(/\\r\\n/g, \"\")}`;\n break;\n case \"AUTH_CRAM_MD5_INIT\":\n line = \"AUTH CRAM-MD5\";\n break;\n case \"AUTH_CRAM_MD5_RESPONSE\":\n return encodeUtf8(`${cmd.response}\\r\\n`);\n case \"AUTH_XOAUTH2\":\n line = `AUTH XOAUTH2 ${cmd.xoauth2String}`;\n break;\n case \"MAIL_FROM\":\n if (/[\\r\\n]/.test(cmd.address)) {\n throw new SMTPError(`Invalid address: contains CRLF`, 0, \"MAIL FROM\", cmd.address);\n }\n line = `MAIL FROM:<${cmd.address}>`;\n break;\n case \"RCPT_TO\":\n if (/[\\r\\n]/.test(cmd.address)) {\n throw new SMTPError(`Invalid address: contains CRLF`, 0, \"RCPT TO\", cmd.address);\n }\n line = `RCPT TO:<${cmd.address}>`;\n break;\n case \"DATA\":\n line = \"DATA\";\n break;\n case \"DATA_BODY\":\n return encodeUtf8(applyDotStuffing(cmd.content));\n case \"QUIT\":\n line = \"QUIT\";\n break;\n case \"RSET\":\n line = \"RSET\";\n break;\n case \"NOOP\":\n line = \"NOOP\";\n break;\n }\n\n return encodeUtf8(`${line}\\r\\n`);\n}\n\n/**\n * Parse raw bytes from the server into an SMTPResponse.\n */\nexport function parseResponse(data: Uint8Array): SMTPResponse {\n const text = new TextDecoder().decode(data).trim();\n const lines = text.split(/\\r?\\n/);\n const lastLine = lines[lines.length - 1] ?? \"\";\n const match = lastLine.match(/^(\\d{3})([\\s-])(.*)$/);\n\n if (!match) {\n throw new SMTPError(\"Invalid SMTP response\", 0, \"PARSE\", text);\n }\n\n const code = Number.parseInt(match[1] ?? \"0\", 10);\n const message = lines.map((l) => l.replace(/^\\d{3}[\\s-]/, \"\")).join(\" \");\n\n return {\n code,\n message,\n isSuccess: code >= 200 && code < 300,\n isReady: code >= 300 && code < 400,\n isError: code >= 400,\n };\n}\n\n/**\n * Accumulate byte chunks until a complete SMTP response is received.\n */\nexport function accumulateResponse(chunks: Uint8Array[]): Uint8Array | null {\n if (chunks.length === 0) {\n return null;\n }\n\n const total = chunks.reduce((sum, c) => sum + c.length, 0);\n const combined = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n combined.set(chunk, offset);\n offset += chunk.length;\n }\n\n const text = new TextDecoder().decode(combined);\n const lines = text.split(/\\r?\\n/).filter((l) => l.length > 0);\n\n if (lines.length === 0) {\n return null;\n }\n\n const lastLine = lines[lines.length - 1] ?? \"\";\n if (/^\\d{3} /.test(lastLine)) {\n return combined;\n }\n\n return null;\n}\n\n/**\n * Select the best AUTH method from EHLO capability lines.\n * Priority: XOAUTH2 > CRAM-MD5 > LOGIN > PLAIN.\n */\nexport function selectAuthMethod(\n capabilities: string[],\n): \"LOGIN\" | \"PLAIN\" | \"CRAM-MD5\" | \"OAUTH2\" {\n const upper = capabilities.map((c) => c.toUpperCase());\n if (upper.some((c) => c.includes(\"AUTH\") && c.includes(\"XOAUTH2\"))) {\n return \"OAUTH2\";\n }\n if (upper.some((c) => c.includes(\"AUTH\") && c.includes(\"CRAM-MD5\"))) {\n return \"CRAM-MD5\";\n }\n if (upper.some((c) => c.includes(\"AUTH\") && c.includes(\"LOGIN\"))) {\n return \"LOGIN\";\n }\n if (upper.some((c) => c.includes(\"AUTH\") && c.includes(\"PLAIN\"))) {\n return \"PLAIN\";\n }\n throw new SMTPError(\"No supported AUTH method\", 0, \"EHLO\", capabilities.join(\" \"));\n}\n\n/**\n * Parse an EHLO multi-line response and extract capability keywords.\n */\nexport function parseEHLO(response: SMTPResponse): string[] {\n return response.message\n .split(/\\s+/)\n .flatMap((part) => part.split(/\\r?\\n/))\n .filter(Boolean);\n}\n\n/**\n * Assert that an SMTPResponse code is within the expected set.\n */\nexport function assertResponse(\n response: SMTPResponse,\n expectedCodes: number[],\n command: string,\n): void {\n if (!expectedCodes.includes(response.code)) {\n throw new SMTPError(\n `Unexpected SMTP response for ${command}`,\n response.code,\n command,\n response.message,\n );\n }\n}\n\nfunction applyDotStuffing(content: Uint8Array): string {\n const text = new TextDecoder().decode(content);\n const lines = text.split(/\\r?\\n/);\n const stuffed = lines.map((line) => (line.startsWith(\".\") ? `.${line}` : line));\n return `${stuffed.join(\"\\r\\n\")}\\r\\n.\\r\\n`;\n}\n\n/** Encode AUTH LOGIN password step (second base64 chunk). */\nexport function encodeAuthLoginPass(pass: string): Uint8Array {\n return encodeUtf8(`${encodeBase64(pass).replace(/\\r\\n/g, \"\")}\\r\\n`);\n}\n\n/** Encode AUTH LOGIN user step when sent separately after 334. */\nexport function encodeAuthLoginUser(user: string): Uint8Array {\n return encodeUtf8(`${encodeBase64(user).replace(/\\r\\n/g, \"\")}\\r\\n`);\n}\n\n/** Encode CRAM-MD5 response after challenge. */\nexport function encodeAuthCramResponse(response: string): Uint8Array {\n return encodeUtf8(`${response}\\r\\n`);\n}\n\n/** Encode raw SMTP line with CRLF. */\nexport function encodeLine(line: string): Uint8Array {\n return encodeUtf8(`${line}\\r\\n`);\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "5BEE29900FA6AC3664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assertSafeAddress,
|
|
2
3
|
extractEmails,
|
|
4
|
+
isValidEmail,
|
|
3
5
|
parseAddresses,
|
|
4
6
|
toMIMEHeader
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-f4c9ttmr.js";
|
|
6
8
|
import {
|
|
7
9
|
encodeBase64,
|
|
8
10
|
encodeHeader,
|
|
@@ -162,10 +164,19 @@ function sanitizeHeaderValue(value) {
|
|
|
162
164
|
return value.replace(/\r\n?|\n/g, " ").trim();
|
|
163
165
|
}
|
|
164
166
|
function sanitizeAddress(addr) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
assertSafeAddress(addr.address, "address");
|
|
168
|
+
if (addr.name !== undefined) {
|
|
169
|
+
assertSafeAddress(addr.name, "display name");
|
|
167
170
|
}
|
|
168
|
-
|
|
171
|
+
const address = addr.address.trim();
|
|
172
|
+
if (!isValidEmail(address)) {
|
|
173
|
+
throw new Error(`Invalid email address: ${JSON.stringify(addr.address)}`);
|
|
174
|
+
}
|
|
175
|
+
const sanitized = { address };
|
|
176
|
+
if (addr.name) {
|
|
177
|
+
sanitized.name = sanitizeHeaderValue(addr.name);
|
|
178
|
+
}
|
|
179
|
+
return sanitized;
|
|
169
180
|
}
|
|
170
181
|
var CRLF2 = `\r
|
|
171
182
|
`;
|
|
@@ -299,17 +310,20 @@ function formatAttachmentPart(attachment) {
|
|
|
299
310
|
if (!attachment.content || typeof attachment.content === "string") {
|
|
300
311
|
throw new Error(`Attachment "${attachment.filename}" requires Uint8Array content`);
|
|
301
312
|
}
|
|
313
|
+
const safeFilename = sanitizeHeaderValue(attachment.filename ?? "");
|
|
302
314
|
const headers = [
|
|
303
315
|
`Content-Type: ${attachment.contentType ?? "application/octet-stream"}`,
|
|
304
316
|
"Content-Transfer-Encoding: base64",
|
|
305
|
-
`Content-Disposition: ${attachment.inline ? "inline" : "attachment"}; filename="${
|
|
317
|
+
`Content-Disposition: ${attachment.inline ? "inline" : "attachment"}; filename="${safeFilename}"`
|
|
306
318
|
];
|
|
307
319
|
if (attachment.contentId) {
|
|
308
320
|
headers.push(`Content-ID: <${attachment.contentId}>`);
|
|
309
321
|
}
|
|
310
322
|
if (attachment.headers) {
|
|
311
323
|
for (const [key, value] of Object.entries(attachment.headers)) {
|
|
312
|
-
|
|
324
|
+
const safeKey = sanitizeHeaderValue(key);
|
|
325
|
+
const safeValue = sanitizeHeaderValue(value);
|
|
326
|
+
headers.push(`${safeKey}: ${safeValue}`);
|
|
313
327
|
}
|
|
314
328
|
}
|
|
315
329
|
return `${headers.join(CRLF2)}${CRLF2}${CRLF2}${encodeBase64(attachment.content)}`;
|
|
@@ -350,4 +364,4 @@ function foldHeader(name, value) {
|
|
|
350
364
|
|
|
351
365
|
export { buildMIME };
|
|
352
366
|
|
|
353
|
-
//# debugId=
|
|
367
|
+
//# debugId=AA495EEF0CB2364A64756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/dkim.ts", "../src/core/mime.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * DKIM (DomainKeys Identified Mail) signing per RFC 6376.\n * Supports RSA-SHA256 and Ed25519-SHA256 using Web Crypto.\n * Ed25519 requires Node.js ≥ 18.4, Bun ≥ 1.0, or Cloudflare Workers.\n *\n * @example\n * ```ts\n * import { signDKIM } from \"sently/core/dkim\";\n * const signed = await signDKIM(rawMessage, {\n * domainName: \"example.com\",\n * keySelector: \"2024\",\n * privateKey: \"-----BEGIN PRIVATE KEY-----\\\\n...\",\n * });\n * ```\n */\nimport { encodeBase64, encodeUtf8 } from \"./base64.js\";\nimport type { DKIMConfig } from \"./types.js\";\n\nconst CRLF = \"\\r\\n\";\nconst DEFAULT_HEADER_FIELDS = \"from:to:subject:date:message-id:mime-version:content-type\";\n\n/** Result of DKIM signing — the header line to prepend. */\nexport interface DKIMSignResult {\n /** Complete DKIM-Signature header line (without trailing CRLF). */\n header: string;\n}\n\n/**\n * Canonicalize headers using the relaxed algorithm (RFC 6376 §3.4.2).\n */\nexport function canonicalizeHeadersRelaxed(headers: string, fieldNames: string[]): string {\n const parsed = parseHeaders(headers);\n const lines: string[] = [];\n\n for (const name of fieldNames) {\n const key = name.toLowerCase().trim();\n const values = parsed.get(key);\n if (!values) {\n continue;\n }\n for (const value of values) {\n const unfolded = value.replace(/\\r?\\n/g, \"\").replace(/\\s+/g, \" \").trim();\n lines.push(`${key}:${unfolded}`);\n }\n }\n\n return lines.length > 0 ? `${lines.join(CRLF)}${CRLF}` : \"\";\n}\n\n/**\n * Canonicalize body using the relaxed algorithm (RFC 6376 §3.4.4).\n */\nexport function canonicalizeBodyRelaxed(body: string): string {\n const normalized = body.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const lines = normalized\n .split(\"\\n\")\n .map((line) => line.replace(/[ \\t]+$/g, \"\").replace(/[ \\t]+/g, \" \"))\n .filter((line) => line.length > 0);\n if (lines.length === 0) {\n return CRLF;\n }\n return `${lines.join(CRLF)}${CRLF}`;\n}\n\n/**\n * Import a PEM-encoded private key into a Web Crypto CryptoKey.\n */\nexport async function importPrivateKey(\n pem: string,\n algorithm: \"rsa-sha256\" | \"ed25519-sha256\",\n): Promise<CryptoKey> {\n if (algorithm === \"ed25519-sha256\" && /OPENSSH PRIVATE KEY/i.test(pem)) {\n throw new Error(\n \"Ed25519 keys must be in PKCS#8 PEM format (-----BEGIN PRIVATE KEY-----). Convert with: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key_pkcs8.pem\",\n );\n }\n\n const der = pemToDer(pem);\n\n const derBuffer = toArrayBuffer(der);\n\n try {\n if (algorithm === \"ed25519-sha256\") {\n return await crypto.subtle.importKey(\"pkcs8\", derBuffer, { name: \"Ed25519\" }, false, [\n \"sign\",\n ]);\n }\n return await crypto.subtle.importKey(\n \"pkcs8\",\n derBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n } catch (err) {\n if (algorithm === \"ed25519-sha256\" && err instanceof DOMException) {\n throw new Error(\"Ed25519 DKIM requires Node.js ≥ 18.4, Bun ≥ 1.0, or Cloudflare Workers\", {\n cause: err,\n });\n }\n throw err;\n }\n}\n\n/**\n * Sign a raw MIME message with DKIM.\n */\nexport async function signDKIM(\n rawMessage: Uint8Array,\n config: DKIMConfig,\n): Promise<DKIMSignResult> {\n const algorithm = config.algorithm ?? \"rsa-sha256\";\n const fieldList = (config.headerFieldNames ?? DEFAULT_HEADER_FIELDS)\n .split(\":\")\n .map((f) => f.trim().toLowerCase())\n .filter(Boolean);\n const skip = new Set(\n (config.skipFields ?? \"\")\n .split(\":\")\n .map((f) => f.trim().toLowerCase())\n .filter(Boolean),\n );\n const signFields = fieldList.filter((f) => !skip.has(f));\n\n const text = new TextDecoder().decode(rawMessage);\n const sep = text.indexOf(\"\\r\\n\\r\\n\");\n const headerPart = sep >= 0 ? text.slice(0, sep) : text;\n const bodyPart = sep >= 0 ? text.slice(sep + 4) : \"\";\n\n const bodyCanonical = canonicalizeBodyRelaxed(bodyPart);\n const bodyHash = await sha256Base64(encodeUtf8(bodyCanonical));\n\n const dkimAlgo = algorithm === \"ed25519-sha256\" ? \"ed25519-sha256\" : \"rsa-sha256\";\n const headerList = signFields.join(\":\");\n const timestamp = Math.floor(Date.now() / 1000);\n\n const dkimWithoutSig = [\n `v=1`,\n `a=${dkimAlgo}`,\n `c=relaxed/relaxed`,\n `d=${config.domainName}`,\n `s=${config.keySelector}`,\n `h=${headerList}`,\n `bh=${bodyHash}`,\n `b=`,\n `t=${timestamp}`,\n ].join(\"; \");\n\n const dkimHeaderName = \"dkim-signature\";\n const dkimHeaderValue = dkimWithoutSig;\n const headersWithDkim = `${headerPart}${CRLF}${dkimHeaderName}:${dkimHeaderValue}`;\n const canonical = canonicalizeHeadersRelaxed(headersWithDkim, [...signFields, dkimHeaderName]);\n\n const key = await importPrivateKey(config.privateKey, algorithm);\n const data = encodeUtf8(canonical);\n const signature = await signData(key, data, algorithm);\n const bValue = encodeBase64(signature).replace(/\\r\\n/g, \"\");\n\n const header = `DKIM-Signature: v=1; a=${dkimAlgo}; c=relaxed/relaxed; d=${config.domainName}; s=${config.keySelector}; h=${headerList}; bh=${bodyHash}; b=${bValue}; t=${timestamp}`;\n\n return { header };\n}\n\nfunction parseHeaders(headerBlock: string): Map<string, string[]> {\n const map = new Map<string, string[]>();\n const lines = headerBlock.split(/\\r?\\n/).filter((l) => l.length > 0);\n let currentName = \"\";\n let currentValue = \"\";\n\n const flush = (): void => {\n if (!currentName) {\n return;\n }\n const key = currentName.toLowerCase();\n const list = map.get(key) ?? [];\n list.push(currentValue);\n map.set(key, list);\n currentName = \"\";\n currentValue = \"\";\n };\n\n for (const line of lines) {\n if (/^[ \\t]/.test(line) && currentName) {\n currentValue += ` ${line.trim()}`;\n continue;\n }\n flush();\n const colon = line.indexOf(\":\");\n if (colon > 0) {\n currentName = line.slice(0, colon).trim();\n currentValue = line.slice(colon + 1).trim();\n }\n }\n flush();\n return map;\n}\n\nfunction pemToDer(pem: string): Uint8Array {\n const lines = pem\n .replace(/-----BEGIN[^-]+-----/g, \"\")\n .replace(/-----END[^-]+-----/g, \"\")\n .replace(/\\s/g, \"\");\n const binary = atob(lines);\n const der = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n der[i] = binary.charCodeAt(i);\n }\n return der;\n}\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nasync function sha256Base64(data: Uint8Array): Promise<string> {\n const hash = await crypto.subtle.digest(\"SHA-256\", toArrayBuffer(data));\n return encodeBase64(new Uint8Array(hash)).replace(/\\r\\n/g, \"\");\n}\n\nasync function signData(\n key: CryptoKey,\n data: Uint8Array,\n algorithm: \"rsa-sha256\" | \"ed25519-sha256\",\n): Promise<Uint8Array> {\n const buf = toArrayBuffer(data);\n if (algorithm === \"ed25519-sha256\") {\n const sig = await crypto.subtle.sign(\"Ed25519\", key, buf);\n return new Uint8Array(sig);\n }\n const sig = await crypto.subtle.sign({ name: \"RSASSA-PKCS1-v1_5\" }, key, buf);\n return new Uint8Array(sig);\n}\n",
|
|
6
|
+
"// src/core/mime.ts\nimport {\n assertSafeAddress,\n extractEmails,\n isValidEmail,\n parseAddresses,\n toMIMEHeader,\n} from \"./address.js\";\nimport { encodeBase64, encodeHeader, encodeUtf8 } from \"./base64.js\";\nimport { signDKIM } from \"./dkim.js\";\nimport type { Address, Attachment, DKIMConfig, Envelope, MailOptions } from \"./types.js\";\n\nfunction sanitizeHeaderValue(value: string): string {\n return value.replace(/\\r\\n?|\\n/g, \" \").trim();\n}\n\nfunction sanitizeAddress(addr: Address): Address {\n // Security check runs on the raw value FIRST and fails closed: any control\n // character (CR/LF/NUL/…) is rejected outright rather than stripped and\n // accepted. parseAddresses() already enforces this upstream; re-asserting\n // here keeps the function safe if it is ever called directly.\n assertSafeAddress(addr.address, \"address\");\n if (addr.name !== undefined) {\n assertSafeAddress(addr.name, \"display name\");\n }\n\n // Only ordinary surrounding whitespace is trimmed — the address identity is\n // never altered (no CR/LF-to-space repair).\n const address = addr.address.trim();\n if (!isValidEmail(address)) {\n throw new Error(`Invalid email address: ${JSON.stringify(addr.address)}`);\n }\n\n const sanitized: Address = { address };\n if (addr.name) {\n sanitized.name = sanitizeHeaderValue(addr.name);\n }\n return sanitized;\n}\n\n/** Result of building a complete MIME message. */\nexport interface MIMEBuildResult {\n raw: Uint8Array;\n envelope: Envelope;\n messageId: string;\n size: number;\n}\n\nconst CRLF = \"\\r\\n\";\n\n/**\n * Build a complete MIME message as a Uint8Array ready for SMTP DATA.\n * When `dkim` is provided, signs the message and prepends the DKIM-Signature header.\n */\nexport async function buildMIME(options: MailOptions, dkim?: DKIMConfig): Promise<MIMEBuildResult> {\n const messageId = options.messageId ?? generateMessageId();\n const date = (options.date ?? new Date()).toUTCString();\n const fromAddrs = parseAddresses(options.from);\n const toAddrs = parseAddresses(options.to);\n const ccAddrs = options.cc ? parseAddresses(options.cc) : [];\n\n if (fromAddrs.length === 0) {\n throw new Error(\"Missing from address\");\n }\n if (toAddrs.length === 0) {\n throw new Error(\"Missing to address\");\n }\n\n const envelope: Envelope = {\n from: fromAddrs[0]?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n };\n\n const attachments = options.attachments ?? [];\n const inlineAttachments = attachments.filter((a) => a.inline || a.contentId);\n const regularAttachments = attachments.filter((a) => !a.inline && !a.contentId);\n\n let root = buildSimpleBody(options);\n\n if (inlineAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/related; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(buildSimpleBody(options)),\n ...inlineAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n if (regularAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/mixed; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(root),\n ...regularAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n const headers: string[] = [\n foldHeader(\"From\", fromAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n foldHeader(\"To\", toAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n ];\n\n if (ccAddrs.length > 0) {\n headers.push(foldHeader(\"Cc\", ccAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")));\n }\n\n if (options.replyTo) {\n headers.push(\n foldHeader(\n \"Reply-To\",\n parseAddresses(options.replyTo)\n .map((a) => toMIMEHeader(sanitizeAddress(a)))\n .join(\", \"),\n ),\n );\n }\n\n headers.push(\n foldHeader(\"Subject\", encodeHeader(sanitizeHeaderValue(options.subject))),\n foldHeader(\"Date\", date),\n foldHeader(\"Message-ID\", messageId),\n \"MIME-Version: 1.0\",\n );\n\n if (options.priority === \"high\") {\n headers.push(\"X-Priority: 1\", \"Importance: high\");\n } else if (options.priority === \"low\") {\n headers.push(\"X-Priority: 5\", \"Importance: low\");\n }\n\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.push(foldHeader(sanitizeHeaderValue(key), sanitizeHeaderValue(value)));\n }\n }\n\n headers.push(`Content-Type: ${root.contentType}`);\n if (root.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${root.contentTransferEncoding}`);\n }\n\n const rawText = `${headers.join(CRLF)}${CRLF}${CRLF}${root.content}`;\n let raw = encodeUtf8(rawText);\n\n if (dkim) {\n const { header } = await signDKIM(raw, dkim);\n const signedText = `${header}${CRLF}${rawText}`;\n raw = encodeUtf8(signedText);\n }\n\n return { raw, envelope, messageId, size: raw.length };\n}\n\ninterface SimpleBody {\n contentType: string;\n contentTransferEncoding?: string;\n content: string;\n}\n\nfunction buildSimpleBody(options: MailOptions): SimpleBody {\n const hasText = Boolean(options.text);\n const hasHtml = Boolean(options.html);\n\n if (hasText && hasHtml) {\n const boundary = generateBoundary();\n return {\n contentType: `multipart/alternative; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatSimplePart({\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n }),\n formatSimplePart({\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n }),\n ]),\n };\n }\n\n if (hasHtml) {\n return {\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n };\n }\n\n return {\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n };\n}\n\nfunction formatSimplePart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatNestedPart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatAttachmentPart(attachment: Attachment): string {\n if (!attachment.content || typeof attachment.content === \"string\") {\n throw new Error(`Attachment \"${attachment.filename}\" requires Uint8Array content`);\n }\n\n const safeFilename = sanitizeHeaderValue(attachment.filename ?? \"\");\n const headers = [\n `Content-Type: ${attachment.contentType ?? \"application/octet-stream\"}`,\n \"Content-Transfer-Encoding: base64\",\n `Content-Disposition: ${attachment.inline ? \"inline\" : \"attachment\"}; filename=\"${safeFilename}\"`,\n ];\n\n if (attachment.contentId) {\n headers.push(`Content-ID: <${attachment.contentId}>`);\n }\n\n if (attachment.headers) {\n for (const [key, value] of Object.entries(attachment.headers)) {\n const safeKey = sanitizeHeaderValue(key);\n const safeValue = sanitizeHeaderValue(value);\n headers.push(`${safeKey}: ${safeValue}`);\n }\n }\n\n return `${headers.join(CRLF)}${CRLF}${CRLF}${encodeBase64(attachment.content)}`;\n}\n\nfunction assembleMultipart(boundary: string, parts: string[]): string {\n const segments = parts.map((part) => `--${boundary}${CRLF}${part}`);\n segments.push(`--${boundary}--`);\n return segments.join(CRLF);\n}\n\nfunction generateMessageId(): string {\n const random = crypto.getRandomValues(new Uint8Array(8));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `<${Date.now()}.${hex}@sently>`;\n}\n\nfunction generateBoundary(): string {\n const random = crypto.getRandomValues(new Uint8Array(12));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `----sently_${hex}`;\n}\n\nfunction foldHeader(name: string, value: string): string {\n const line = `${name}: ${value}`;\n if (line.length <= 76) {\n return line;\n }\n\n const chunks: string[] = [];\n let remaining = line;\n\n while (remaining.length > 76) {\n let breakAt = remaining.lastIndexOf(\" \", 76);\n if (breakAt <= name.length + 1) {\n breakAt = 76;\n }\n chunks.push(remaining.slice(0, breakAt));\n remaining = ` ${remaining.slice(breakAt).trimStart()}`;\n }\n chunks.push(remaining);\n\n return chunks.join(`${CRLF} `);\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "AA495EEF0CB2364A64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/core/address.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { Address, AddressInput } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Assert that a raw address or display-name value contains no forbidden
|
|
4
|
+
* control characters. Throws immediately (fail closed) — the library never
|
|
5
|
+
* attempts to strip or rewrite hostile input into an accepted value.
|
|
6
|
+
*
|
|
7
|
+
* The check is intentionally performed on the RAW input, before any trimming
|
|
8
|
+
* or normalization, so hostile values are rejected rather than repaired.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The raw, untransformed string to validate.
|
|
11
|
+
* @param label - Field label used in the error message (e.g. "address").
|
|
12
|
+
* @throws {Error} If the value contains any forbidden control character.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSafeAddress(value: string, label?: string): void;
|
|
2
15
|
/**
|
|
3
16
|
* Normalize any AddressInput form into Address[].
|
|
17
|
+
*
|
|
18
|
+
* Every address and display name is validated against control-character
|
|
19
|
+
* injection before any transformation. This is the single chokepoint shared
|
|
20
|
+
* by all transports and address fields (From, To, Cc, Bcc, Reply-To), so the
|
|
21
|
+
* protection is uniform and secure by default.
|
|
22
|
+
*
|
|
23
|
+
* @throws {Error} If any address or name contains a forbidden control character.
|
|
4
24
|
*/
|
|
5
25
|
export declare function parseAddresses(input: AddressInput): Address[];
|
|
6
26
|
/**
|
|
@@ -9,6 +29,12 @@ export declare function parseAddresses(input: AddressInput): Address[];
|
|
|
9
29
|
export declare function toEnvelope(address: Address): string;
|
|
10
30
|
/**
|
|
11
31
|
* Format an Address for use in a MIME header (From, To, CC, etc.).
|
|
32
|
+
*
|
|
33
|
+
* Re-validates the address and name at render time so a header can never be
|
|
34
|
+
* emitted with an embedded control character, even if the {@link Address} was
|
|
35
|
+
* constructed without going through {@link parseAddresses}.
|
|
36
|
+
*
|
|
37
|
+
* @throws {Error} If the address or name contains a forbidden control character.
|
|
12
38
|
*/
|
|
13
39
|
export declare function toMIMEHeader(address: Address): string;
|
|
14
40
|
/**
|
|
@@ -17,5 +43,9 @@ export declare function toMIMEHeader(address: Address): string;
|
|
|
17
43
|
export declare function extractEmails(input: AddressInput): string[];
|
|
18
44
|
/**
|
|
19
45
|
* Basic email format validation (format only, no DNS lookup).
|
|
46
|
+
*
|
|
47
|
+
* Rejects any control character (including CR, LF, tab, and NUL) before
|
|
48
|
+
* applying the structural check, so a "valid" result is always safe to place
|
|
49
|
+
* into a header or SMTP command.
|
|
20
50
|
*/
|
|
21
51
|
export declare function isValidEmail(email: string): boolean;
|
package/dist/core/smtp.js
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -155,6 +155,13 @@ export interface SMTPConfig extends PoolConfig {
|
|
|
155
155
|
port?: number;
|
|
156
156
|
secure?: boolean;
|
|
157
157
|
auth?: SMTPAuth;
|
|
158
|
+
/**
|
|
159
|
+
* Refuse to authenticate over a non-TLS connection.
|
|
160
|
+
* When true, throws SMTPError before sending AUTH if the connection
|
|
161
|
+
* is not encrypted. Prevents credential exposure on STARTTLS-stripping
|
|
162
|
+
* MITM attacks. Default: true when auth is set, false otherwise.
|
|
163
|
+
*/
|
|
164
|
+
requireTLS?: boolean;
|
|
158
165
|
tls?: TLSOptions;
|
|
159
166
|
connectionTimeout?: number;
|
|
160
167
|
greetingTimeout?: number;
|
package/dist/detect.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SMTPPool
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-mp5c9bfd.js";
|
|
4
4
|
import {
|
|
5
5
|
SMTPTransport
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7fqv71z1.js";
|
|
7
7
|
import"./chunk-ym3zzv8b.js";
|
|
8
|
-
import"./chunk-
|
|
9
|
-
import"./chunk-
|
|
10
|
-
import"./chunk-
|
|
8
|
+
import"./chunk-x3szga4k.js";
|
|
9
|
+
import"./chunk-tymfm441.js";
|
|
10
|
+
import"./chunk-f4c9ttmr.js";
|
|
11
11
|
import"./chunk-794hc3m4.js";
|
|
12
12
|
import {
|
|
13
13
|
__require
|
package/dist/pool/pool.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RateLimiter,
|
|
3
3
|
SMTPPool
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import"../chunk-
|
|
4
|
+
} from "../chunk-mp5c9bfd.js";
|
|
5
|
+
import"../chunk-7fqv71z1.js";
|
|
6
6
|
import"../chunk-ym3zzv8b.js";
|
|
7
|
-
import"../chunk-
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-
|
|
7
|
+
import"../chunk-x3szga4k.js";
|
|
8
|
+
import"../chunk-tymfm441.js";
|
|
9
|
+
import"../chunk-f4c9ttmr.js";
|
|
10
10
|
import"../chunk-794hc3m4.js";
|
|
11
11
|
import"../chunk-v0bahtg2.js";
|
|
12
12
|
export {
|
package/dist/transports/brevo.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMIME
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-x3szga4k.js";
|
|
4
4
|
import {
|
|
5
5
|
extractEmails,
|
|
6
6
|
resolveAttachments
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-f4c9ttmr.js";
|
|
8
8
|
import"../chunk-794hc3m4.js";
|
|
9
9
|
import {
|
|
10
10
|
__require
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { Attachment } from "../core/types.js";
|
|
2
2
|
/** Options for {@link resolveAttachments}. */
|
|
3
3
|
export interface ResolveAttachmentsOptions {
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* If set, attachment paths must resolve within this directory.
|
|
6
|
+
* Prevents path traversal via `..` segments and sibling-directory
|
|
7
|
+
* prefix matches. Opt-in only.
|
|
8
|
+
*
|
|
9
|
+
* Note: this check uses `node:path` `resolve()`, which does NOT
|
|
10
|
+
* dereference symlinks. A symlink located inside `basePath` that
|
|
11
|
+
* points outside of it will pass this check. If symlink traversal is
|
|
12
|
+
* a concern, resolve paths with `fs.realpath()` before passing them in.
|
|
13
|
+
*/
|
|
6
14
|
basePath?: string;
|
|
7
15
|
}
|
|
8
16
|
/**
|
package/dist/transports/retry.js
CHANGED
package/dist/transports/ses.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMIME
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-x3szga4k.js";
|
|
4
4
|
import {
|
|
5
5
|
extractEmails,
|
|
6
6
|
parseAddresses,
|
|
7
7
|
resolveAttachments,
|
|
8
8
|
toMIMEHeader
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-f4c9ttmr.js";
|
|
10
10
|
import {
|
|
11
11
|
encodeBase64
|
|
12
12
|
} from "../chunk-794hc3m4.js";
|
|
@@ -27,7 +27,7 @@ async function hmacSHA256(key, data) {
|
|
|
27
27
|
async function signRequest(request) {
|
|
28
28
|
const { method, url, body, credentials } = request;
|
|
29
29
|
const parsed = new URL(url);
|
|
30
|
-
const amzDate = request._date ?? `${new Date().toISOString().replace(/[-:]/g, "").slice(0,
|
|
30
|
+
const amzDate = request._date ?? `${new Date().toISOString().replace(/[-:]/g, "").slice(0, 15)}Z`;
|
|
31
31
|
const dateStamp = amzDate.slice(0, 8);
|
|
32
32
|
const headers = {
|
|
33
33
|
...Object.fromEntries(Object.entries(request.headers).map(([key, value]) => [key.toLowerCase(), value.trim()])),
|
|
@@ -250,4 +250,4 @@ export {
|
|
|
250
250
|
SESError
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
-
//# debugId=
|
|
253
|
+
//# debugId=E3DA50CEC668691D64756E2164756E21
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/core/sigv4.ts", "../src/transports/ses.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * AWS Signature Version 4 signing using Web Crypto (HMAC-SHA256).\n * Works on Node.js, Bun, Deno, and Cloudflare Workers.\n * No external dependencies.\n *\n * @example\n * ```ts\n * import { signRequest } from \"sently/core/sigv4\";\n * const signed = await signRequest({\n * method: \"POST\",\n * url: \"https://email.us-east-1.amazonaws.com/v2/email/outbound-emails\",\n * headers: { \"content-type\": \"application/json\" },\n * body: '{\"...\"}',\n * credentials: { accessKeyId, secretAccessKey, region: \"us-east-1\", service: \"ses\" },\n * });\n * ```\n */\n\n/** AWS credentials and signing scope for SigV4. */\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n sessionToken?: string;\n}\n\n/** HTTP request to sign with AWS Signature Version 4. */\nexport interface SigV4Request {\n method: string;\n url: string;\n headers: Record<string, string>;\n body: string;\n credentials: SigV4Credentials;\n /** Override datetime for testing. Full 'YYYYMMDDTHHMMSSZ' when provided. */\n _date?: string;\n}\n\n/** Signed request headers including Authorization. */\nexport interface SigV4Result {\n /** All headers including Authorization, x-amz-date, and x-amz-security-token */\n headers: Record<string, string>;\n}\n\nconst encoder = new TextEncoder();\n\n/**\n * Compute SHA-256 hash of a string using Web Crypto.\n * Returns lowercase hex string.\n * @internal\n */\nexport async function sha256Hex(data: string): Promise<string> {\n const hash = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n return bytesToHex(new Uint8Array(hash));\n}\n\n/**\n * Compute HMAC-SHA256 using Web Crypto.\n * @internal\n */\nexport async function hmacSHA256(key: Uint8Array | string, data: string): Promise<Uint8Array> {\n const keyBytes = typeof key === \"string\" ? encoder.encode(key) : new Uint8Array(key);\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyBytes,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, encoder.encode(data));\n return new Uint8Array(signature);\n}\n\n/**\n * Sign an HTTP request with AWS Signature Version 4.\n * Returns the complete set of headers to include in the request.\n */\nexport async function signRequest(request: SigV4Request): Promise<SigV4Result> {\n const { method, url, body, credentials } = request;\n const parsed = new URL(url);\n const amzDate = request._date ?? `${new Date().toISOString().replace(/[-:]/g, \"\").slice(0,
|
|
5
|
+
"/**\n * @module\n * AWS Signature Version 4 signing using Web Crypto (HMAC-SHA256).\n * Works on Node.js, Bun, Deno, and Cloudflare Workers.\n * No external dependencies.\n *\n * @example\n * ```ts\n * import { signRequest } from \"sently/core/sigv4\";\n * const signed = await signRequest({\n * method: \"POST\",\n * url: \"https://email.us-east-1.amazonaws.com/v2/email/outbound-emails\",\n * headers: { \"content-type\": \"application/json\" },\n * body: '{\"...\"}',\n * credentials: { accessKeyId, secretAccessKey, region: \"us-east-1\", service: \"ses\" },\n * });\n * ```\n */\n\n/** AWS credentials and signing scope for SigV4. */\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n sessionToken?: string;\n}\n\n/** HTTP request to sign with AWS Signature Version 4. */\nexport interface SigV4Request {\n method: string;\n url: string;\n headers: Record<string, string>;\n body: string;\n credentials: SigV4Credentials;\n /** Override datetime for testing. Full 'YYYYMMDDTHHMMSSZ' when provided. */\n _date?: string;\n}\n\n/** Signed request headers including Authorization. */\nexport interface SigV4Result {\n /** All headers including Authorization, x-amz-date, and x-amz-security-token */\n headers: Record<string, string>;\n}\n\nconst encoder = new TextEncoder();\n\n/**\n * Compute SHA-256 hash of a string using Web Crypto.\n * Returns lowercase hex string.\n * @internal\n */\nexport async function sha256Hex(data: string): Promise<string> {\n const hash = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n return bytesToHex(new Uint8Array(hash));\n}\n\n/**\n * Compute HMAC-SHA256 using Web Crypto.\n * @internal\n */\nexport async function hmacSHA256(key: Uint8Array | string, data: string): Promise<Uint8Array> {\n const keyBytes = typeof key === \"string\" ? encoder.encode(key) : new Uint8Array(key);\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyBytes,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, encoder.encode(data));\n return new Uint8Array(signature);\n}\n\n/**\n * Sign an HTTP request with AWS Signature Version 4.\n * Returns the complete set of headers to include in the request.\n */\nexport async function signRequest(request: SigV4Request): Promise<SigV4Result> {\n const { method, url, body, credentials } = request;\n const parsed = new URL(url);\n const amzDate = request._date ?? `${new Date().toISOString().replace(/[-:]/g, \"\").slice(0, 15)}Z`;\n const dateStamp = amzDate.slice(0, 8);\n\n const headers: Record<string, string> = {\n ...Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [key.toLowerCase(), value.trim()]),\n ),\n host: parsed.host,\n \"x-amz-date\": amzDate,\n };\n\n if (credentials.sessionToken) {\n headers[\"x-amz-security-token\"] = credentials.sessionToken;\n }\n\n const signedHeaderNames = Object.keys(headers)\n .map((name) => name.toLowerCase())\n .sort();\n const signedHeaders = signedHeaderNames.join(\";\");\n const canonicalHeaders = `${signedHeaderNames.map((name) => `${name}:${headers[name]}`).join(\"\\n\")}\\n`;\n const canonicalQuery = normalizeQuery(parsed.searchParams);\n const payloadHash = await sha256Hex(body);\n const canonicalRequest = [\n method.toUpperCase(),\n canonicalUri(parsed.pathname),\n canonicalQuery,\n canonicalHeaders,\n signedHeaders,\n payloadHash,\n ].join(\"\\n\");\n\n const credentialScope = `${dateStamp}/${credentials.region}/${credentials.service}/aws4_request`;\n const stringToSign = [\n \"AWS4-HMAC-SHA256\",\n amzDate,\n credentialScope,\n await sha256Hex(canonicalRequest),\n ].join(\"\\n\");\n\n const signingKey = await deriveSigningKey(\n credentials.secretAccessKey,\n dateStamp,\n credentials.region,\n credentials.service,\n );\n const signature = bytesToHex(await hmacSHA256(signingKey, stringToSign));\n const authorization = [\n `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${credentialScope}`,\n `SignedHeaders=${signedHeaders}`,\n `Signature=${signature}`,\n ].join(\", \");\n\n return {\n headers: {\n ...headers,\n Authorization: authorization,\n },\n };\n}\n\nasync function deriveSigningKey(\n secretAccessKey: string,\n dateStamp: string,\n region: string,\n service: string,\n): Promise<Uint8Array> {\n const kDate = await hmacSHA256(`AWS4${secretAccessKey}`, dateStamp);\n const kRegion = await hmacSHA256(kDate, region);\n const kService = await hmacSHA256(kRegion, service);\n return hmacSHA256(kService, \"aws4_request\");\n}\n\nfunction canonicalUri(pathname: string): string {\n if (!pathname || pathname === \"/\") {\n return \"/\";\n }\n return pathname\n .split(\"/\")\n .map((segment) => encodeURIComponent(decodeURIComponent(segment)))\n .join(\"/\");\n}\n\nfunction normalizeQuery(searchParams: URLSearchParams): string {\n const pairs: string[] = [];\n for (const [key, value] of searchParams.entries()) {\n pairs.push(`${encodeRfc3986(key)}=${encodeRfc3986(value)}`);\n }\n pairs.sort();\n return pairs.join(\"&\");\n}\n\nfunction encodeRfc3986(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n",
|
|
6
6
|
"/**\n * @module\n * AWS SES v2 HTTP transport for sently.\n * Signs requests with AWS Signature Version 4 using Web Crypto.\n * Works on Node.js, Bun, Deno, and Cloudflare Workers.\n *\n * @example\n * ```ts\n * import { SESTransport } from \"sently/transports/ses\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SESTransport({\n * accessKeyId: process.env.AWS_ACCESS_KEY_ID!,\n * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,\n * region: \"us-east-1\",\n * }),\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { buildMIME } from \"../core/mime.js\";\nimport { signRequest } from \"../core/sigv4.js\";\nimport type { MailOptions, SESConfig, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Error thrown when the AWS SES API returns a non-success response. */\nexport class SESError extends Error {\n /** Creates an AWS SES API error with status code, error code, and request ID. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n public readonly requestId: string,\n ) {\n super(message);\n this.name = \"SESError\";\n }\n}\n\n/**\n * AWS SES v2 HTTP API transport.\n */\nexport class SESTransport implements Transport {\n private readonly accessKeyId: string;\n private readonly secretAccessKey: string;\n private readonly region: string;\n private readonly sessionToken: string | undefined;\n private readonly dkim: SESConfig[\"dkim\"];\n\n /** Creates an SES transport with AWS credentials. */\n constructor(config: SESConfig) {\n this.accessKeyId = config.accessKeyId;\n this.secretAccessKey = config.secretAccessKey;\n this.region = config.region ?? \"us-east-1\";\n this.sessionToken = config.sessionToken;\n this.dkim = config.dkim;\n }\n\n /** Sends an email via the AWS SES v2 HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const resolvedOptions = { ...options, attachments };\n const from = parseAddresses(options.from)[0];\n const fromEmail = from ? toMIMEHeader(from) : \"\";\n const toEmails = extractEmails(options.to);\n const ccEmails = options.cc ? extractEmails(options.cc) : [];\n const bccEmails = options.bcc ? extractEmails(options.bcc) : [];\n\n const destination = {\n ToAddresses: toEmails,\n CcAddresses: ccEmails,\n BccAddresses: bccEmails,\n };\n\n let requestBody: Record<string, unknown>;\n\n if (attachments.length > 0) {\n const mime = await buildMIME(resolvedOptions, this.dkim);\n requestBody = {\n FromEmailAddress: fromEmail,\n Destination: destination,\n Content: {\n Raw: {\n Data: encodeBase64(mime.raw).replace(/\\r\\n/g, \"\"),\n },\n },\n };\n } else {\n requestBody = {\n FromEmailAddress: fromEmail,\n Destination: destination,\n Content: {\n Simple: {\n Subject: { Data: options.subject, Charset: \"UTF-8\" },\n Body: {\n ...(options.text ? { Text: { Data: options.text, Charset: \"UTF-8\" } } : {}),\n ...(options.html ? { Html: { Data: options.html, Charset: \"UTF-8\" } } : {}),\n },\n },\n },\n };\n }\n\n const body = JSON.stringify(requestBody);\n const url = `https://email.${this.region}.amazonaws.com/v2/email/outbound-emails`;\n const signed = await signRequest({\n method: \"POST\",\n url,\n headers: {\n \"content-type\": \"application/json\",\n },\n body,\n credentials: {\n accessKeyId: this.accessKeyId,\n secretAccessKey: this.secretAccessKey,\n region: this.region,\n service: \"ses\",\n ...(this.sessionToken ? { sessionToken: this.sessionToken } : {}),\n },\n });\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: signed.headers,\n body,\n });\n\n const payload = (await response.json()) as {\n MessageId?: string;\n message?: string;\n Code?: string;\n };\n\n if (!response.ok) {\n throw new SESError(\n payload.message ?? \"SES API error\",\n response.status,\n payload.Code ?? \"\",\n response.headers.get(\"x-amzn-requestid\") ?? \"\",\n );\n }\n\n const messageId = payload.MessageId ?? \"\";\n return {\n messageId,\n accepted: [...toEmails, ...ccEmails, ...bccEmails],\n rejected: [],\n response: `MessageId: ${messageId}`,\n envelope: {\n from: from?.address ?? \"\",\n to: toEmails,\n },\n };\n }\n\n /** Verifies AWS credentials by listing SES configuration sets. */\n async verify(): Promise<VerifyResult> {\n try {\n const url = `https://email.${this.region}.amazonaws.com/v2/email/configuration-sets`;\n const signed = await signRequest({\n method: \"GET\",\n url,\n headers: {},\n body: \"\",\n credentials: {\n accessKeyId: this.accessKeyId,\n secretAccessKey: this.secretAccessKey,\n region: this.region,\n service: \"ses\",\n ...(this.sessionToken ? { sessionToken: this.sessionToken } : {}),\n },\n });\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: signed.headers,\n });\n\n const payload = (await response.json().catch(() => ({}))) as { message?: string };\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"ses\",\n message: payload.message ?? `HTTP ${response.status}`,\n };\n }\n\n return { ok: true, provider: \"ses\", message: \"Credentials are valid\" };\n } catch (err) {\n return {\n ok: false,\n provider: \"ses\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
7
7
|
],
|
|
8
8
|
"mappings": "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",
|
|
9
|
-
"debugId": "
|
|
9
|
+
"debugId": "E3DA50CEC668691D64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/transports/smtp.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
openSMTPSession,
|
|
6
6
|
readSMTPResponse,
|
|
7
7
|
resolveSMTPConfig
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-7fqv71z1.js";
|
|
9
9
|
import"../chunk-ym3zzv8b.js";
|
|
10
|
-
import"../chunk-
|
|
10
|
+
import"../chunk-x3szga4k.js";
|
|
11
11
|
import {
|
|
12
12
|
encodeLine
|
|
13
|
-
} from "../chunk-
|
|
14
|
-
import"../chunk-
|
|
13
|
+
} from "../chunk-tymfm441.js";
|
|
14
|
+
import"../chunk-f4c9ttmr.js";
|
|
15
15
|
import"../chunk-794hc3m4.js";
|
|
16
16
|
import"../chunk-v0bahtg2.js";
|
|
17
17
|
export {
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/transports/smtp.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n private readonly config: ResolvedSMTPConfig;\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n host: string;\n port: number;\n secure: boolean;\n auth?: SMTPConfig[\"auth\"];\n tls?: SMTPConfig[\"tls\"];\n dkim?: SMTPConfig[\"dkim\"];\n connectionTimeout?: number;\n greetingTimeout?: number;\n socketTimeout?: number;\n direct?: boolean;\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "44FB4148CD9604AA64756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/core/address.ts", "../src/transports/resolve-attachments.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"// src/core/address.ts\nimport { encodeHeader } from \"./base64.js\";\nimport type { Address, AddressInput } from \"./types.js\";\n\n/**\n * Normalize any AddressInput form into Address[].\n */\nexport function parseAddresses(input: AddressInput): Address[] {\n if (Array.isArray(input)) {\n return input.flatMap((item) => parseAddresses(item));\n }\n\n if (typeof input === \"object\") {\n return [{ ...input }];\n }\n\n const trimmed = input.trim();\n if (!trimmed) {\n return [];\n }\n\n return splitAddressList(trimmed).map(parseSingleAddress);\n}\n\n/**\n * Format an Address for SMTP envelope commands (MAIL FROM / RCPT TO).\n */\nexport function toEnvelope(address: Address): string {\n return address.address;\n}\n\n/**\n * Format an Address for use in a MIME header (From, To, CC, etc.).\n */\nexport function toMIMEHeader(address: Address): string {\n if (address.name) {\n const name = encodeHeader(address.name);\n return `${name} <${address.address}>`;\n }\n return address.address;\n}\n\n/**\n * Extract plain email strings from any AddressInput.\n */\nexport function extractEmails(input: AddressInput): string[] {\n return parseAddresses(input).map((addr) => addr.address);\n}\n\n/**\n * Basic email format validation (format only, no DNS lookup).\n */\nexport function isValidEmail(email: string): boolean {\n if (/[\\r\\n\\t]/.test(email)) return false;\n return /^[^\\s@<>]+@[^\\s@<>]+\\.[^\\s@<>]+$/.test(email);\n}\n\nfunction splitAddressList(input: string): string[] {\n const parts: string[] = [];\n let current = \"\";\n let inQuotes = false;\n let inAngle = false;\n\n for (let i = 0; i < input.length; i++) {\n const char = input[i] ?? \"\";\n if (char === '\"' && input[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n current += char;\n continue;\n }\n if (char === \"<\" && !inQuotes) {\n inAngle = true;\n current += char;\n continue;\n }\n if (char === \">\" && !inQuotes) {\n inAngle = false;\n current += char;\n continue;\n }\n if (char === \",\" && !inQuotes && !inAngle) {\n if (current.trim()) {\n parts.push(current.trim());\n }\n current = \"\";\n continue;\n }\n current += char;\n }\n\n if (current.trim()) {\n parts.push(current.trim());\n }\n\n return parts;\n}\n\nfunction parseSingleAddress(input: string): Address {\n const trimmed = input.trim();\n\n const angleMatch = trimmed.match(/^(?:\"([^\"]*)\"|([^<]*?))\\s*<([^>]+)>$/);\n if (angleMatch) {\n const name = (angleMatch[1] ?? angleMatch[2] ?? \"\").trim();\n const address = (angleMatch[3] ?? \"\").trim();\n if (name) {\n return { name, address };\n }\n return { address };\n }\n\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n return { address: trimmed.slice(1, -1) };\n }\n\n return { address: trimmed };\n}\n",
|
|
6
|
-
"import type { Attachment } from \"../core/types.js\";\n\n/** Options for {@link resolveAttachments}. */\nexport interface ResolveAttachmentsOptions {\n /** If set, attachment paths must resolve within this directory.\n * Prevents symlink escape and path traversal. Opt-in only. */\n basePath?: string;\n}\n\n/**\n * Resolve attachment.path to in-memory Uint8Array content.\n * @throws When attachment.path is used on runtimes without node:fs/promises\n */\nexport async function resolveAttachments(\n attachments: Attachment[] | undefined,\n options?: ResolveAttachmentsOptions,\n): Promise<Attachment[]> {\n const list = attachments ?? [];\n const resolved: Attachment[] = [];\n\n for (const attachment of list) {\n if (attachment.content instanceof Uint8Array) {\n resolved.push(attachment);\n continue;\n }\n\n if (attachment.path) {\n let fs: typeof import(\"node:fs/promises\");\n try {\n fs = await import(\"node:fs/promises\");\n } catch {\n throw new Error(\n \"attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead\",\n );\n }\n\n if (options?.basePath) {\n const { resolve } = await import(\"node:path\");\n const resolvedPath = resolve(attachment.path);\n const resolvedBase = resolve(options.basePath);\n if (!resolvedPath.startsWith(resolvedBase)) {\n throw new Error(\n `[sently] Attachment path \"${resolvedPath}\" escapes basePath \"${options.basePath}\". ` +\n \"Use absolute paths within the allowed directory.\",\n );\n }\n }\n\n const data = await fs.readFile(attachment.path);\n const { path: _path, ...rest } = attachment;\n resolved.push({ ...rest, content: new Uint8Array(data) });\n continue;\n }\n\n if (typeof attachment.content === \"string\") {\n resolved.push(attachment);\n continue;\n }\n\n resolved.push(attachment);\n }\n\n return resolved;\n}\n"
|
|
7
|
-
],
|
|
8
|
-
"mappings": "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",
|
|
9
|
-
"debugId": "7C59D40B68EA994E64756E2164756E21",
|
|
10
|
-
"names": []
|
|
11
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/core/dkim.ts", "../src/core/mime.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * DKIM (DomainKeys Identified Mail) signing per RFC 6376.\n * Supports RSA-SHA256 and Ed25519-SHA256 using Web Crypto.\n * Ed25519 requires Node.js ≥ 18.4, Bun ≥ 1.0, or Cloudflare Workers.\n *\n * @example\n * ```ts\n * import { signDKIM } from \"sently/core/dkim\";\n * const signed = await signDKIM(rawMessage, {\n * domainName: \"example.com\",\n * keySelector: \"2024\",\n * privateKey: \"-----BEGIN PRIVATE KEY-----\\\\n...\",\n * });\n * ```\n */\nimport { encodeBase64, encodeUtf8 } from \"./base64.js\";\nimport type { DKIMConfig } from \"./types.js\";\n\nconst CRLF = \"\\r\\n\";\nconst DEFAULT_HEADER_FIELDS = \"from:to:subject:date:message-id:mime-version:content-type\";\n\n/** Result of DKIM signing — the header line to prepend. */\nexport interface DKIMSignResult {\n /** Complete DKIM-Signature header line (without trailing CRLF). */\n header: string;\n}\n\n/**\n * Canonicalize headers using the relaxed algorithm (RFC 6376 §3.4.2).\n */\nexport function canonicalizeHeadersRelaxed(headers: string, fieldNames: string[]): string {\n const parsed = parseHeaders(headers);\n const lines: string[] = [];\n\n for (const name of fieldNames) {\n const key = name.toLowerCase().trim();\n const values = parsed.get(key);\n if (!values) {\n continue;\n }\n for (const value of values) {\n const unfolded = value.replace(/\\r?\\n/g, \"\").replace(/\\s+/g, \" \").trim();\n lines.push(`${key}:${unfolded}`);\n }\n }\n\n return lines.length > 0 ? `${lines.join(CRLF)}${CRLF}` : \"\";\n}\n\n/**\n * Canonicalize body using the relaxed algorithm (RFC 6376 §3.4.4).\n */\nexport function canonicalizeBodyRelaxed(body: string): string {\n const normalized = body.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const lines = normalized\n .split(\"\\n\")\n .map((line) => line.replace(/[ \\t]+$/g, \"\").replace(/[ \\t]+/g, \" \"))\n .filter((line) => line.length > 0);\n if (lines.length === 0) {\n return CRLF;\n }\n return `${lines.join(CRLF)}${CRLF}`;\n}\n\n/**\n * Import a PEM-encoded private key into a Web Crypto CryptoKey.\n */\nexport async function importPrivateKey(\n pem: string,\n algorithm: \"rsa-sha256\" | \"ed25519-sha256\",\n): Promise<CryptoKey> {\n if (algorithm === \"ed25519-sha256\" && /OPENSSH PRIVATE KEY/i.test(pem)) {\n throw new Error(\n \"Ed25519 keys must be in PKCS#8 PEM format (-----BEGIN PRIVATE KEY-----). Convert with: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key_pkcs8.pem\",\n );\n }\n\n const der = pemToDer(pem);\n\n const derBuffer = toArrayBuffer(der);\n\n try {\n if (algorithm === \"ed25519-sha256\") {\n return await crypto.subtle.importKey(\"pkcs8\", derBuffer, { name: \"Ed25519\" }, false, [\n \"sign\",\n ]);\n }\n return await crypto.subtle.importKey(\n \"pkcs8\",\n derBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n } catch (err) {\n if (algorithm === \"ed25519-sha256\" && err instanceof DOMException) {\n throw new Error(\"Ed25519 DKIM requires Node.js ≥ 18.4, Bun ≥ 1.0, or Cloudflare Workers\", {\n cause: err,\n });\n }\n throw err;\n }\n}\n\n/**\n * Sign a raw MIME message with DKIM.\n */\nexport async function signDKIM(\n rawMessage: Uint8Array,\n config: DKIMConfig,\n): Promise<DKIMSignResult> {\n const algorithm = config.algorithm ?? \"rsa-sha256\";\n const fieldList = (config.headerFieldNames ?? DEFAULT_HEADER_FIELDS)\n .split(\":\")\n .map((f) => f.trim().toLowerCase())\n .filter(Boolean);\n const skip = new Set(\n (config.skipFields ?? \"\")\n .split(\":\")\n .map((f) => f.trim().toLowerCase())\n .filter(Boolean),\n );\n const signFields = fieldList.filter((f) => !skip.has(f));\n\n const text = new TextDecoder().decode(rawMessage);\n const sep = text.indexOf(\"\\r\\n\\r\\n\");\n const headerPart = sep >= 0 ? text.slice(0, sep) : text;\n const bodyPart = sep >= 0 ? text.slice(sep + 4) : \"\";\n\n const bodyCanonical = canonicalizeBodyRelaxed(bodyPart);\n const bodyHash = await sha256Base64(encodeUtf8(bodyCanonical));\n\n const dkimAlgo = algorithm === \"ed25519-sha256\" ? \"ed25519-sha256\" : \"rsa-sha256\";\n const headerList = signFields.join(\":\");\n const timestamp = Math.floor(Date.now() / 1000);\n\n const dkimWithoutSig = [\n `v=1`,\n `a=${dkimAlgo}`,\n `c=relaxed/relaxed`,\n `d=${config.domainName}`,\n `s=${config.keySelector}`,\n `h=${headerList}`,\n `bh=${bodyHash}`,\n `b=`,\n `t=${timestamp}`,\n ].join(\"; \");\n\n const dkimHeaderName = \"dkim-signature\";\n const dkimHeaderValue = dkimWithoutSig;\n const headersWithDkim = `${headerPart}${CRLF}${dkimHeaderName}:${dkimHeaderValue}`;\n const canonical = canonicalizeHeadersRelaxed(headersWithDkim, [...signFields, dkimHeaderName]);\n\n const key = await importPrivateKey(config.privateKey, algorithm);\n const data = encodeUtf8(canonical);\n const signature = await signData(key, data, algorithm);\n const bValue = encodeBase64(signature).replace(/\\r\\n/g, \"\");\n\n const header = `DKIM-Signature: v=1; a=${dkimAlgo}; c=relaxed/relaxed; d=${config.domainName}; s=${config.keySelector}; h=${headerList}; bh=${bodyHash}; b=${bValue}; t=${timestamp}`;\n\n return { header };\n}\n\nfunction parseHeaders(headerBlock: string): Map<string, string[]> {\n const map = new Map<string, string[]>();\n const lines = headerBlock.split(/\\r?\\n/).filter((l) => l.length > 0);\n let currentName = \"\";\n let currentValue = \"\";\n\n const flush = (): void => {\n if (!currentName) {\n return;\n }\n const key = currentName.toLowerCase();\n const list = map.get(key) ?? [];\n list.push(currentValue);\n map.set(key, list);\n currentName = \"\";\n currentValue = \"\";\n };\n\n for (const line of lines) {\n if (/^[ \\t]/.test(line) && currentName) {\n currentValue += ` ${line.trim()}`;\n continue;\n }\n flush();\n const colon = line.indexOf(\":\");\n if (colon > 0) {\n currentName = line.slice(0, colon).trim();\n currentValue = line.slice(colon + 1).trim();\n }\n }\n flush();\n return map;\n}\n\nfunction pemToDer(pem: string): Uint8Array {\n const lines = pem\n .replace(/-----BEGIN[^-]+-----/g, \"\")\n .replace(/-----END[^-]+-----/g, \"\")\n .replace(/\\s/g, \"\");\n const binary = atob(lines);\n const der = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n der[i] = binary.charCodeAt(i);\n }\n return der;\n}\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nasync function sha256Base64(data: Uint8Array): Promise<string> {\n const hash = await crypto.subtle.digest(\"SHA-256\", toArrayBuffer(data));\n return encodeBase64(new Uint8Array(hash)).replace(/\\r\\n/g, \"\");\n}\n\nasync function signData(\n key: CryptoKey,\n data: Uint8Array,\n algorithm: \"rsa-sha256\" | \"ed25519-sha256\",\n): Promise<Uint8Array> {\n const buf = toArrayBuffer(data);\n if (algorithm === \"ed25519-sha256\") {\n const sig = await crypto.subtle.sign(\"Ed25519\", key, buf);\n return new Uint8Array(sig);\n }\n const sig = await crypto.subtle.sign({ name: \"RSASSA-PKCS1-v1_5\" }, key, buf);\n return new Uint8Array(sig);\n}\n",
|
|
6
|
-
"// src/core/mime.ts\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"./address.js\";\nimport { encodeBase64, encodeHeader, encodeUtf8 } from \"./base64.js\";\nimport { signDKIM } from \"./dkim.js\";\nimport type { Address, Attachment, DKIMConfig, Envelope, MailOptions } from \"./types.js\";\n\nfunction sanitizeHeaderValue(value: string): string {\n return value.replace(/\\r\\n?|\\n/g, \" \").trim();\n}\n\nfunction sanitizeAddress(addr: Address): Address {\n if (!addr.name) {\n return addr;\n }\n return { ...addr, name: sanitizeHeaderValue(addr.name) };\n}\n\n/** Result of building a complete MIME message. */\nexport interface MIMEBuildResult {\n raw: Uint8Array;\n envelope: Envelope;\n messageId: string;\n size: number;\n}\n\nconst CRLF = \"\\r\\n\";\n\n/**\n * Build a complete MIME message as a Uint8Array ready for SMTP DATA.\n * When `dkim` is provided, signs the message and prepends the DKIM-Signature header.\n */\nexport async function buildMIME(options: MailOptions, dkim?: DKIMConfig): Promise<MIMEBuildResult> {\n const messageId = options.messageId ?? generateMessageId();\n const date = (options.date ?? new Date()).toUTCString();\n const fromAddrs = parseAddresses(options.from);\n const toAddrs = parseAddresses(options.to);\n const ccAddrs = options.cc ? parseAddresses(options.cc) : [];\n\n if (fromAddrs.length === 0) {\n throw new Error(\"Missing from address\");\n }\n if (toAddrs.length === 0) {\n throw new Error(\"Missing to address\");\n }\n\n const envelope: Envelope = {\n from: fromAddrs[0]?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n };\n\n const attachments = options.attachments ?? [];\n const inlineAttachments = attachments.filter((a) => a.inline || a.contentId);\n const regularAttachments = attachments.filter((a) => !a.inline && !a.contentId);\n\n let root = buildSimpleBody(options);\n\n if (inlineAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/related; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(buildSimpleBody(options)),\n ...inlineAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n if (regularAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/mixed; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(root),\n ...regularAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n const headers: string[] = [\n foldHeader(\"From\", fromAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n foldHeader(\"To\", toAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n ];\n\n if (ccAddrs.length > 0) {\n headers.push(foldHeader(\"Cc\", ccAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")));\n }\n\n if (options.replyTo) {\n headers.push(\n foldHeader(\n \"Reply-To\",\n parseAddresses(options.replyTo)\n .map((a) => toMIMEHeader(sanitizeAddress(a)))\n .join(\", \"),\n ),\n );\n }\n\n headers.push(\n foldHeader(\"Subject\", encodeHeader(sanitizeHeaderValue(options.subject))),\n foldHeader(\"Date\", date),\n foldHeader(\"Message-ID\", messageId),\n \"MIME-Version: 1.0\",\n );\n\n if (options.priority === \"high\") {\n headers.push(\"X-Priority: 1\", \"Importance: high\");\n } else if (options.priority === \"low\") {\n headers.push(\"X-Priority: 5\", \"Importance: low\");\n }\n\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.push(foldHeader(sanitizeHeaderValue(key), sanitizeHeaderValue(value)));\n }\n }\n\n headers.push(`Content-Type: ${root.contentType}`);\n if (root.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${root.contentTransferEncoding}`);\n }\n\n const rawText = `${headers.join(CRLF)}${CRLF}${CRLF}${root.content}`;\n let raw = encodeUtf8(rawText);\n\n if (dkim) {\n const { header } = await signDKIM(raw, dkim);\n const signedText = `${header}${CRLF}${rawText}`;\n raw = encodeUtf8(signedText);\n }\n\n return { raw, envelope, messageId, size: raw.length };\n}\n\ninterface SimpleBody {\n contentType: string;\n contentTransferEncoding?: string;\n content: string;\n}\n\nfunction buildSimpleBody(options: MailOptions): SimpleBody {\n const hasText = Boolean(options.text);\n const hasHtml = Boolean(options.html);\n\n if (hasText && hasHtml) {\n const boundary = generateBoundary();\n return {\n contentType: `multipart/alternative; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatSimplePart({\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n }),\n formatSimplePart({\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n }),\n ]),\n };\n }\n\n if (hasHtml) {\n return {\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n };\n }\n\n return {\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n };\n}\n\nfunction formatSimplePart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatNestedPart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatAttachmentPart(attachment: Attachment): string {\n if (!attachment.content || typeof attachment.content === \"string\") {\n throw new Error(`Attachment \"${attachment.filename}\" requires Uint8Array content`);\n }\n\n const headers = [\n `Content-Type: ${attachment.contentType ?? \"application/octet-stream\"}`,\n \"Content-Transfer-Encoding: base64\",\n `Content-Disposition: ${attachment.inline ? \"inline\" : \"attachment\"}; filename=\"${attachment.filename}\"`,\n ];\n\n if (attachment.contentId) {\n headers.push(`Content-ID: <${attachment.contentId}>`);\n }\n\n if (attachment.headers) {\n for (const [key, value] of Object.entries(attachment.headers)) {\n headers.push(`${key}: ${value}`);\n }\n }\n\n return `${headers.join(CRLF)}${CRLF}${CRLF}${encodeBase64(attachment.content)}`;\n}\n\nfunction assembleMultipart(boundary: string, parts: string[]): string {\n const segments = parts.map((part) => `--${boundary}${CRLF}${part}`);\n segments.push(`--${boundary}--`);\n return segments.join(CRLF);\n}\n\nfunction generateMessageId(): string {\n const random = crypto.getRandomValues(new Uint8Array(8));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `<${Date.now()}.${hex}@sently>`;\n}\n\nfunction generateBoundary(): string {\n const random = crypto.getRandomValues(new Uint8Array(12));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `----sently_${hex}`;\n}\n\nfunction foldHeader(name: string, value: string): string {\n const line = `${name}: ${value}`;\n if (line.length <= 76) {\n return line;\n }\n\n const chunks: string[] = [];\n let remaining = line;\n\n while (remaining.length > 76) {\n let breakAt = remaining.lastIndexOf(\" \", 76);\n if (breakAt <= name.length + 1) {\n breakAt = 76;\n }\n chunks.push(remaining.slice(0, breakAt));\n remaining = ` ${remaining.slice(breakAt).trimStart()}`;\n }\n chunks.push(remaining);\n\n return chunks.join(`${CRLF} `);\n}\n"
|
|
7
|
-
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;AAmBA,IAAM,OAAO;AAAA;AACb,IAAM,wBAAwB;AAWvB,SAAS,0BAA0B,CAAC,SAAiB,YAA8B;AAAA,EACxF,MAAM,SAAS,aAAa,OAAO;AAAA,EACnC,MAAM,QAAkB,CAAC;AAAA,EAEzB,WAAW,QAAQ,YAAY;AAAA,IAC7B,MAAM,MAAM,KAAK,YAAY,EAAE,KAAK;AAAA,IACpC,MAAM,SAAS,OAAO,IAAI,GAAG;AAAA,IAC7B,IAAI,CAAC,QAAQ;AAAA,MACX;AAAA,IACF;AAAA,IACA,WAAW,SAAS,QAAQ;AAAA,MAC1B,MAAM,WAAW,MAAM,QAAQ,UAAU,EAAE,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAAA,MACvE,MAAM,KAAK,GAAG,OAAO,UAAU;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,OAAO,MAAM,SAAS,IAAI,GAAG,MAAM,KAAK,IAAI,IAAI,SAAS;AAAA;AAMpD,SAAS,uBAAuB,CAAC,MAAsB;AAAA,EAC5D,MAAM,aAAa,KAAK,QAAQ,SAAS;AAAA,CAAI,EAAE,QAAQ,OAAO;AAAA,CAAI;AAAA,EAClE,MAAM,QAAQ,WACX,MAAM;AAAA,CAAI,EACV,IAAI,CAAC,SAAS,KAAK,QAAQ,YAAY,EAAE,EAAE,QAAQ,WAAW,GAAG,CAAC,EAClE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AAAA,EACnC,IAAI,MAAM,WAAW,GAAG;AAAA,IACtB,OAAO;AAAA,EACT;AAAA,EACA,OAAO,GAAG,MAAM,KAAK,IAAI,IAAI;AAAA;AAM/B,eAAsB,gBAAgB,CACpC,KACA,WACoB;AAAA,EACpB,IAAI,cAAc,oBAAoB,uBAAuB,KAAK,GAAG,GAAG;AAAA,IACtE,MAAM,IAAI,MACR,qJACF;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,SAAS,GAAG;AAAA,EAExB,MAAM,YAAY,cAAc,GAAG;AAAA,EAEnC,IAAI;AAAA,IACF,IAAI,cAAc,kBAAkB;AAAA,MAClC,OAAO,MAAM,OAAO,OAAO,UAAU,SAAS,WAAW,EAAE,MAAM,UAAU,GAAG,OAAO;AAAA,QACnF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,OAAO,MAAM,OAAO,OAAO,UACzB,SACA,WACA,EAAE,MAAM,qBAAqB,MAAM,UAAU,GAC7C,OACA,CAAC,MAAM,CACT;AAAA,IACA,OAAO,KAAK;AAAA,IACZ,IAAI,cAAc,oBAAoB,eAAe,cAAc;AAAA,MACjE,MAAM,IAAI,MAAM,0EAAyE;AAAA,QACvF,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,MAAM;AAAA;AAAA;AAOV,eAAsB,QAAQ,CAC5B,YACA,QACyB;AAAA,EACzB,MAAM,YAAY,OAAO,aAAa;AAAA,EACtC,MAAM,aAAa,OAAO,oBAAoB,uBAC3C,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO;AAAA,EACjB,MAAM,OAAO,IAAI,KACd,OAAO,cAAc,IACnB,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO,CACnB;AAAA,EACA,MAAM,aAAa,UAAU,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;AAAA,EAEvD,MAAM,OAAO,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,EAChD,MAAM,MAAM,KAAK,QAAQ;AAAA;AAAA,CAAU;AAAA,EACnC,MAAM,aAAa,OAAO,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI;AAAA,EACnD,MAAM,WAAW,OAAO,IAAI,KAAK,MAAM,MAAM,CAAC,IAAI;AAAA,EAElD,MAAM,gBAAgB,wBAAwB,QAAQ;AAAA,EACtD,MAAM,WAAW,MAAM,aAAa,WAAW,aAAa,CAAC;AAAA,EAE7D,MAAM,WAAW,cAAc,mBAAmB,mBAAmB;AAAA,EACrE,MAAM,aAAa,WAAW,KAAK,GAAG;AAAA,EACtC,MAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,IAAI;AAAA,EAE9C,MAAM,iBAAiB;AAAA,IACrB;AAAA,IACA,KAAK;AAAA,IACL;AAAA,IACA,KAAK,OAAO;AAAA,IACZ,KAAK,OAAO;AAAA,IACZ,KAAK;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,KAAK;AAAA,EACP,EAAE,KAAK,IAAI;AAAA,EAEX,MAAM,iBAAiB;AAAA,EACvB,MAAM,kBAAkB;AAAA,EACxB,MAAM,kBAAkB,GAAG,aAAa,OAAO,kBAAkB;AAAA,EACjE,MAAM,YAAY,2BAA2B,iBAAiB,CAAC,GAAG,YAAY,cAAc,CAAC;AAAA,EAE7F,MAAM,MAAM,MAAM,iBAAiB,OAAO,YAAY,SAAS;AAAA,EAC/D,MAAM,OAAO,WAAW,SAAS;AAAA,EACjC,MAAM,YAAY,MAAM,SAAS,KAAK,MAAM,SAAS;AAAA,EACrD,MAAM,SAAS,aAAa,SAAS,EAAE,QAAQ,SAAS,EAAE;AAAA,EAE1D,MAAM,SAAS,0BAA0B,kCAAkC,OAAO,iBAAiB,OAAO,kBAAkB,kBAAkB,eAAe,aAAa;AAAA,EAE1K,OAAO,EAAE,OAAO;AAAA;AAGlB,SAAS,YAAY,CAAC,aAA4C;AAAA,EAChE,MAAM,MAAM,IAAI;AAAA,EAChB,MAAM,QAAQ,YAAY,MAAM,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAAA,EACnE,IAAI,cAAc;AAAA,EAClB,IAAI,eAAe;AAAA,EAEnB,MAAM,QAAQ,MAAY;AAAA,IACxB,IAAI,CAAC,aAAa;AAAA,MAChB;AAAA,IACF;AAAA,IACA,MAAM,MAAM,YAAY,YAAY;AAAA,IACpC,MAAM,OAAO,IAAI,IAAI,GAAG,KAAK,CAAC;AAAA,IAC9B,KAAK,KAAK,YAAY;AAAA,IACtB,IAAI,IAAI,KAAK,IAAI;AAAA,IACjB,cAAc;AAAA,IACd,eAAe;AAAA;AAAA,EAGjB,WAAW,QAAQ,OAAO;AAAA,IACxB,IAAI,SAAS,KAAK,IAAI,KAAK,aAAa;AAAA,MACtC,gBAAgB,IAAI,KAAK,KAAK;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,MAAM;AAAA,IACN,MAAM,QAAQ,KAAK,QAAQ,GAAG;AAAA,IAC9B,IAAI,QAAQ,GAAG;AAAA,MACb,cAAc,KAAK,MAAM,GAAG,KAAK,EAAE,KAAK;AAAA,MACxC,eAAe,KAAK,MAAM,QAAQ,CAAC,EAAE,KAAK;AAAA,IAC5C;AAAA,EACF;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA;AAGT,SAAS,QAAQ,CAAC,KAAyB;AAAA,EACzC,MAAM,QAAQ,IACX,QAAQ,yBAAyB,EAAE,EACnC,QAAQ,uBAAuB,EAAE,EACjC,QAAQ,OAAO,EAAE;AAAA,EACpB,MAAM,SAAS,KAAK,KAAK;AAAA,EACzB,MAAM,MAAM,IAAI,WAAW,OAAO,MAAM;AAAA,EACxC,SAAS,IAAI,EAAG,IAAI,OAAO,QAAQ,KAAK;AAAA,IACtC,IAAI,KAAK,OAAO,WAAW,CAAC;AAAA,EAC9B;AAAA,EACA,OAAO;AAAA;AAGT,SAAS,aAAa,CAAC,OAAgC;AAAA,EACrD,OAAO,MAAM,OAAO,MAAM,MAAM,YAAY,MAAM,aAAa,MAAM,UAAU;AAAA;AAGjF,eAAe,YAAY,CAAC,MAAmC;AAAA,EAC7D,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,cAAc,IAAI,CAAC;AAAA,EACtE,OAAO,aAAa,IAAI,WAAW,IAAI,CAAC,EAAE,QAAQ,SAAS,EAAE;AAAA;AAG/D,eAAe,QAAQ,CACrB,KACA,MACA,WACqB;AAAA,EACrB,MAAM,MAAM,cAAc,IAAI;AAAA,EAC9B,IAAI,cAAc,kBAAkB;AAAA,IAClC,MAAM,OAAM,MAAM,OAAO,OAAO,KAAK,WAAW,KAAK,GAAG;AAAA,IACxD,OAAO,IAAI,WAAW,IAAG;AAAA,EAC3B;AAAA,EACA,MAAM,MAAM,MAAM,OAAO,OAAO,KAAK,EAAE,MAAM,oBAAoB,GAAG,KAAK,GAAG;AAAA,EAC5E,OAAO,IAAI,WAAW,GAAG;AAAA;;;ACjO3B,SAAS,mBAAmB,CAAC,OAAuB;AAAA,EAClD,OAAO,MAAM,QAAQ,aAAa,GAAG,EAAE,KAAK;AAAA;AAG9C,SAAS,eAAe,CAAC,MAAwB;AAAA,EAC/C,IAAI,CAAC,KAAK,MAAM;AAAA,IACd,OAAO;AAAA,EACT;AAAA,EACA,OAAO,KAAK,MAAM,MAAM,oBAAoB,KAAK,IAAI,EAAE;AAAA;AAWzD,IAAM,QAAO;AAAA;AAMb,eAAsB,SAAS,CAAC,SAAsB,MAA6C;AAAA,EACjG,MAAM,YAAY,QAAQ,aAAa,kBAAkB;AAAA,EACzD,MAAM,QAAQ,QAAQ,QAAQ,IAAI,MAAQ,YAAY;AAAA,EACtD,MAAM,YAAY,eAAe,QAAQ,IAAI;AAAA,EAC7C,MAAM,UAAU,eAAe,QAAQ,EAAE;AAAA,EACzC,MAAM,UAAU,QAAQ,KAAK,eAAe,QAAQ,EAAE,IAAI,CAAC;AAAA,EAE3D,IAAI,UAAU,WAAW,GAAG;AAAA,IAC1B,MAAM,IAAI,MAAM,sBAAsB;AAAA,EACxC;AAAA,EACA,IAAI,QAAQ,WAAW,GAAG;AAAA,IACxB,MAAM,IAAI,MAAM,oBAAoB;AAAA,EACtC;AAAA,EAEA,MAAM,WAAqB;AAAA,IACzB,MAAM,UAAU,IAAI,WAAW;AAAA,IAC/B,IAAI;AAAA,MACF,GAAG,cAAc,QAAQ,EAAE;AAAA,MAC3B,GAAI,QAAQ,KAAK,cAAc,QAAQ,EAAE,IAAI,CAAC;AAAA,MAC9C,GAAI,QAAQ,MAAM,cAAc,QAAQ,GAAG,IAAI,CAAC;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,QAAQ,eAAe,CAAC;AAAA,EAC5C,MAAM,oBAAoB,YAAY,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS;AAAA,EAC3E,MAAM,qBAAqB,YAAY,OAAO,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,SAAS;AAAA,EAE9E,IAAI,OAAO,gBAAgB,OAAO;AAAA,EAElC,IAAI,kBAAkB,SAAS,GAAG;AAAA,IAChC,MAAM,WAAW,iBAAiB;AAAA,IAClC,OAAO;AAAA,MACL,aAAa,gCAAgC;AAAA,MAC7C,SAAS,kBAAkB,UAAU;AAAA,QACnC,iBAAiB,gBAAgB,OAAO,CAAC;AAAA,QACzC,GAAG,kBAAkB,IAAI,oBAAoB;AAAA,MAC/C,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,IAAI,mBAAmB,SAAS,GAAG;AAAA,IACjC,MAAM,WAAW,iBAAiB;AAAA,IAClC,OAAO;AAAA,MACL,aAAa,8BAA8B;AAAA,MAC3C,SAAS,kBAAkB,UAAU;AAAA,QACnC,iBAAiB,IAAI;AAAA,QACrB,GAAG,mBAAmB,IAAI,oBAAoB;AAAA,MAChD,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,UAAoB;AAAA,IACxB,WAAW,QAAQ,UAAU,IAAI,CAAC,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IACpF,WAAW,MAAM,QAAQ,IAAI,CAAC,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,EAClF;AAAA,EAEA,IAAI,QAAQ,SAAS,GAAG;AAAA,IACtB,QAAQ,KAAK,WAAW,MAAM,QAAQ,IAAI,CAAC,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;AAAA,EAChG;AAAA,EAEA,IAAI,QAAQ,SAAS;AAAA,IACnB,QAAQ,KACN,WACE,YACA,eAAe,QAAQ,OAAO,EAC3B,IAAI,CAAC,MAAM,aAAa,gBAAgB,CAAC,CAAC,CAAC,EAC3C,KAAK,IAAI,CACd,CACF;AAAA,EACF;AAAA,EAEA,QAAQ,KACN,WAAW,WAAW,aAAa,oBAAoB,QAAQ,OAAO,CAAC,CAAC,GACxE,WAAW,QAAQ,IAAI,GACvB,WAAW,cAAc,SAAS,GAClC,mBACF;AAAA,EAEA,IAAI,QAAQ,aAAa,QAAQ;AAAA,IAC/B,QAAQ,KAAK,iBAAiB,kBAAkB;AAAA,EAClD,EAAO,SAAI,QAAQ,aAAa,OAAO;AAAA,IACrC,QAAQ,KAAK,iBAAiB,iBAAiB;AAAA,EACjD;AAAA,EAEA,IAAI,QAAQ,SAAS;AAAA,IACnB,YAAY,KAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAAA,MAC1D,QAAQ,KAAK,WAAW,oBAAoB,GAAG,GAAG,oBAAoB,KAAK,CAAC,CAAC;AAAA,IAC/E;AAAA,EACF;AAAA,EAEA,QAAQ,KAAK,iBAAiB,KAAK,aAAa;AAAA,EAChD,IAAI,KAAK,yBAAyB;AAAA,IAChC,QAAQ,KAAK,8BAA8B,KAAK,yBAAyB;AAAA,EAC3E;AAAA,EAEA,MAAM,UAAU,GAAG,QAAQ,KAAK,KAAI,IAAI,QAAO,QAAO,KAAK;AAAA,EAC3D,IAAI,MAAM,WAAW,OAAO;AAAA,EAE5B,IAAI,MAAM;AAAA,IACR,QAAQ,WAAW,MAAM,SAAS,KAAK,IAAI;AAAA,IAC3C,MAAM,aAAa,GAAG,SAAS,QAAO;AAAA,IACtC,MAAM,WAAW,UAAU;AAAA,EAC7B;AAAA,EAEA,OAAO,EAAE,KAAK,UAAU,WAAW,MAAM,IAAI,OAAO;AAAA;AAStD,SAAS,eAAe,CAAC,SAAkC;AAAA,EACzD,MAAM,UAAU,QAAQ,QAAQ,IAAI;AAAA,EACpC,MAAM,UAAU,QAAQ,QAAQ,IAAI;AAAA,EAEpC,IAAI,WAAW,SAAS;AAAA,IACtB,MAAM,WAAW,iBAAiB;AAAA,IAClC,OAAO;AAAA,MACL,aAAa,oCAAoC;AAAA,MACjD,SAAS,kBAAkB,UAAU;AAAA,QACnC,iBAAiB;AAAA,UACf,aAAa;AAAA,UACb,yBAAyB;AAAA,UACzB,SAAS,QAAQ,QAAQ;AAAA,QAC3B,CAAC;AAAA,QACD,iBAAiB;AAAA,UACf,aAAa;AAAA,UACb,yBAAyB;AAAA,UACzB,SAAS,QAAQ,QAAQ;AAAA,QAC3B,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,IAAI,SAAS;AAAA,IACX,OAAO;AAAA,MACL,aAAa;AAAA,MACb,yBAAyB;AAAA,MACzB,SAAS,QAAQ,QAAQ;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,yBAAyB;AAAA,IACzB,SAAS,QAAQ,QAAQ;AAAA,EAC3B;AAAA;AAGF,SAAS,gBAAgB,CAAC,MAA0B;AAAA,EAClD,MAAM,UAAU,CAAC,iBAAiB,KAAK,aAAa;AAAA,EACpD,IAAI,KAAK,yBAAyB;AAAA,IAChC,QAAQ,KAAK,8BAA8B,KAAK,yBAAyB;AAAA,EAC3E;AAAA,EACA,OAAO,GAAG,QAAQ,KAAK,KAAI,IAAI,QAAO,QAAO,KAAK;AAAA;AAGpD,SAAS,gBAAgB,CAAC,MAA0B;AAAA,EAClD,MAAM,UAAU,CAAC,iBAAiB,KAAK,aAAa;AAAA,EACpD,IAAI,KAAK,yBAAyB;AAAA,IAChC,QAAQ,KAAK,8BAA8B,KAAK,yBAAyB;AAAA,EAC3E;AAAA,EACA,OAAO,GAAG,QAAQ,KAAK,KAAI,IAAI,QAAO,QAAO,KAAK;AAAA;AAGpD,SAAS,oBAAoB,CAAC,YAAgC;AAAA,EAC5D,IAAI,CAAC,WAAW,WAAW,OAAO,WAAW,YAAY,UAAU;AAAA,IACjE,MAAM,IAAI,MAAM,eAAe,WAAW,uCAAuC;AAAA,EACnF;AAAA,EAEA,MAAM,UAAU;AAAA,IACd,iBAAiB,WAAW,eAAe;AAAA,IAC3C;AAAA,IACA,wBAAwB,WAAW,SAAS,WAAW,2BAA2B,WAAW;AAAA,EAC/F;AAAA,EAEA,IAAI,WAAW,WAAW;AAAA,IACxB,QAAQ,KAAK,gBAAgB,WAAW,YAAY;AAAA,EACtD;AAAA,EAEA,IAAI,WAAW,SAAS;AAAA,IACtB,YAAY,KAAK,UAAU,OAAO,QAAQ,WAAW,OAAO,GAAG;AAAA,MAC7D,QAAQ,KAAK,GAAG,QAAQ,OAAO;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,OAAO,GAAG,QAAQ,KAAK,KAAI,IAAI,QAAO,QAAO,aAAa,WAAW,OAAO;AAAA;AAG9E,SAAS,iBAAiB,CAAC,UAAkB,OAAyB;AAAA,EACpE,MAAM,WAAW,MAAM,IAAI,CAAC,SAAS,KAAK,WAAW,QAAO,MAAM;AAAA,EAClE,SAAS,KAAK,KAAK,YAAY;AAAA,EAC/B,OAAO,SAAS,KAAK,KAAI;AAAA;AAG3B,SAAS,iBAAiB,GAAW;AAAA,EACnC,MAAM,SAAS,OAAO,gBAAgB,IAAI,WAAW,CAAC,CAAC;AAAA,EACvD,MAAM,MAAM,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAAA,EAC9E,OAAO,IAAI,KAAK,IAAI,KAAK;AAAA;AAG3B,SAAS,gBAAgB,GAAW;AAAA,EAClC,MAAM,SAAS,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAAA,EACxD,MAAM,MAAM,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAAA,EAC9E,OAAO,cAAc;AAAA;AAGvB,SAAS,UAAU,CAAC,MAAc,OAAuB;AAAA,EACvD,MAAM,OAAO,GAAG,SAAS;AAAA,EACzB,IAAI,KAAK,UAAU,IAAI;AAAA,IACrB,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAmB,CAAC;AAAA,EAC1B,IAAI,YAAY;AAAA,EAEhB,OAAO,UAAU,SAAS,IAAI;AAAA,IAC5B,IAAI,UAAU,UAAU,YAAY,KAAK,EAAE;AAAA,IAC3C,IAAI,WAAW,KAAK,SAAS,GAAG;AAAA,MAC9B,UAAU;AAAA,IACZ;AAAA,IACA,OAAO,KAAK,UAAU,MAAM,GAAG,OAAO,CAAC;AAAA,IACvC,YAAY,IAAI,UAAU,MAAM,OAAO,EAAE,UAAU;AAAA,EACrD;AAAA,EACA,OAAO,KAAK,SAAS;AAAA,EAErB,OAAO,OAAO,KAAK,GAAG,QAAO;AAAA;",
|
|
9
|
-
"debugId": "5D2DCB9A2B18B9D764756E2164756E21",
|
|
10
|
-
"names": []
|
|
11
|
-
}
|
|
File without changes
|