sently 0.4.1 → 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 +55 -0
- package/README.md +193 -94
- package/dist/{chunk-z3eq2t1d.js → chunk-7fqv71z1.js} +15 -8
- 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-mp5c9bfd.js +270 -0
- package/dist/chunk-mp5c9bfd.js.map +11 -0
- package/dist/{chunk-tjsgb3qb.js → chunk-tymfm441.js} +10 -4
- package/dist/{chunk-tjsgb3qb.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 +32 -0
- package/dist/{index.js.map → core/smtp.js.map} +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/detect.js +181 -0
- package/dist/detect.js.map +11 -0
- package/dist/index.js +14 -29
- package/dist/pool/pool.js +8 -268
- package/dist/pool/pool.js.map +3 -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 +6 -6
- package/dist/transports/smtp.js.map +1 -1
- package/package.json +16 -3
- package/dist/chunk-bvxkmq94.js.map +0 -11
- package/dist/chunk-j6qw8ms6.js.map +0 -11
- package/dist/chunk-z3eq2t1d.js.map +0 -10
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSMTPSession,
|
|
3
|
+
deliverSMTPMessage,
|
|
4
|
+
openSMTPSession,
|
|
5
|
+
resolveSMTPConfig
|
|
6
|
+
} from "./chunk-7fqv71z1.js";
|
|
7
|
+
import {
|
|
8
|
+
buildMIME
|
|
9
|
+
} from "./chunk-x3szga4k.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveAttachments
|
|
12
|
+
} from "./chunk-f4c9ttmr.js";
|
|
13
|
+
|
|
14
|
+
// src/pool/connection.ts
|
|
15
|
+
async function createPooledConnection(options) {
|
|
16
|
+
const config = resolveSMTPConfig(options.config);
|
|
17
|
+
const adapter = await options.createAdapter();
|
|
18
|
+
await adapter.connect(options.connectHost, config.port);
|
|
19
|
+
await openSMTPSession(adapter, config);
|
|
20
|
+
let messageCount = 0;
|
|
21
|
+
let idle = true;
|
|
22
|
+
let sendChain = Promise.resolve();
|
|
23
|
+
const maxMessages = options.maxMessages;
|
|
24
|
+
return {
|
|
25
|
+
get idle() {
|
|
26
|
+
return idle;
|
|
27
|
+
},
|
|
28
|
+
get messageCount() {
|
|
29
|
+
return messageCount;
|
|
30
|
+
},
|
|
31
|
+
get usable() {
|
|
32
|
+
return messageCount < maxMessages;
|
|
33
|
+
},
|
|
34
|
+
async send(mailOptions) {
|
|
35
|
+
const run = async () => {
|
|
36
|
+
idle = false;
|
|
37
|
+
try {
|
|
38
|
+
const resolvedOptions = {
|
|
39
|
+
...mailOptions,
|
|
40
|
+
attachments: await resolveAttachments(mailOptions.attachments)
|
|
41
|
+
};
|
|
42
|
+
const mime = await buildMIME(resolvedOptions, config.dkim);
|
|
43
|
+
const result = await deliverSMTPMessage(adapter, mime);
|
|
44
|
+
messageCount += 1;
|
|
45
|
+
return result;
|
|
46
|
+
} finally {
|
|
47
|
+
idle = true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const resultPromise = sendChain.then(run);
|
|
51
|
+
sendChain = resultPromise.then(() => {
|
|
52
|
+
return;
|
|
53
|
+
}, () => {
|
|
54
|
+
return;
|
|
55
|
+
});
|
|
56
|
+
return resultPromise;
|
|
57
|
+
},
|
|
58
|
+
async close() {
|
|
59
|
+
await sendChain;
|
|
60
|
+
await closeSMTPSession(adapter);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/pool/pool.ts
|
|
66
|
+
class RateLimiter {
|
|
67
|
+
rateDelta;
|
|
68
|
+
rateLimit;
|
|
69
|
+
now;
|
|
70
|
+
tokens;
|
|
71
|
+
lastRefill;
|
|
72
|
+
waiters = [];
|
|
73
|
+
constructor(rateDelta, rateLimit, now = Date.now) {
|
|
74
|
+
this.rateDelta = rateDelta;
|
|
75
|
+
this.rateLimit = rateLimit;
|
|
76
|
+
this.now = now;
|
|
77
|
+
this.tokens = rateDelta;
|
|
78
|
+
this.lastRefill = now();
|
|
79
|
+
}
|
|
80
|
+
async acquire() {
|
|
81
|
+
for (;; ) {
|
|
82
|
+
this.refill();
|
|
83
|
+
if (this.tokens > 0) {
|
|
84
|
+
this.tokens -= 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
this.waiters.push(resolve);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
notify() {
|
|
93
|
+
this.refill();
|
|
94
|
+
}
|
|
95
|
+
refill() {
|
|
96
|
+
const t = this.now();
|
|
97
|
+
const elapsed = t - this.lastRefill;
|
|
98
|
+
if (elapsed >= this.rateLimit) {
|
|
99
|
+
const periods = Math.floor(elapsed / this.rateLimit);
|
|
100
|
+
this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);
|
|
101
|
+
this.lastRefill += periods * this.rateLimit;
|
|
102
|
+
while (this.tokens > 0 && this.waiters.length > 0) {
|
|
103
|
+
this.tokens -= 1;
|
|
104
|
+
const next = this.waiters.shift();
|
|
105
|
+
next?.();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class SMTPPool {
|
|
112
|
+
config;
|
|
113
|
+
maxConnections;
|
|
114
|
+
maxMessages;
|
|
115
|
+
createAdapterFn;
|
|
116
|
+
rateLimiter;
|
|
117
|
+
connections = [];
|
|
118
|
+
queue = [];
|
|
119
|
+
draining = false;
|
|
120
|
+
closed = false;
|
|
121
|
+
processChain = Promise.resolve();
|
|
122
|
+
constructor(config, options) {
|
|
123
|
+
this.config = config;
|
|
124
|
+
this.maxConnections = config.maxConnections ?? 5;
|
|
125
|
+
this.maxMessages = config.maxMessages ?? 100;
|
|
126
|
+
if (options?.createAdapter) {
|
|
127
|
+
const factory = options.createAdapter;
|
|
128
|
+
this.createAdapterFn = async () => factory();
|
|
129
|
+
} else if (config.adapter) {
|
|
130
|
+
this.createAdapterFn = async () => config.adapter;
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error("SMTPPool requires config.adapter or options.createAdapter");
|
|
133
|
+
}
|
|
134
|
+
if (config.rateDelta !== undefined && config.rateDelta > 0) {
|
|
135
|
+
this.rateLimiter = new RateLimiter(config.rateDelta, config.rateLimit ?? 1000, options?.now);
|
|
136
|
+
} else {
|
|
137
|
+
this.rateLimiter = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async send(options) {
|
|
141
|
+
if (this.draining) {
|
|
142
|
+
throw new Error("SMTPPool is closing — no new messages accepted");
|
|
143
|
+
}
|
|
144
|
+
if (this.closed) {
|
|
145
|
+
throw new Error("SMTPPool is closed");
|
|
146
|
+
}
|
|
147
|
+
if (this.rateLimiter) {
|
|
148
|
+
await this.rateLimiter.acquire();
|
|
149
|
+
}
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this.queue.push({ options, resolve, reject });
|
|
152
|
+
this.scheduleProcess();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
scheduleProcess() {
|
|
156
|
+
this.processChain = this.processChain.then(() => this.processQueue()).catch(() => {
|
|
157
|
+
return;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async verify() {
|
|
161
|
+
try {
|
|
162
|
+
const conn = await this.spawnConnection();
|
|
163
|
+
try {
|
|
164
|
+
return { ok: true, provider: "smtp-pool" };
|
|
165
|
+
} finally {
|
|
166
|
+
await conn.close();
|
|
167
|
+
this.removeConnection(conn);
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
provider: "smtp-pool",
|
|
173
|
+
message: err instanceof Error ? err.message : String(err)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async close() {
|
|
178
|
+
this.draining = true;
|
|
179
|
+
await this.drainQueue();
|
|
180
|
+
await Promise.allSettled(this.connections.map((c) => c.close()));
|
|
181
|
+
this.connections.length = 0;
|
|
182
|
+
this.closed = true;
|
|
183
|
+
}
|
|
184
|
+
get connectionCount() {
|
|
185
|
+
return this.connections.length;
|
|
186
|
+
}
|
|
187
|
+
get queueSize() {
|
|
188
|
+
return this.queue.length;
|
|
189
|
+
}
|
|
190
|
+
async processQueue() {
|
|
191
|
+
if (this.draining) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
while (this.queue.length > 0) {
|
|
195
|
+
const idleConn = this.connections.find((c) => c.idle && c.usable);
|
|
196
|
+
if (idleConn) {
|
|
197
|
+
const entry = this.queue.shift();
|
|
198
|
+
if (!entry) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const result = await idleConn.send(entry.options);
|
|
203
|
+
entry.resolve(result);
|
|
204
|
+
if (!idleConn.usable) {
|
|
205
|
+
await idleConn.close();
|
|
206
|
+
this.removeConnection(idleConn);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
entry.reject(err);
|
|
210
|
+
await idleConn.close().catch(() => {
|
|
211
|
+
return;
|
|
212
|
+
});
|
|
213
|
+
this.removeConnection(idleConn);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (this.connections.length < this.maxConnections) {
|
|
218
|
+
const entry = this.queue.shift();
|
|
219
|
+
if (!entry) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
const conn = await this.spawnConnection();
|
|
223
|
+
try {
|
|
224
|
+
const result = await conn.send(entry.options);
|
|
225
|
+
entry.resolve(result);
|
|
226
|
+
if (!conn.usable) {
|
|
227
|
+
await conn.close();
|
|
228
|
+
this.removeConnection(conn);
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
entry.reject(err);
|
|
232
|
+
await conn.close().catch(() => {
|
|
233
|
+
return;
|
|
234
|
+
});
|
|
235
|
+
this.removeConnection(conn);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async spawnConnection() {
|
|
243
|
+
const resolved = resolveSMTPConfig(this.config);
|
|
244
|
+
const conn = await createPooledConnection({
|
|
245
|
+
config: this.config,
|
|
246
|
+
maxMessages: this.maxMessages,
|
|
247
|
+
connectHost: resolved.host,
|
|
248
|
+
createAdapter: this.createAdapterFn
|
|
249
|
+
});
|
|
250
|
+
this.connections.push(conn);
|
|
251
|
+
return conn;
|
|
252
|
+
}
|
|
253
|
+
removeConnection(conn) {
|
|
254
|
+
const index = this.connections.indexOf(conn);
|
|
255
|
+
if (index >= 0) {
|
|
256
|
+
this.connections.splice(index, 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async drainQueue() {
|
|
260
|
+
while (this.queue.length > 0 || this.connections.some((c) => !c.idle)) {
|
|
261
|
+
await this.processQueue();
|
|
262
|
+
if (this.queue.length > 0) {
|
|
263
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export { RateLimiter, SMTPPool };
|
|
269
|
+
|
|
270
|
+
//# debugId=D47B35696A356F3464756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/pool/connection.ts", "../src/pool/pool.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { buildMIME } from \"../core/mime.js\";\nimport type { MailOptions, SendResult, SMTPConfig } from \"../core/types.js\";\nimport { resolveAttachments } from \"../transports/resolve-attachments.js\";\nimport {\n closeSMTPSession,\n deliverSMTPMessage,\n openSMTPSession,\n resolveSMTPConfig,\n} from \"../transports/smtp.js\";\n\n/** A single pooled SMTP connection with a persistent session. */\nexport interface PooledConnection {\n /** Send one message over this connection. */\n send(options: MailOptions): Promise<SendResult>;\n /** Whether this connection is idle and available for work. */\n readonly idle: boolean;\n /** Number of messages sent on this connection. */\n readonly messageCount: number;\n /** Whether this connection can accept more messages before recycle. */\n readonly usable: boolean;\n /** Close the connection and end the SMTP session. */\n close(): Promise<void>;\n}\n\n/** Options for creating a pooled connection. */\nexport interface PooledConnectionOptions {\n config: SMTPConfig;\n maxMessages: number;\n connectHost: string;\n createAdapter: () => Promise<import(\"../core/types.js\").SocketAdapter>;\n}\n\n/**\n * Create a pooled SMTP connection with an open authenticated session.\n */\nexport async function createPooledConnection(\n options: PooledConnectionOptions,\n): Promise<PooledConnection> {\n const config = resolveSMTPConfig(options.config);\n const adapter = await options.createAdapter();\n await adapter.connect(options.connectHost, config.port);\n await openSMTPSession(adapter, config);\n\n let messageCount = 0;\n let idle = true;\n let sendChain: Promise<void> = Promise.resolve();\n\n const maxMessages = options.maxMessages;\n\n return {\n get idle(): boolean {\n return idle;\n },\n get messageCount(): number {\n return messageCount;\n },\n get usable(): boolean {\n return messageCount < maxMessages;\n },\n\n async send(mailOptions: MailOptions): Promise<SendResult> {\n const run = async (): Promise<SendResult> => {\n idle = false;\n try {\n const resolvedOptions = {\n ...mailOptions,\n attachments: await resolveAttachments(mailOptions.attachments),\n };\n const mime = await buildMIME(resolvedOptions, config.dkim);\n const result = await deliverSMTPMessage(adapter, mime);\n messageCount += 1;\n return result;\n } finally {\n idle = true;\n }\n };\n\n const resultPromise = sendChain.then(run);\n sendChain = resultPromise.then(\n () => undefined,\n () => undefined,\n );\n return resultPromise;\n },\n\n async close(): Promise<void> {\n await sendChain;\n await closeSMTPSession(adapter);\n },\n };\n}\n",
|
|
6
|
+
"import type {\n MailOptions,\n PoolConfig,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveSMTPConfig } from \"../transports/smtp.js\";\nimport { createPooledConnection, type PooledConnection } from \"./connection.js\";\n\n/** Options for {@link SMTPPool}. */\nexport interface SMTPPoolOptions {\n /** Factory for a new socket adapter per pooled connection. */\n createAdapter?: () => Promise<SocketAdapter> | SocketAdapter;\n /** Injectable clock for rate limiting (testing). */\n now?: () => number;\n}\n\ninterface QueueEntry {\n options: MailOptions;\n resolve: (result: SendResult) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Token bucket rate limiter with lazy refill on acquire.\n */\nclass RateLimiter {\n private tokens: number;\n private lastRefill: number;\n private waiters: Array<() => void> = [];\n\n constructor(\n private readonly rateDelta: number,\n private readonly rateLimit: number,\n private readonly now: () => number = Date.now,\n ) {\n this.tokens = rateDelta;\n this.lastRefill = now();\n }\n\n /** Wait until a token is available, then consume one. */\n async acquire(): Promise<void> {\n for (;;) {\n this.refill();\n if (this.tokens > 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve) => {\n this.waiters.push(resolve);\n });\n }\n }\n\n /** Wake waiters after the clock advances (for testing). */\n notify(): void {\n this.refill();\n }\n\n private refill(): void {\n const t = this.now();\n const elapsed = t - this.lastRefill;\n if (elapsed >= this.rateLimit) {\n const periods = Math.floor(elapsed / this.rateLimit);\n this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);\n this.lastRefill += periods * this.rateLimit;\n while (this.tokens > 0 && this.waiters.length > 0) {\n this.tokens -= 1;\n const next = this.waiters.shift();\n next?.();\n }\n }\n }\n}\n\n/**\n * SMTP connection pool with optional rate limiting.\n */\nexport class SMTPPool implements Transport {\n private readonly config: SMTPConfig & PoolConfig;\n private readonly maxConnections: number;\n private readonly maxMessages: number;\n private readonly createAdapterFn: () => Promise<SocketAdapter>;\n private readonly rateLimiter: RateLimiter | null;\n private readonly connections: PooledConnection[] = [];\n private readonly queue: QueueEntry[] = [];\n private draining = false;\n private closed = false;\n private processChain: Promise<void> = Promise.resolve();\n\n /** Creates an SMTP connection pool. */\n constructor(config: SMTPConfig & PoolConfig, options?: SMTPPoolOptions) {\n this.config = config;\n this.maxConnections = config.maxConnections ?? 5;\n this.maxMessages = config.maxMessages ?? 100;\n\n if (options?.createAdapter) {\n const factory = options.createAdapter;\n this.createAdapterFn = async () => factory();\n } else if (config.adapter) {\n this.createAdapterFn = async () => config.adapter as SocketAdapter;\n } else {\n throw new Error(\"SMTPPool requires config.adapter or options.createAdapter\");\n }\n\n if (config.rateDelta !== undefined && config.rateDelta > 0) {\n this.rateLimiter = new RateLimiter(config.rateDelta, config.rateLimit ?? 1000, options?.now);\n } else {\n this.rateLimiter = null;\n }\n }\n\n /** Sends a message through the pool. */\n async send(options: MailOptions): Promise<SendResult> {\n if (this.draining) {\n throw new Error(\"SMTPPool is closing — no new messages accepted\");\n }\n if (this.closed) {\n throw new Error(\"SMTPPool is closed\");\n }\n if (this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n return new Promise<SendResult>((resolve, reject) => {\n this.queue.push({ options, resolve, reject });\n this.scheduleProcess();\n });\n }\n\n private scheduleProcess(): void {\n this.processChain = this.processChain.then(() => this.processQueue()).catch(() => undefined);\n }\n\n /** Verifies connectivity using a temporary connection. */\n async verify(): Promise<VerifyResult> {\n try {\n const conn = await this.spawnConnection();\n try {\n return { ok: true, provider: \"smtp-pool\" };\n } finally {\n await conn.close();\n this.removeConnection(conn);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp-pool\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Drains the queue and closes all connections. */\n async close(): Promise<void> {\n this.draining = true;\n await this.drainQueue();\n await Promise.allSettled(this.connections.map((c) => c.close()));\n this.connections.length = 0;\n this.closed = true;\n }\n\n /** Current number of open pooled connections. */\n get connectionCount(): number {\n return this.connections.length;\n }\n\n /** Number of messages waiting in the send queue. */\n get queueSize(): number {\n return this.queue.length;\n }\n\n private async processQueue(): Promise<void> {\n if (this.draining) {\n return;\n }\n\n while (this.queue.length > 0) {\n const idleConn = this.connections.find((c) => c.idle && c.usable);\n if (idleConn) {\n const entry = this.queue.shift();\n if (!entry) {\n break;\n }\n try {\n const result = await idleConn.send(entry.options);\n entry.resolve(result);\n if (!idleConn.usable) {\n await idleConn.close();\n this.removeConnection(idleConn);\n }\n } catch (err) {\n entry.reject(err);\n await idleConn.close().catch(() => undefined);\n this.removeConnection(idleConn);\n }\n continue;\n }\n\n if (this.connections.length < this.maxConnections) {\n const entry = this.queue.shift();\n if (!entry) {\n break;\n }\n const conn = await this.spawnConnection();\n try {\n const result = await conn.send(entry.options);\n entry.resolve(result);\n if (!conn.usable) {\n await conn.close();\n this.removeConnection(conn);\n }\n } catch (err) {\n entry.reject(err);\n await conn.close().catch(() => undefined);\n this.removeConnection(conn);\n }\n continue;\n }\n\n break;\n }\n }\n\n private async spawnConnection(): Promise<PooledConnection> {\n const resolved = resolveSMTPConfig(this.config);\n const conn = await createPooledConnection({\n config: this.config,\n maxMessages: this.maxMessages,\n connectHost: resolved.host,\n createAdapter: this.createAdapterFn,\n });\n this.connections.push(conn);\n return conn;\n }\n\n private removeConnection(conn: PooledConnection): void {\n const index = this.connections.indexOf(conn);\n if (index >= 0) {\n this.connections.splice(index, 1);\n }\n }\n\n private async drainQueue(): Promise<void> {\n while (this.queue.length > 0 || this.connections.some((c) => !c.idle)) {\n await this.processQueue();\n if (this.queue.length > 0) {\n await new Promise((r) => setTimeout(r, 10));\n }\n }\n }\n}\n\n/** @internal Exposed for deterministic rate limiter tests. */\nexport { RateLimiter };\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "D47B35696A356F3464756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -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;
|
|
@@ -389,11 +391,15 @@ function encodeAuthLoginUser(user) {
|
|
|
389
391
|
return encodeUtf8(`${encodeBase64(user).replace(/\r\n/g, "")}\r
|
|
390
392
|
`);
|
|
391
393
|
}
|
|
394
|
+
function encodeAuthCramResponse(response) {
|
|
395
|
+
return encodeUtf8(`${response}\r
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
392
398
|
function encodeLine(line) {
|
|
393
399
|
return encodeUtf8(`${line}\r
|
|
394
400
|
`);
|
|
395
401
|
}
|
|
396
402
|
|
|
397
|
-
export { computeCRAMMD5, SMTPError, encodeCommand, parseResponse, accumulateResponse, selectAuthMethod, parseEHLO, assertResponse, encodeAuthLoginPass, encodeAuthLoginUser, encodeLine };
|
|
403
|
+
export { computeCRAMMD5, SMTPError, encodeCommand, parseResponse, accumulateResponse, selectAuthMethod, parseEHLO, assertResponse, encodeAuthLoginPass, encodeAuthLoginUser, encodeAuthCramResponse, encodeLine };
|
|
398
404
|
|
|
399
|
-
//# 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;
|