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,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SMTPError,
|
|
3
|
+
accumulateResponse,
|
|
4
|
+
assertResponse,
|
|
5
|
+
computeCRAMMD5,
|
|
6
|
+
encodeAuthCramResponse,
|
|
7
|
+
encodeAuthLoginPass,
|
|
8
|
+
encodeAuthLoginUser,
|
|
9
|
+
encodeCommand,
|
|
10
|
+
encodeLine,
|
|
11
|
+
parseEHLO,
|
|
12
|
+
parseResponse,
|
|
13
|
+
selectAuthMethod
|
|
14
|
+
} from "../chunk-tymfm441.js";
|
|
15
|
+
import"../chunk-794hc3m4.js";
|
|
16
|
+
import"../chunk-v0bahtg2.js";
|
|
17
|
+
export {
|
|
18
|
+
selectAuthMethod,
|
|
19
|
+
parseResponse,
|
|
20
|
+
parseEHLO,
|
|
21
|
+
encodeLine,
|
|
22
|
+
encodeCommand,
|
|
23
|
+
encodeAuthLoginUser,
|
|
24
|
+
encodeAuthLoginPass,
|
|
25
|
+
encodeAuthCramResponse,
|
|
26
|
+
computeCRAMMD5,
|
|
27
|
+
assertResponse,
|
|
28
|
+
accumulateResponse,
|
|
29
|
+
SMTPError
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//# debugId=A31E44B7CFB74A8164756E2164756E21
|
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
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SMTPPool
|
|
3
|
+
} from "./chunk-mp5c9bfd.js";
|
|
4
|
+
import {
|
|
5
|
+
SMTPTransport
|
|
6
|
+
} from "./chunk-7fqv71z1.js";
|
|
7
|
+
import"./chunk-ym3zzv8b.js";
|
|
8
|
+
import"./chunk-x3szga4k.js";
|
|
9
|
+
import"./chunk-tymfm441.js";
|
|
10
|
+
import"./chunk-f4c9ttmr.js";
|
|
11
|
+
import"./chunk-794hc3m4.js";
|
|
12
|
+
import {
|
|
13
|
+
__require
|
|
14
|
+
} from "./chunk-v0bahtg2.js";
|
|
15
|
+
|
|
16
|
+
// src/core/plugin.ts
|
|
17
|
+
async function runPlugins(options, plugins) {
|
|
18
|
+
if (!plugins || plugins.length === 0) {
|
|
19
|
+
return options;
|
|
20
|
+
}
|
|
21
|
+
let current = options;
|
|
22
|
+
for (const plugin of plugins) {
|
|
23
|
+
current = await plugin(current);
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/detect.ts
|
|
29
|
+
function detectRuntime() {
|
|
30
|
+
if (typeof Bun !== "undefined") {
|
|
31
|
+
return "bun";
|
|
32
|
+
}
|
|
33
|
+
if (typeof Deno !== "undefined") {
|
|
34
|
+
return "deno";
|
|
35
|
+
}
|
|
36
|
+
if (typeof caches !== "undefined" && globalThis.navigator?.userAgent === "Cloudflare-Workers") {
|
|
37
|
+
return "cf-workers";
|
|
38
|
+
}
|
|
39
|
+
if (typeof window !== "undefined") {
|
|
40
|
+
return "browser";
|
|
41
|
+
}
|
|
42
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
43
|
+
return "node";
|
|
44
|
+
}
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
async function createDefaultAdapter(options) {
|
|
48
|
+
const runtime = detectRuntime();
|
|
49
|
+
switch (runtime) {
|
|
50
|
+
case "node": {
|
|
51
|
+
const { NodeAdapter } = await import("./adapters/node.js");
|
|
52
|
+
return new NodeAdapter(options);
|
|
53
|
+
}
|
|
54
|
+
case "bun": {
|
|
55
|
+
const { BunAdapter } = await import("./adapters/bun.js");
|
|
56
|
+
return new BunAdapter(options);
|
|
57
|
+
}
|
|
58
|
+
case "deno": {
|
|
59
|
+
const { DenoAdapter } = await import("./adapters/deno.js");
|
|
60
|
+
return new DenoAdapter(options);
|
|
61
|
+
}
|
|
62
|
+
case "cf-workers": {
|
|
63
|
+
const { CloudflareAdapter } = await import("./adapters/cf.js");
|
|
64
|
+
return new CloudflareAdapter(options);
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`No socket adapter available for runtime: ${runtime}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function createMailer(options) {
|
|
71
|
+
if ("transport" in options) {
|
|
72
|
+
return new MailerImpl(options.transport, options.plugins ?? []);
|
|
73
|
+
}
|
|
74
|
+
const smtpConfig = options;
|
|
75
|
+
if (smtpConfig.pool) {
|
|
76
|
+
return new MailerImpl(new SMTPPool(smtpConfig, {
|
|
77
|
+
createAdapter: async () => smtpConfig.adapter ?? await createDefaultAdapter({
|
|
78
|
+
...smtpConfig.secure !== undefined ? { secure: smtpConfig.secure } : {},
|
|
79
|
+
...smtpConfig.connectionTimeout !== undefined ? { connectionTimeout: smtpConfig.connectionTimeout } : {},
|
|
80
|
+
...smtpConfig.tls !== undefined ? { tls: smtpConfig.tls } : {}
|
|
81
|
+
})
|
|
82
|
+
}), smtpConfig.plugins);
|
|
83
|
+
}
|
|
84
|
+
const adapter = smtpConfig.adapter ?? await createDefaultAdapter({
|
|
85
|
+
...smtpConfig.secure !== undefined ? { secure: smtpConfig.secure } : {},
|
|
86
|
+
...smtpConfig.connectionTimeout !== undefined ? { connectionTimeout: smtpConfig.connectionTimeout } : {},
|
|
87
|
+
...smtpConfig.tls !== undefined ? { tls: smtpConfig.tls } : {}
|
|
88
|
+
});
|
|
89
|
+
return new MailerImpl(new SMTPTransport({ ...smtpConfig, adapter }), smtpConfig.plugins);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class MailerImpl {
|
|
93
|
+
transport;
|
|
94
|
+
plugins;
|
|
95
|
+
constructor(transport, plugins = []) {
|
|
96
|
+
this.transport = transport;
|
|
97
|
+
this.plugins = plugins;
|
|
98
|
+
}
|
|
99
|
+
async send(options) {
|
|
100
|
+
const processed = await runPlugins(options, this.plugins);
|
|
101
|
+
return this.transport.send(processed);
|
|
102
|
+
}
|
|
103
|
+
async sendBulk(messages, options) {
|
|
104
|
+
const concurrency = options?.concurrency ?? 1;
|
|
105
|
+
const results = new Array(messages.length);
|
|
106
|
+
const queue = [...messages.entries()];
|
|
107
|
+
let active = 0;
|
|
108
|
+
await new Promise((resolve) => {
|
|
109
|
+
if (messages.length === 0) {
|
|
110
|
+
resolve();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const maybeDone = () => {
|
|
114
|
+
if (queue.length === 0 && active === 0) {
|
|
115
|
+
resolve();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const processNext = () => {
|
|
119
|
+
if (queue.length === 0) {
|
|
120
|
+
maybeDone();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const entry = queue.shift();
|
|
124
|
+
if (entry === undefined) {
|
|
125
|
+
maybeDone();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const [index, message] = entry;
|
|
129
|
+
active++;
|
|
130
|
+
this.send(message).then((result) => {
|
|
131
|
+
results[index] = { status: "sent", result };
|
|
132
|
+
options?.onSuccess?.(message, index, result);
|
|
133
|
+
}).catch((error) => {
|
|
134
|
+
results[index] = { status: "failed", error };
|
|
135
|
+
options?.onError?.(message, index, error);
|
|
136
|
+
}).finally(() => {
|
|
137
|
+
active--;
|
|
138
|
+
processNext();
|
|
139
|
+
maybeDone();
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
for (let i = 0;i < concurrency; i++) {
|
|
143
|
+
processNext();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
let sent = 0;
|
|
147
|
+
let failed = 0;
|
|
148
|
+
for (const result of results) {
|
|
149
|
+
if (result.status === "sent") {
|
|
150
|
+
sent++;
|
|
151
|
+
} else {
|
|
152
|
+
failed++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
total: messages.length,
|
|
157
|
+
sent,
|
|
158
|
+
failed,
|
|
159
|
+
results
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
verify() {
|
|
163
|
+
if (this.transport.verify) {
|
|
164
|
+
return this.transport.verify();
|
|
165
|
+
}
|
|
166
|
+
return Promise.resolve({ ok: true, provider: "mailer" });
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
if (this.transport.close) {
|
|
170
|
+
return this.transport.close();
|
|
171
|
+
}
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
detectRuntime,
|
|
177
|
+
createMailer,
|
|
178
|
+
createDefaultAdapter
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//# debugId=685AB5DE4D1E74D864756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/plugin.ts", "../src/detect.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Plugin pipeline for sently.\n * Plugins transform MailOptions before message construction.\n * They run sequentially — each receives the previous plugin's output.\n *\n * @example\n * ```ts\n * import { runPlugins } from \"sently/core/plugin\";\n * const result = await runPlugins(options, [pluginA, pluginB]);\n * ```\n */\nimport type { MailOptions, MailPlugin } from \"./types.js\";\n\n/**\n * Run a list of plugins sequentially over MailOptions.\n * If plugins is empty or undefined, returns options unchanged.\n * Each plugin may be sync or async.\n *\n * @param options - the original mail options\n * @param plugins - ordered list of plugins to apply\n * @returns transformed mail options after all plugins have run\n */\nexport async function runPlugins(\n options: MailOptions,\n plugins: MailPlugin[] | undefined,\n): Promise<MailOptions> {\n if (!plugins || plugins.length === 0) {\n return options;\n }\n\n let current = options;\n for (const plugin of plugins) {\n current = await plugin(current);\n }\n return current;\n}\n",
|
|
6
|
+
"// src/detect.ts\nimport { runPlugins } from \"./core/plugin.js\";\nimport type {\n BulkSendOptions,\n BulkSendResult,\n CreateMailerOptions,\n Mailer,\n MailOptions,\n MailPlugin,\n Runtime,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n TLSOptions,\n Transport,\n VerifyResult,\n} from \"./core/types.js\";\nimport { SMTPPool } from \"./pool/pool.js\";\nimport { SMTPTransport } from \"./transports/smtp.js\";\n\n/** Detect the current JavaScript runtime. */\nexport function detectRuntime(): Runtime {\n if (typeof Bun !== \"undefined\") {\n return \"bun\";\n }\n if (typeof Deno !== \"undefined\") {\n return \"deno\";\n }\n if (typeof caches !== \"undefined\" && globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cf-workers\";\n }\n if (typeof window !== \"undefined\") {\n return \"browser\";\n }\n if (typeof process !== \"undefined\" && process.versions?.node) {\n return \"node\";\n }\n return \"unknown\";\n}\n\n/**\n * Dynamically import and instantiate the correct adapter for the current runtime.\n */\nexport async function createDefaultAdapter(options?: {\n secure?: boolean;\n connectionTimeout?: number;\n tls?: TLSOptions;\n}): Promise<SocketAdapter> {\n const runtime = detectRuntime();\n\n switch (runtime) {\n case \"node\": {\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n }\n case \"bun\": {\n const { BunAdapter } = await import(\"./adapters/bun.js\");\n return new BunAdapter(options);\n }\n case \"deno\": {\n const { DenoAdapter } = await import(\"./adapters/deno.js\");\n return new DenoAdapter(options);\n }\n case \"cf-workers\": {\n const { CloudflareAdapter } = await import(\"./adapters/cf.js\");\n return new CloudflareAdapter(options);\n }\n default:\n throw new Error(`No socket adapter available for runtime: ${runtime}`);\n }\n}\n\n/**\n * Create a ready-to-use Mailer instance.\n */\nexport async function createMailer(options: CreateMailerOptions): Promise<Mailer> {\n if (\"transport\" in options) {\n return new MailerImpl(options.transport, options.plugins ?? []);\n }\n\n const smtpConfig = options as SMTPConfig;\n\n if (smtpConfig.pool) {\n return new MailerImpl(\n new SMTPPool(smtpConfig, {\n createAdapter: async () =>\n smtpConfig.adapter ??\n (await createDefaultAdapter({\n ...(smtpConfig.secure !== undefined ? { secure: smtpConfig.secure } : {}),\n ...(smtpConfig.connectionTimeout !== undefined\n ? { connectionTimeout: smtpConfig.connectionTimeout }\n : {}),\n ...(smtpConfig.tls !== undefined ? { tls: smtpConfig.tls } : {}),\n })),\n }),\n smtpConfig.plugins,\n );\n }\n\n const adapter =\n smtpConfig.adapter ??\n (await createDefaultAdapter({\n ...(smtpConfig.secure !== undefined ? { secure: smtpConfig.secure } : {}),\n ...(smtpConfig.connectionTimeout !== undefined\n ? { connectionTimeout: smtpConfig.connectionTimeout }\n : {}),\n ...(smtpConfig.tls !== undefined ? { tls: smtpConfig.tls } : {}),\n }));\n\n return new MailerImpl(new SMTPTransport({ ...smtpConfig, adapter }), smtpConfig.plugins);\n}\n\nclass MailerImpl implements Mailer {\n constructor(\n private readonly transport: Transport,\n private readonly plugins: MailPlugin[] = [],\n ) {}\n\n async send(options: MailOptions): Promise<SendResult> {\n const processed = await runPlugins(options, this.plugins);\n return this.transport.send(processed);\n }\n\n async sendBulk(messages: MailOptions[], options?: BulkSendOptions): Promise<BulkSendResult> {\n const concurrency = options?.concurrency ?? 1;\n const results: BulkSendResult[\"results\"] = new Array(messages.length);\n const queue = [...messages.entries()];\n let active = 0;\n\n await new Promise<void>((resolve) => {\n if (messages.length === 0) {\n resolve();\n return;\n }\n\n const maybeDone = (): void => {\n if (queue.length === 0 && active === 0) {\n resolve();\n }\n };\n\n const processNext = (): void => {\n if (queue.length === 0) {\n maybeDone();\n return;\n }\n\n const entry = queue.shift();\n if (entry === undefined) {\n maybeDone();\n return;\n }\n\n const [index, message] = entry;\n active++;\n\n void this.send(message)\n .then((result) => {\n results[index] = { status: \"sent\", result };\n options?.onSuccess?.(message, index, result);\n })\n .catch((error: unknown) => {\n results[index] = { status: \"failed\", error };\n options?.onError?.(message, index, error);\n })\n .finally(() => {\n active--;\n processNext();\n maybeDone();\n });\n };\n\n for (let i = 0; i < concurrency; i++) {\n processNext();\n }\n });\n\n let sent = 0;\n let failed = 0;\n for (const result of results) {\n if (result.status === \"sent\") {\n sent++;\n } else {\n failed++;\n }\n }\n\n return {\n total: messages.length,\n sent,\n failed,\n results,\n };\n }\n\n verify(): Promise<VerifyResult> {\n if (this.transport.verify) {\n return this.transport.verify();\n }\n return Promise.resolve({ ok: true, provider: \"mailer\" });\n }\n\n close(): Promise<void> {\n if (this.transport.close) {\n return this.transport.close();\n }\n return Promise.resolve();\n }\n}\n\ndeclare const Bun: unknown;\ndeclare const Deno: unknown;\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "685AB5DE4D1E74D864756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ResendTransport,
|
|
16
|
-
ResendError,
|
|
17
|
-
PreviewTransport,
|
|
18
|
-
PostmarkTransport,
|
|
19
|
-
PostmarkError,
|
|
20
|
-
OAuth2Client,
|
|
21
|
-
MailgunTransport,
|
|
22
|
-
MailgunError,
|
|
23
|
-
MICROSOFT_TOKEN_URL,
|
|
24
|
-
GOOGLE_TOKEN_URL,
|
|
25
|
-
BrevoTransport,
|
|
26
|
-
BrevoError
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
//# debugId=5A145E20F9DF002A64756E2164756E21
|
|
1
|
+
export { GOOGLE_TOKEN_URL, MICROSOFT_TOKEN_URL, OAuth2Client } from "./auth/oauth2.js";
|
|
2
|
+
export { SMTPError } from "./core/smtp.js";
|
|
3
|
+
export { createMailer, detectRuntime } from "./detect.js";
|
|
4
|
+
export { simpleEngine, templatePlugin } from "./plugins/template.js";
|
|
5
|
+
export { SMTPPool } from "./pool/pool.js";
|
|
6
|
+
export { BrevoError, BrevoTransport } from "./transports/brevo.js";
|
|
7
|
+
export { MailgunError, MailgunTransport } from "./transports/mailgun.js";
|
|
8
|
+
export { PostmarkError, PostmarkTransport } from "./transports/postmark.js";
|
|
9
|
+
export { PreviewTransport } from "./transports/preview.js";
|
|
10
|
+
export { ResendError, ResendTransport } from "./transports/resend.js";
|
|
11
|
+
export { RetryTransport } from "./transports/retry.js";
|
|
12
|
+
export { SendGridError, SendGridTransport } from "./transports/sendgrid.js";
|
|
13
|
+
export { SESError, SESTransport } from "./transports/ses.js";
|
|
14
|
+
export { SMTPTransport } from "./transports/smtp.js";
|
package/dist/pool/pool.js
CHANGED
|
@@ -1,277 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "../chunk-z3eq2t1d.js";
|
|
7
|
-
import {
|
|
8
|
-
buildMIME
|
|
9
|
-
} from "../chunk-j6qw8ms6.js";
|
|
10
|
-
import"../chunk-tjsgb3qb.js";
|
|
2
|
+
RateLimiter,
|
|
3
|
+
SMTPPool
|
|
4
|
+
} from "../chunk-mp5c9bfd.js";
|
|
5
|
+
import"../chunk-7fqv71z1.js";
|
|
11
6
|
import"../chunk-ym3zzv8b.js";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
import"../chunk-x3szga4k.js";
|
|
8
|
+
import"../chunk-tymfm441.js";
|
|
9
|
+
import"../chunk-f4c9ttmr.js";
|
|
15
10
|
import"../chunk-794hc3m4.js";
|
|
16
11
|
import"../chunk-v0bahtg2.js";
|
|
17
|
-
|
|
18
|
-
// src/pool/connection.ts
|
|
19
|
-
async function createPooledConnection(options) {
|
|
20
|
-
const config = resolveSMTPConfig(options.config);
|
|
21
|
-
const adapter = await options.createAdapter();
|
|
22
|
-
await adapter.connect(options.connectHost, config.port);
|
|
23
|
-
await openSMTPSession(adapter, config);
|
|
24
|
-
let messageCount = 0;
|
|
25
|
-
let idle = true;
|
|
26
|
-
let sendChain = Promise.resolve();
|
|
27
|
-
const maxMessages = options.maxMessages;
|
|
28
|
-
return {
|
|
29
|
-
get idle() {
|
|
30
|
-
return idle;
|
|
31
|
-
},
|
|
32
|
-
get messageCount() {
|
|
33
|
-
return messageCount;
|
|
34
|
-
},
|
|
35
|
-
get usable() {
|
|
36
|
-
return messageCount < maxMessages;
|
|
37
|
-
},
|
|
38
|
-
async send(mailOptions) {
|
|
39
|
-
const run = async () => {
|
|
40
|
-
idle = false;
|
|
41
|
-
try {
|
|
42
|
-
const resolvedOptions = {
|
|
43
|
-
...mailOptions,
|
|
44
|
-
attachments: await resolveAttachments(mailOptions.attachments)
|
|
45
|
-
};
|
|
46
|
-
const mime = await buildMIME(resolvedOptions, config.dkim);
|
|
47
|
-
const result = await deliverSMTPMessage(adapter, mime);
|
|
48
|
-
messageCount += 1;
|
|
49
|
-
return result;
|
|
50
|
-
} finally {
|
|
51
|
-
idle = true;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const resultPromise = sendChain.then(run);
|
|
55
|
-
sendChain = resultPromise.then(() => {
|
|
56
|
-
return;
|
|
57
|
-
}, () => {
|
|
58
|
-
return;
|
|
59
|
-
});
|
|
60
|
-
return resultPromise;
|
|
61
|
-
},
|
|
62
|
-
async close() {
|
|
63
|
-
await sendChain;
|
|
64
|
-
await closeSMTPSession(adapter);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// src/pool/pool.ts
|
|
70
|
-
class RateLimiter {
|
|
71
|
-
rateDelta;
|
|
72
|
-
rateLimit;
|
|
73
|
-
now;
|
|
74
|
-
tokens;
|
|
75
|
-
lastRefill;
|
|
76
|
-
waiters = [];
|
|
77
|
-
constructor(rateDelta, rateLimit, now = Date.now) {
|
|
78
|
-
this.rateDelta = rateDelta;
|
|
79
|
-
this.rateLimit = rateLimit;
|
|
80
|
-
this.now = now;
|
|
81
|
-
this.tokens = rateDelta;
|
|
82
|
-
this.lastRefill = now();
|
|
83
|
-
}
|
|
84
|
-
async acquire() {
|
|
85
|
-
for (;; ) {
|
|
86
|
-
this.refill();
|
|
87
|
-
if (this.tokens > 0) {
|
|
88
|
-
this.tokens -= 1;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
await new Promise((resolve) => {
|
|
92
|
-
this.waiters.push(resolve);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
notify() {
|
|
97
|
-
this.refill();
|
|
98
|
-
}
|
|
99
|
-
refill() {
|
|
100
|
-
const t = this.now();
|
|
101
|
-
const elapsed = t - this.lastRefill;
|
|
102
|
-
if (elapsed >= this.rateLimit) {
|
|
103
|
-
const periods = Math.floor(elapsed / this.rateLimit);
|
|
104
|
-
this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);
|
|
105
|
-
this.lastRefill += periods * this.rateLimit;
|
|
106
|
-
while (this.tokens > 0 && this.waiters.length > 0) {
|
|
107
|
-
this.tokens -= 1;
|
|
108
|
-
const next = this.waiters.shift();
|
|
109
|
-
next?.();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
class SMTPPool {
|
|
116
|
-
config;
|
|
117
|
-
maxConnections;
|
|
118
|
-
maxMessages;
|
|
119
|
-
createAdapterFn;
|
|
120
|
-
rateLimiter;
|
|
121
|
-
connections = [];
|
|
122
|
-
queue = [];
|
|
123
|
-
draining = false;
|
|
124
|
-
closed = false;
|
|
125
|
-
processChain = Promise.resolve();
|
|
126
|
-
constructor(config, options) {
|
|
127
|
-
this.config = config;
|
|
128
|
-
this.maxConnections = config.maxConnections ?? 5;
|
|
129
|
-
this.maxMessages = config.maxMessages ?? 100;
|
|
130
|
-
if (options?.createAdapter) {
|
|
131
|
-
const factory = options.createAdapter;
|
|
132
|
-
this.createAdapterFn = async () => factory();
|
|
133
|
-
} else if (config.adapter) {
|
|
134
|
-
this.createAdapterFn = async () => config.adapter;
|
|
135
|
-
} else {
|
|
136
|
-
throw new Error("SMTPPool requires config.adapter or options.createAdapter");
|
|
137
|
-
}
|
|
138
|
-
if (config.rateDelta !== undefined && config.rateDelta > 0) {
|
|
139
|
-
this.rateLimiter = new RateLimiter(config.rateDelta, config.rateLimit ?? 1000, options?.now);
|
|
140
|
-
} else {
|
|
141
|
-
this.rateLimiter = null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async send(options) {
|
|
145
|
-
if (this.draining) {
|
|
146
|
-
throw new Error("SMTPPool is closing — no new messages accepted");
|
|
147
|
-
}
|
|
148
|
-
if (this.closed) {
|
|
149
|
-
throw new Error("SMTPPool is closed");
|
|
150
|
-
}
|
|
151
|
-
if (this.rateLimiter) {
|
|
152
|
-
await this.rateLimiter.acquire();
|
|
153
|
-
}
|
|
154
|
-
return new Promise((resolve, reject) => {
|
|
155
|
-
this.queue.push({ options, resolve, reject });
|
|
156
|
-
this.scheduleProcess();
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
scheduleProcess() {
|
|
160
|
-
this.processChain = this.processChain.then(() => this.processQueue()).catch(() => {
|
|
161
|
-
return;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
async verify() {
|
|
165
|
-
try {
|
|
166
|
-
const conn = await this.spawnConnection();
|
|
167
|
-
try {
|
|
168
|
-
return { ok: true, provider: "smtp-pool" };
|
|
169
|
-
} finally {
|
|
170
|
-
await conn.close();
|
|
171
|
-
this.removeConnection(conn);
|
|
172
|
-
}
|
|
173
|
-
} catch (err) {
|
|
174
|
-
return {
|
|
175
|
-
ok: false,
|
|
176
|
-
provider: "smtp-pool",
|
|
177
|
-
message: err instanceof Error ? err.message : String(err)
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
async close() {
|
|
182
|
-
this.draining = true;
|
|
183
|
-
await this.drainQueue();
|
|
184
|
-
await Promise.allSettled(this.connections.map((c) => c.close()));
|
|
185
|
-
this.connections.length = 0;
|
|
186
|
-
this.closed = true;
|
|
187
|
-
}
|
|
188
|
-
get connectionCount() {
|
|
189
|
-
return this.connections.length;
|
|
190
|
-
}
|
|
191
|
-
get queueSize() {
|
|
192
|
-
return this.queue.length;
|
|
193
|
-
}
|
|
194
|
-
async processQueue() {
|
|
195
|
-
if (this.draining) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
while (this.queue.length > 0) {
|
|
199
|
-
const idleConn = this.connections.find((c) => c.idle && c.usable);
|
|
200
|
-
if (idleConn) {
|
|
201
|
-
const entry = this.queue.shift();
|
|
202
|
-
if (!entry) {
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
try {
|
|
206
|
-
const result = await idleConn.send(entry.options);
|
|
207
|
-
entry.resolve(result);
|
|
208
|
-
if (!idleConn.usable) {
|
|
209
|
-
await idleConn.close();
|
|
210
|
-
this.removeConnection(idleConn);
|
|
211
|
-
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
entry.reject(err);
|
|
214
|
-
await idleConn.close().catch(() => {
|
|
215
|
-
return;
|
|
216
|
-
});
|
|
217
|
-
this.removeConnection(idleConn);
|
|
218
|
-
}
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (this.connections.length < this.maxConnections) {
|
|
222
|
-
const entry = this.queue.shift();
|
|
223
|
-
if (!entry) {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
const conn = await this.spawnConnection();
|
|
227
|
-
try {
|
|
228
|
-
const result = await conn.send(entry.options);
|
|
229
|
-
entry.resolve(result);
|
|
230
|
-
if (!conn.usable) {
|
|
231
|
-
await conn.close();
|
|
232
|
-
this.removeConnection(conn);
|
|
233
|
-
}
|
|
234
|
-
} catch (err) {
|
|
235
|
-
entry.reject(err);
|
|
236
|
-
await conn.close().catch(() => {
|
|
237
|
-
return;
|
|
238
|
-
});
|
|
239
|
-
this.removeConnection(conn);
|
|
240
|
-
}
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async spawnConnection() {
|
|
247
|
-
const resolved = resolveSMTPConfig(this.config);
|
|
248
|
-
const conn = await createPooledConnection({
|
|
249
|
-
config: this.config,
|
|
250
|
-
maxMessages: this.maxMessages,
|
|
251
|
-
connectHost: resolved.host,
|
|
252
|
-
createAdapter: this.createAdapterFn
|
|
253
|
-
});
|
|
254
|
-
this.connections.push(conn);
|
|
255
|
-
return conn;
|
|
256
|
-
}
|
|
257
|
-
removeConnection(conn) {
|
|
258
|
-
const index = this.connections.indexOf(conn);
|
|
259
|
-
if (index >= 0) {
|
|
260
|
-
this.connections.splice(index, 1);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
async drainQueue() {
|
|
264
|
-
while (this.queue.length > 0 || this.connections.some((c) => !c.idle)) {
|
|
265
|
-
await this.processQueue();
|
|
266
|
-
if (this.queue.length > 0) {
|
|
267
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
12
|
export {
|
|
273
13
|
SMTPPool,
|
|
274
14
|
RateLimiter
|
|
275
15
|
};
|
|
276
16
|
|
|
277
|
-
//# debugId=
|
|
17
|
+
//# debugId=C06866950F09705E64756E2164756E21
|
package/dist/pool/pool.js.map
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [
|
|
3
|
+
"sources": [],
|
|
4
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
5
|
],
|
|
8
|
-
"mappings": "
|
|
9
|
-
"debugId": "
|
|
6
|
+
"mappings": "",
|
|
7
|
+
"debugId": "C06866950F09705E64756E2164756E21",
|
|
10
8
|
"names": []
|
|
11
9
|
}
|
package/dist/transports/brevo.js
CHANGED