sently 0.3.2 → 0.4.0
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 +108 -0
- package/README.md +98 -5
- package/dist/adapters/bun.d.ts +35 -0
- package/dist/{src/adapters → adapters}/bun.js +1 -1
- package/dist/adapters/cf.d.ts +55 -0
- package/dist/{src/adapters → adapters}/cf.js +1 -1
- package/dist/adapters/deno.d.ts +48 -0
- package/dist/{src/adapters → adapters}/deno.js +1 -1
- package/dist/adapters/node.d.ts +35 -0
- package/dist/{src/adapters → adapters}/node.js +1 -1
- package/dist/auth/oauth2.d.ts +34 -0
- package/dist/{src/auth → auth}/oauth2.js +3 -3
- package/dist/{chunk-hdqpvsm8.js → chunk-bvxkmq94.js} +12 -3
- package/dist/{chunk-hdqpvsm8.js.map → chunk-bvxkmq94.js.map} +3 -3
- package/dist/{chunk-dhbe64fc.js → chunk-j6qw8ms6.js} +1 -1
- package/dist/{chunk-qb05tsqn.js → chunk-tjsgb3qb.js} +2 -221
- package/dist/chunk-tjsgb3qb.js.map +11 -0
- package/dist/chunk-z3eq2t1d.js +244 -0
- package/dist/chunk-z3eq2t1d.js.map +10 -0
- package/dist/core/address.d.ts +21 -0
- package/dist/core/base64.d.ts +27 -0
- package/dist/core/cram-md5.d.ts +17 -0
- package/dist/core/dkim.d.ts +22 -0
- package/dist/core/mime.d.ts +13 -0
- package/dist/core/plugin.d.ts +23 -0
- package/dist/core/sigv4.d.ts +57 -0
- package/dist/core/smtp.d.ts +90 -0
- package/dist/core/types.d.ts +291 -0
- package/dist/detect.d.ts +15 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +29 -0
- package/dist/{src/index.js.map → index.js.map} +1 -1
- package/dist/plugins/template.d.ts +61 -0
- package/dist/plugins/template.js +29 -0
- package/dist/plugins/template.js.map +10 -0
- package/dist/pool/connection.d.ts +25 -0
- package/dist/pool/pool.d.ts +59 -0
- package/dist/{src/pool → pool}/pool.js +26 -14
- package/dist/pool/pool.js.map +11 -0
- package/dist/transports/brevo.d.ts +20 -0
- package/dist/{src/transports → transports}/brevo.js +32 -4
- package/dist/transports/brevo.js.map +10 -0
- package/dist/transports/mailgun.d.ts +22 -0
- package/dist/{src/transports → transports}/mailgun.js +29 -4
- package/dist/transports/mailgun.js.map +10 -0
- package/dist/transports/postmark.d.ts +24 -0
- package/dist/{src/transports → transports}/postmark.js +33 -4
- package/dist/transports/postmark.js.map +10 -0
- package/dist/transports/preview.d.ts +15 -0
- package/dist/transports/preview.js +73 -0
- package/dist/transports/preview.js.map +10 -0
- package/dist/transports/resend.d.ts +26 -0
- package/dist/{src/transports → transports}/resend.js +28 -4
- package/dist/transports/resend.js.map +10 -0
- package/dist/transports/resolve-attachments.d.ts +12 -0
- package/dist/transports/retry.d.ts +21 -0
- package/dist/transports/retry.js +79 -0
- package/dist/transports/retry.js.map +10 -0
- package/dist/transports/sendgrid.d.ts +24 -0
- package/dist/{src/transports → transports}/sendgrid.js +33 -4
- package/dist/transports/sendgrid.js.map +10 -0
- package/dist/transports/ses.d.ts +25 -0
- package/dist/{src/transports → transports}/ses.js +45 -6
- package/dist/{src/transports → transports}/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +52 -0
- package/dist/transports/smtp.js +27 -0
- package/dist/{src/transports → transports}/smtp.js.map +1 -1
- package/package.json +25 -4
- package/dist/chunk-qb05tsqn.js.map +0 -12
- package/dist/src/index.js +0 -18
- package/dist/src/pool/pool.js.map +0 -11
- package/dist/src/transports/brevo.js.map +0 -10
- package/dist/src/transports/mailgun.js.map +0 -10
- package/dist/src/transports/postmark.js.map +0 -10
- package/dist/src/transports/resend.js.map +0 -10
- package/dist/src/transports/sendgrid.js.map +0 -10
- package/dist/src/transports/smtp.js +0 -25
- /package/dist/{src/adapters → adapters}/bun.js.map +0 -0
- /package/dist/{src/adapters → adapters}/cf.js.map +0 -0
- /package/dist/{src/adapters → adapters}/deno.js.map +0 -0
- /package/dist/{src/adapters → adapters}/node.js.map +0 -0
- /package/dist/{src/auth → auth}/oauth2.js.map +0 -0
- /package/dist/{chunk-dhbe64fc.js.map → chunk-j6qw8ms6.js.map} +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/template.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Template plugin for sently.\n * Renders HTML email templates using a pluggable engine.\n * Built-in: simple {{variable}} interpolation (zero dependencies).\n * Bring your own engine: pass any render function.\n *\n * @example\n * ```ts\n * import { templatePlugin, simpleEngine } from 'sently/plugins/template'\n *\n * const mailer = await createMailer({\n * transport: new ResendTransport({ apiKey }),\n * plugins: [\n * templatePlugin({\n * engine: simpleEngine,\n * templates: {\n * welcome: '<h1>Hello, {{name}}!</h1>',\n * reset: '<p>Reset link: {{link}}</p>',\n * },\n * }),\n * ],\n * })\n *\n * await mailer.send({\n * from: '...',\n * to: '...',\n * subject: 'Welcome!',\n * template: 'welcome',\n * data: { name: 'Ali' },\n * })\n * ```\n */\nimport type { MailOptions, MailPlugin } from \"../core/types.js\";\n\n/** Renders a template string with the given data object. */\nexport type TemplateEngine = (template: string, data: Record<string, unknown>) => string;\n\n/** Configuration for the template plugin. */\nexport interface TemplatePluginConfig {\n /**\n * Rendering function. Use `simpleEngine` for zero-dep {{var}} interpolation,\n * or pass any function: (template, data) => string.\n */\n engine: TemplateEngine;\n /**\n * Map of template names to HTML strings.\n * Templates are loaded once at plugin creation time.\n */\n templates: Record<string, string>;\n}\n\n/**\n * Built-in zero-dependency template engine.\n * Replaces {{variable}} with the matching value from data.\n * Unknown variables are replaced with an empty string.\n */\nexport function simpleEngine(template: string, data: Record<string, unknown>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n const val = data[key];\n return val !== undefined && val !== null ? String(val) : \"\";\n });\n}\n\n/**\n * Create a template plugin from the given config.\n * The plugin reads options.template and options.data, renders the HTML,\n * sets options.html, and removes template/data before passing to the next plugin.\n */\nexport function templatePlugin(config: TemplatePluginConfig): MailPlugin {\n return (options: MailOptions): MailOptions => {\n if (!options.template) {\n return options;\n }\n\n const tmpl = config.templates[options.template];\n if (!tmpl) {\n throw new Error(`sently: template \"${options.template}\" not found`);\n }\n\n const html = config.engine(tmpl, options.data ?? {});\n const { template: _template, data: _data, ...rest } = options;\n return { ...rest, html };\n };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;AAyDO,SAAS,YAAY,CAAC,UAAkB,MAAuC;AAAA,EACpF,OAAO,SAAS,QAAQ,kBAAkB,CAAC,GAAG,QAAgB;AAAA,IAC5D,MAAM,MAAM,KAAK;AAAA,IACjB,OAAO,QAAQ,aAAa,QAAQ,OAAO,OAAO,GAAG,IAAI;AAAA,GAC1D;AAAA;AAQI,SAAS,cAAc,CAAC,QAA0C;AAAA,EACvE,OAAO,CAAC,YAAsC;AAAA,IAC5C,IAAI,CAAC,QAAQ,UAAU;AAAA,MACrB,OAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,OAAO,UAAU,QAAQ;AAAA,IACtC,IAAI,CAAC,MAAM;AAAA,MACT,MAAM,IAAI,MAAM,qBAAqB,QAAQ,qBAAqB;AAAA,IACpE;AAAA,IAEA,MAAM,OAAO,OAAO,OAAO,MAAM,QAAQ,QAAQ,CAAC,CAAC;AAAA,IACnD,QAAQ,UAAU,WAAW,MAAM,UAAU,SAAS;AAAA,IACtD,OAAO,KAAK,MAAM,KAAK;AAAA;AAAA;",
|
|
8
|
+
"debugId": "BF4AF728A3AA093064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MailOptions, SendResult, SMTPConfig } from "../core/types.js";
|
|
2
|
+
/** A single pooled SMTP connection with a persistent session. */
|
|
3
|
+
export interface PooledConnection {
|
|
4
|
+
/** Send one message over this connection. */
|
|
5
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
6
|
+
/** Whether this connection is idle and available for work. */
|
|
7
|
+
readonly idle: boolean;
|
|
8
|
+
/** Number of messages sent on this connection. */
|
|
9
|
+
readonly messageCount: number;
|
|
10
|
+
/** Whether this connection can accept more messages before recycle. */
|
|
11
|
+
readonly usable: boolean;
|
|
12
|
+
/** Close the connection and end the SMTP session. */
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Options for creating a pooled connection. */
|
|
16
|
+
export interface PooledConnectionOptions {
|
|
17
|
+
config: SMTPConfig;
|
|
18
|
+
maxMessages: number;
|
|
19
|
+
connectHost: string;
|
|
20
|
+
createAdapter: () => Promise<import("../core/types.js").SocketAdapter>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a pooled SMTP connection with an open authenticated session.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createPooledConnection(options: PooledConnectionOptions): Promise<PooledConnection>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MailOptions, PoolConfig, SendResult, SMTPConfig, SocketAdapter, Transport, VerifyResult } from "../core/types.js";
|
|
2
|
+
/** Options for {@link SMTPPool}. */
|
|
3
|
+
export interface SMTPPoolOptions {
|
|
4
|
+
/** Factory for a new socket adapter per pooled connection. */
|
|
5
|
+
createAdapter?: () => Promise<SocketAdapter> | SocketAdapter;
|
|
6
|
+
/** Injectable clock for rate limiting (testing). */
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Token bucket rate limiter with lazy refill on acquire.
|
|
11
|
+
*/
|
|
12
|
+
declare class RateLimiter {
|
|
13
|
+
private readonly rateDelta;
|
|
14
|
+
private readonly rateLimit;
|
|
15
|
+
private readonly now;
|
|
16
|
+
private tokens;
|
|
17
|
+
private lastRefill;
|
|
18
|
+
private waiters;
|
|
19
|
+
constructor(rateDelta: number, rateLimit: number, now?: () => number);
|
|
20
|
+
/** Wait until a token is available, then consume one. */
|
|
21
|
+
acquire(): Promise<void>;
|
|
22
|
+
/** Wake waiters after the clock advances (for testing). */
|
|
23
|
+
notify(): void;
|
|
24
|
+
private refill;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* SMTP connection pool with optional rate limiting.
|
|
28
|
+
*/
|
|
29
|
+
export declare class SMTPPool implements Transport {
|
|
30
|
+
private readonly config;
|
|
31
|
+
private readonly maxConnections;
|
|
32
|
+
private readonly maxMessages;
|
|
33
|
+
private readonly createAdapterFn;
|
|
34
|
+
private readonly rateLimiter;
|
|
35
|
+
private readonly connections;
|
|
36
|
+
private readonly queue;
|
|
37
|
+
private draining;
|
|
38
|
+
private closed;
|
|
39
|
+
private processChain;
|
|
40
|
+
/** Creates an SMTP connection pool. */
|
|
41
|
+
constructor(config: SMTPConfig & PoolConfig, options?: SMTPPoolOptions);
|
|
42
|
+
/** Sends a message through the pool. */
|
|
43
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
44
|
+
private scheduleProcess;
|
|
45
|
+
/** Verifies connectivity using a temporary connection. */
|
|
46
|
+
verify(): Promise<VerifyResult>;
|
|
47
|
+
/** Drains the queue and closes all connections. */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
/** Current number of open pooled connections. */
|
|
50
|
+
get connectionCount(): number;
|
|
51
|
+
/** Number of messages waiting in the send queue. */
|
|
52
|
+
get queueSize(): number;
|
|
53
|
+
private processQueue;
|
|
54
|
+
private spawnConnection;
|
|
55
|
+
private removeConnection;
|
|
56
|
+
private drainQueue;
|
|
57
|
+
}
|
|
58
|
+
/** @internal Exposed for deterministic rate limiter tests. */
|
|
59
|
+
export { RateLimiter };
|
|
@@ -3,16 +3,17 @@ import {
|
|
|
3
3
|
deliverSMTPMessage,
|
|
4
4
|
openSMTPSession,
|
|
5
5
|
resolveSMTPConfig
|
|
6
|
-
} from "
|
|
6
|
+
} from "../chunk-z3eq2t1d.js";
|
|
7
7
|
import {
|
|
8
8
|
buildMIME
|
|
9
|
-
} from "
|
|
10
|
-
import"
|
|
9
|
+
} from "../chunk-j6qw8ms6.js";
|
|
10
|
+
import"../chunk-tjsgb3qb.js";
|
|
11
|
+
import"../chunk-ym3zzv8b.js";
|
|
11
12
|
import {
|
|
12
13
|
resolveAttachments
|
|
13
|
-
} from "
|
|
14
|
-
import"
|
|
15
|
-
import"
|
|
14
|
+
} from "../chunk-bvxkmq94.js";
|
|
15
|
+
import"../chunk-794hc3m4.js";
|
|
16
|
+
import"../chunk-v0bahtg2.js";
|
|
16
17
|
|
|
17
18
|
// src/pool/connection.ts
|
|
18
19
|
async function createPooledConnection(options) {
|
|
@@ -141,6 +142,9 @@ class SMTPPool {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
async send(options) {
|
|
145
|
+
if (this.draining) {
|
|
146
|
+
throw new Error("SMTPPool is closing — no new messages accepted");
|
|
147
|
+
}
|
|
144
148
|
if (this.closed) {
|
|
145
149
|
throw new Error("SMTPPool is closed");
|
|
146
150
|
}
|
|
@@ -158,20 +162,28 @@ class SMTPPool {
|
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
164
|
async verify() {
|
|
161
|
-
const conn = await this.spawnConnection();
|
|
162
165
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
};
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
181
|
async close() {
|
|
170
|
-
this.closed = true;
|
|
171
182
|
this.draining = true;
|
|
172
183
|
await this.drainQueue();
|
|
173
|
-
await Promise.
|
|
184
|
+
await Promise.allSettled(this.connections.map((c) => c.close()));
|
|
174
185
|
this.connections.length = 0;
|
|
186
|
+
this.closed = true;
|
|
175
187
|
}
|
|
176
188
|
get connectionCount() {
|
|
177
189
|
return this.connections.length;
|
|
@@ -262,4 +274,4 @@ export {
|
|
|
262
274
|
RateLimiter
|
|
263
275
|
};
|
|
264
276
|
|
|
265
|
-
//# debugId=
|
|
277
|
+
//# debugId=8D3A435BD1D17B7464756E2164756E21
|
|
@@ -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": "8D3A435BD1D17B7464756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BrevoConfig, MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
2
|
+
/** Error thrown when the Brevo API returns a non-success response. */
|
|
3
|
+
export declare class BrevoError extends Error {
|
|
4
|
+
readonly statusCode: number;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
/** Creates a Brevo API error with status code and error code. */
|
|
7
|
+
constructor(message: string, statusCode: number, code: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Brevo HTTP API transport.
|
|
11
|
+
*/
|
|
12
|
+
export declare class BrevoTransport implements Transport {
|
|
13
|
+
private readonly apiKey;
|
|
14
|
+
/** Creates a Brevo transport with the given API key. */
|
|
15
|
+
constructor(config: BrevoConfig);
|
|
16
|
+
/** Sends an email via the Brevo HTTP API. */
|
|
17
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
18
|
+
/** Verifies the Brevo API key by fetching account info. */
|
|
19
|
+
verify(): Promise<VerifyResult>;
|
|
20
|
+
}
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
extractEmails,
|
|
3
3
|
parseAddresses,
|
|
4
4
|
resolveAttachments
|
|
5
|
-
} from "
|
|
5
|
+
} from "../chunk-bvxkmq94.js";
|
|
6
6
|
import {
|
|
7
7
|
encodeBase64
|
|
8
|
-
} from "
|
|
9
|
-
import"
|
|
8
|
+
} from "../chunk-794hc3m4.js";
|
|
9
|
+
import"../chunk-v0bahtg2.js";
|
|
10
10
|
|
|
11
11
|
// src/transports/brevo.ts
|
|
12
12
|
class BrevoError extends Error {
|
|
@@ -79,10 +79,38 @@ class BrevoTransport {
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
async verify() {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch("https://api.brevo.com/v3/account", {
|
|
85
|
+
headers: {
|
|
86
|
+
"api-key": this.apiKey
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const payload = await response.json();
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
provider: "brevo",
|
|
94
|
+
message: payload.message ?? `HTTP ${response.status}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
provider: "brevo",
|
|
100
|
+
...payload.companyName ? { message: payload.companyName } : {}
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
provider: "brevo",
|
|
106
|
+
message: err instanceof Error ? err.message : String(err)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
82
110
|
}
|
|
83
111
|
export {
|
|
84
112
|
BrevoTransport,
|
|
85
113
|
BrevoError
|
|
86
114
|
};
|
|
87
115
|
|
|
88
|
-
//# debugId=
|
|
116
|
+
//# debugId=C5824397C8CE442364756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/brevo.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Brevo (formerly Sendinblue) HTTP transport for sently.\n *\n * @example\n * ```ts\n * import { BrevoTransport } from \"sently/transports/brevo\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new BrevoTransport({ apiKey: \"xkeysib-...\" }),\n * });\n * ```\n */\nimport { extractEmails, parseAddresses } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport type {\n BrevoConfig,\n MailOptions,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Error thrown when the Brevo API returns a non-success response. */\nexport class BrevoError extends Error {\n /** Creates a Brevo API error with status code and error code. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n ) {\n super(message);\n this.name = \"BrevoError\";\n }\n}\n\nfunction toAddressObjects(input: MailOptions[\"to\"]): Array<{ email: string; name?: string }> {\n return parseAddresses(input).map((addr) => ({\n email: addr.address,\n ...(addr.name ? { name: addr.name } : {}),\n }));\n}\n\n/**\n * Brevo HTTP API transport.\n */\nexport class BrevoTransport implements Transport {\n private readonly apiKey: string;\n\n /** Creates a Brevo transport with the given API key. */\n constructor(config: BrevoConfig) {\n this.apiKey = config.apiKey;\n }\n\n /** Sends an email via the Brevo HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n\n const body: Record<string, unknown> = {\n sender: from\n ? { email: from.address, ...(from.name ? { name: from.name } : {}) }\n : { email: \"\" },\n to: toAddressObjects(options.to),\n subject: options.subject,\n ...(options.cc ? { cc: toAddressObjects(options.cc) } : {}),\n ...(options.bcc ? { bcc: toAddressObjects(options.bcc) } : {}),\n ...(options.replyTo\n ? {\n replyTo: (() => {\n const reply = parseAddresses(options.replyTo as MailOptions[\"to\"])[0];\n return reply\n ? { email: reply.address, ...(reply.name ? { name: reply.name } : {}) }\n : undefined;\n })(),\n }\n : {}),\n ...(options.html ? { htmlContent: options.html } : {}),\n ...(options.text ? { textContent: options.text } : {}),\n ...(attachments.length > 0\n ? {\n attachment: attachments.map((att) => ({\n name: att.filename,\n content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n })),\n }\n : {}),\n };\n\n const response = await fetch(\"https://api.brevo.com/v3/smtp/email\", {\n method: \"POST\",\n headers: {\n \"api-key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n messageId?: string;\n message?: string;\n code?: string;\n };\n\n if (!response.ok) {\n throw new BrevoError(\n payload.message ?? \"Brevo API error\",\n response.status,\n payload.code ?? \"\",\n );\n }\n\n const toEmails = extractEmails(options.to);\n return {\n messageId: payload.messageId ?? \"\",\n accepted: toEmails,\n rejected: [],\n response: payload.messageId ?? \"sent\",\n envelope: {\n from: from?.address ?? \"\",\n to: toEmails,\n },\n };\n }\n\n /** Verifies the Brevo API key by fetching account info. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(\"https://api.brevo.com/v3/account\", {\n headers: {\n \"api-key\": this.apiKey,\n },\n });\n\n const payload = (await response.json()) as { companyName?: string; message?: string };\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"brevo\",\n message: payload.message ?? `HTTP ${response.status}`,\n };\n }\n\n return {\n ok: true,\n provider: \"brevo\",\n ...(payload.companyName ? { message: payload.companyName } : {}),\n };\n } catch (err) {\n return {\n ok: false,\n provider: \"brevo\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C5824397C8CE442364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MailgunConfig, MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
2
|
+
/** Error thrown when the Mailgun API returns a non-success response. */
|
|
3
|
+
export declare class MailgunError extends Error {
|
|
4
|
+
readonly statusCode: number;
|
|
5
|
+
readonly apiError: unknown;
|
|
6
|
+
/** Creates a Mailgun API error with status code and response payload. */
|
|
7
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Mailgun HTTP API transport (multipart/form-data).
|
|
11
|
+
*/
|
|
12
|
+
export declare class MailgunTransport implements Transport {
|
|
13
|
+
private readonly apiKey;
|
|
14
|
+
private readonly domain;
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
/** Creates a Mailgun transport with the given API key and domain. */
|
|
17
|
+
constructor(config: MailgunConfig);
|
|
18
|
+
/** Sends an email via the Mailgun Messages API. */
|
|
19
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
20
|
+
/** Verifies the Mailgun API key by listing domains. */
|
|
21
|
+
verify(): Promise<VerifyResult>;
|
|
22
|
+
}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
parseAddresses,
|
|
4
4
|
resolveAttachments,
|
|
5
5
|
toMIMEHeader
|
|
6
|
-
} from "
|
|
6
|
+
} from "../chunk-bvxkmq94.js";
|
|
7
7
|
import {
|
|
8
8
|
encodeBase64
|
|
9
|
-
} from "
|
|
10
|
-
import"
|
|
9
|
+
} from "../chunk-794hc3m4.js";
|
|
10
|
+
import"../chunk-v0bahtg2.js";
|
|
11
11
|
|
|
12
12
|
// src/transports/mailgun.ts
|
|
13
13
|
class MailgunError extends Error {
|
|
@@ -86,10 +86,35 @@ class MailgunTransport {
|
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
async verify() {
|
|
90
|
+
try {
|
|
91
|
+
const auth = encodeBase64(`api:${this.apiKey}`).replace(/\r\n/g, "");
|
|
92
|
+
const response = await fetch(`${this.baseUrl}/v3/domains`, {
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Basic ${auth}`
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const payload = await response.json().catch(() => ({}));
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
provider: "mailgun",
|
|
102
|
+
message: payload.message ?? `HTTP ${response.status}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, provider: "mailgun", message: "API key is valid" };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
provider: "mailgun",
|
|
110
|
+
message: err instanceof Error ? err.message : String(err)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
89
114
|
}
|
|
90
115
|
export {
|
|
91
116
|
MailgunTransport,
|
|
92
117
|
MailgunError
|
|
93
118
|
};
|
|
94
119
|
|
|
95
|
-
//# debugId=
|
|
120
|
+
//# debugId=0BB5E6A11ACE98B264756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/mailgun.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Mailgun HTTP transport for sently.\n * Uses the Mailgun Messages API v3 with multipart/form-data.\n *\n * @example\n * ```ts\n * import { MailgunTransport } from \"sently/transports/mailgun\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new MailgunTransport({\n * apiKey: \"key-...\",\n * domain: \"mg.example.com\",\n * }),\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport type {\n MailgunConfig,\n MailOptions,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Error thrown when the Mailgun API returns a non-success response. */\nexport class MailgunError extends Error {\n /** Creates a Mailgun API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message);\n this.name = \"MailgunError\";\n }\n}\n\n/**\n * Mailgun HTTP API transport (multipart/form-data).\n */\nexport class MailgunTransport implements Transport {\n private readonly apiKey: string;\n private readonly domain: string;\n private readonly baseUrl: string;\n\n /** Creates a Mailgun transport with the given API key and domain. */\n constructor(config: MailgunConfig) {\n this.apiKey = config.apiKey;\n this.domain = config.domain;\n this.baseUrl =\n config.region === \"eu\" ? \"https://api.eu.mailgun.net\" : \"https://api.mailgun.net\";\n }\n\n /** Sends an email via the Mailgun Messages API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n const form = new FormData();\n\n form.append(\"from\", from ? toMIMEHeader(from) : \"\");\n form.append(\"to\", parseAddresses(options.to).map(toMIMEHeader).join(\", \"));\n\n if (options.cc) {\n form.append(\"cc\", parseAddresses(options.cc).map(toMIMEHeader).join(\", \"));\n }\n if (options.bcc) {\n form.append(\"bcc\", parseAddresses(options.bcc).map(toMIMEHeader).join(\", \"));\n }\n\n form.append(\"subject\", options.subject);\n\n if (options.text) {\n form.append(\"text\", options.text);\n }\n if (options.html) {\n form.append(\"html\", options.html);\n }\n if (options.replyTo) {\n const replyTo = parseAddresses(options.replyTo)[0];\n if (replyTo) {\n form.append(\"h:Reply-To\", toMIMEHeader(replyTo));\n }\n }\n\n for (const attachment of attachments) {\n const content =\n attachment.content instanceof Uint8Array\n ? attachment.content\n : new TextEncoder().encode(String(attachment.content ?? \"\"));\n const blob = new Blob([new Uint8Array(content)], {\n type: attachment.contentType ?? \"application/octet-stream\",\n });\n form.append(\"attachment\", blob, attachment.filename);\n }\n\n const auth = encodeBase64(`api:${this.apiKey}`).replace(/\\r\\n/g, \"\");\n const response = await fetch(`${this.baseUrl}/v3/${this.domain}/messages`, {\n method: \"POST\",\n headers: {\n Authorization: `Basic ${auth}`,\n },\n body: form,\n });\n\n const payload = (await response.json()) as { id?: string; message?: string };\n\n if (!response.ok) {\n throw new MailgunError(payload.message ?? \"Mailgun API error\", response.status, payload);\n }\n\n const toEmails = extractEmails(options.to);\n return {\n messageId: payload.id ?? \"\",\n accepted: toEmails,\n rejected: [],\n response: payload.message ?? \"queued\",\n envelope: {\n from: from?.address ?? \"\",\n to: toEmails,\n },\n };\n }\n\n /** Verifies the Mailgun API key by listing domains. */\n async verify(): Promise<VerifyResult> {\n try {\n const auth = encodeBase64(`api:${this.apiKey}`).replace(/\\r\\n/g, \"\");\n const response = await fetch(`${this.baseUrl}/v3/domains`, {\n headers: {\n Authorization: `Basic ${auth}`,\n },\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as { message?: string };\n return {\n ok: false,\n provider: \"mailgun\",\n message: payload.message ?? `HTTP ${response.status}`,\n };\n }\n\n return { ok: true, provider: \"mailgun\", message: \"API key is valid\" };\n } catch (err) {\n return {\n ok: false,\n provider: \"mailgun\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;AA8BO,MAAM,qBAAqB,MAAM;AAAA,EAIpB;AAAA,EACA;AAAA,EAHlB,WAAW,CACT,SACgB,YACA,UAChB;AAAA,IACA,MAAM,OAAO;AAAA,IAHG;AAAA,IACA;AAAA,IAGhB,KAAK,OAAO;AAAA;AAEhB;AAAA;AAKO,MAAM,iBAAsC;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EAGjB,WAAW,CAAC,QAAuB;AAAA,IACjC,KAAK,SAAS,OAAO;AAAA,IACrB,KAAK,SAAS,OAAO;AAAA,IACrB,KAAK,UACH,OAAO,WAAW,OAAO,+BAA+B;AAAA;AAAA,OAItD,KAAI,CAAC,SAA2C;AAAA,IACpD,MAAM,cAAc,MAAM,mBAAmB,QAAQ,WAAW;AAAA,IAChE,MAAM,OAAO,eAAe,QAAQ,IAAI,EAAE;AAAA,IAC1C,MAAM,OAAO,IAAI;AAAA,IAEjB,KAAK,OAAO,QAAQ,OAAO,aAAa,IAAI,IAAI,EAAE;AAAA,IAClD,KAAK,OAAO,MAAM,eAAe,QAAQ,EAAE,EAAE,IAAI,YAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IAEzE,IAAI,QAAQ,IAAI;AAAA,MACd,KAAK,OAAO,MAAM,eAAe,QAAQ,EAAE,EAAE,IAAI,YAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IAC3E;AAAA,IACA,IAAI,QAAQ,KAAK;AAAA,MACf,KAAK,OAAO,OAAO,eAAe,QAAQ,GAAG,EAAE,IAAI,YAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IAC7E;AAAA,IAEA,KAAK,OAAO,WAAW,QAAQ,OAAO;AAAA,IAEtC,IAAI,QAAQ,MAAM;AAAA,MAChB,KAAK,OAAO,QAAQ,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA,IAAI,QAAQ,MAAM;AAAA,MAChB,KAAK,OAAO,QAAQ,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA,IAAI,QAAQ,SAAS;AAAA,MACnB,MAAM,UAAU,eAAe,QAAQ,OAAO,EAAE;AAAA,MAChD,IAAI,SAAS;AAAA,QACX,KAAK,OAAO,cAAc,aAAa,OAAO,CAAC;AAAA,MACjD;AAAA,IACF;AAAA,IAEA,WAAW,cAAc,aAAa;AAAA,MACpC,MAAM,UACJ,WAAW,mBAAmB,aAC1B,WAAW,UACX,IAAI,YAAY,EAAE,OAAO,OAAO,WAAW,WAAW,EAAE,CAAC;AAAA,MAC/D,MAAM,OAAO,IAAI,KAAK,CAAC,IAAI,WAAW,OAAO,CAAC,GAAG;AAAA,QAC/C,MAAM,WAAW,eAAe;AAAA,MAClC,CAAC;AAAA,MACD,KAAK,OAAO,cAAc,MAAM,WAAW,QAAQ;AAAA,IACrD;AAAA,IAEA,MAAM,OAAO,aAAa,OAAO,KAAK,QAAQ,EAAE,QAAQ,SAAS,EAAE;AAAA,IACnE,MAAM,WAAW,MAAM,MAAM,GAAG,KAAK,cAAc,KAAK,mBAAmB;AAAA,MACzE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,SAAS;AAAA,MAC1B;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AAAA,IAED,MAAM,UAAW,MAAM,SAAS,KAAK;AAAA,IAErC,IAAI,CAAC,SAAS,IAAI;AAAA,MAChB,MAAM,IAAI,aAAa,QAAQ,WAAW,qBAAqB,SAAS,QAAQ,OAAO;AAAA,IACzF;AAAA,IAEA,MAAM,WAAW,cAAc,QAAQ,EAAE;AAAA,IACzC,OAAO;AAAA,MACL,WAAW,QAAQ,MAAM;AAAA,MACzB,UAAU;AAAA,MACV,UAAU,CAAC;AAAA,MACX,UAAU,QAAQ,WAAW;AAAA,MAC7B,UAAU;AAAA,QACR,MAAM,MAAM,WAAW;AAAA,QACvB,IAAI;AAAA,MACN;AAAA,IACF;AAAA;AAAA,OAII,OAAM,GAA0B;AAAA,IACpC,IAAI;AAAA,MACF,MAAM,OAAO,aAAa,OAAO,KAAK,QAAQ,EAAE,QAAQ,SAAS,EAAE;AAAA,MACnE,MAAM,WAAW,MAAM,MAAM,GAAG,KAAK,sBAAsB;AAAA,QACzD,SAAS;AAAA,UACP,eAAe,SAAS;AAAA,QAC1B;AAAA,MACF,CAAC;AAAA,MAED,IAAI,CAAC,SAAS,IAAI;AAAA,QAChB,MAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAAA,QACvD,OAAO;AAAA,UACL,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAS,QAAQ,WAAW,QAAQ,SAAS;AAAA,QAC/C;AAAA,MACF;AAAA,MAEA,OAAO,EAAE,IAAI,MAAM,UAAU,WAAW,SAAS,mBAAmB;AAAA,MACpE,OAAO,KAAK;AAAA,MACZ,OAAO;AAAA,QACL,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC1D;AAAA;AAAA;AAGN;",
|
|
8
|
+
"debugId": "0BB5E6A11ACE98B264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
2
|
+
/** Postmark API configuration. */
|
|
3
|
+
export interface PostmarkConfig {
|
|
4
|
+
serverToken: string;
|
|
5
|
+
}
|
|
6
|
+
/** Error thrown when the Postmark API returns a non-success response. */
|
|
7
|
+
export declare class PostmarkError extends Error {
|
|
8
|
+
readonly statusCode: number;
|
|
9
|
+
readonly apiError: unknown;
|
|
10
|
+
/** Creates a Postmark API error with status code and response payload. */
|
|
11
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Postmark HTTP API transport.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PostmarkTransport implements Transport {
|
|
17
|
+
private readonly serverToken;
|
|
18
|
+
/** Creates a Postmark transport with the given server token. */
|
|
19
|
+
constructor(config: PostmarkConfig);
|
|
20
|
+
/** Sends an email via the Postmark HTTP API. */
|
|
21
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
22
|
+
/** Verifies the Postmark server token by fetching server info. */
|
|
23
|
+
verify(): Promise<VerifyResult>;
|
|
24
|
+
}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
parseAddresses,
|
|
4
4
|
resolveAttachments,
|
|
5
5
|
toMIMEHeader
|
|
6
|
-
} from "
|
|
6
|
+
} from "../chunk-bvxkmq94.js";
|
|
7
7
|
import {
|
|
8
8
|
encodeBase64
|
|
9
|
-
} from "
|
|
10
|
-
import"
|
|
9
|
+
} from "../chunk-794hc3m4.js";
|
|
10
|
+
import"../chunk-v0bahtg2.js";
|
|
11
11
|
|
|
12
12
|
// src/transports/postmark.ts
|
|
13
13
|
class PostmarkError extends Error {
|
|
@@ -76,10 +76,39 @@ class PostmarkTransport {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
async verify() {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch("https://api.postmarkapp.com/server", {
|
|
82
|
+
headers: {
|
|
83
|
+
"X-Postmark-Server-Token": this.serverToken,
|
|
84
|
+
Accept: "application/json"
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const payload = await response.json();
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
provider: "postmark",
|
|
92
|
+
message: payload.Message ?? `HTTP ${response.status}`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
provider: "postmark",
|
|
98
|
+
...payload.Name ? { message: payload.Name } : {}
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
provider: "postmark",
|
|
104
|
+
message: err instanceof Error ? err.message : String(err)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
79
108
|
}
|
|
80
109
|
export {
|
|
81
110
|
PostmarkTransport,
|
|
82
111
|
PostmarkError
|
|
83
112
|
};
|
|
84
113
|
|
|
85
|
-
//# debugId=
|
|
114
|
+
//# debugId=650BF60942E0759C64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/postmark.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Postmark HTTP API transport for sending email via api.postmarkapp.com.\n *\n * @example\n * ```ts\n * import { PostmarkTransport } from \"sently/transports/postmark\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new PostmarkTransport({ serverToken: process.env.POSTMARK_TOKEN! }),\n * });\n *\n * await mailer.send({\n * from: \"sender@example.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * text: \"Plain text body\",\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Postmark API configuration. */\nexport interface PostmarkConfig {\n serverToken: string;\n}\n\n/** Error thrown when the Postmark API returns a non-success response. */\nexport class PostmarkError extends Error {\n /** Creates a Postmark API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message);\n this.name = \"PostmarkError\";\n }\n}\n\n/**\n * Postmark HTTP API transport.\n */\nexport class PostmarkTransport implements Transport {\n private readonly serverToken: string;\n\n /** Creates a Postmark transport with the given server token. */\n constructor(config: PostmarkConfig) {\n this.serverToken = config.serverToken;\n }\n\n /** Sends an email via the Postmark HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n\n const body = {\n From: from ? toMIMEHeader(from) : \"\",\n To: parseAddresses(options.to).map(toMIMEHeader).join(\", \"),\n Subject: options.subject,\n ...(options.cc ? { Cc: parseAddresses(options.cc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.bcc ? { Bcc: parseAddresses(options.bcc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.replyTo\n ? { ReplyTo: parseAddresses(options.replyTo).map(toMIMEHeader).join(\", \") }\n : {}),\n ...(options.text ? { TextBody: options.text } : {}),\n ...(options.html ? { HtmlBody: options.html } : {}),\n ...(options.headers\n ? { Headers: Object.entries(options.headers).map(([Name, Value]) => ({ Name, Value })) }\n : {}),\n ...(attachments.length > 0\n ? {\n Attachments: attachments.map((att) => ({\n Name: att.filename,\n Content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n ContentType: att.contentType ?? \"application/octet-stream\",\n ...(att.contentId ? { ContentID: att.contentId } : {}),\n })),\n }\n : {}),\n };\n\n const response = await fetch(\"https://api.postmarkapp.com/email\", {\n method: \"POST\",\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n MessageID?: string;\n Message?: string;\n ErrorCode?: number;\n };\n\n if (!response.ok) {\n throw new PostmarkError(payload.Message ?? \"Postmark API error\", response.status, payload);\n }\n\n return {\n messageId: payload.MessageID ?? options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: payload.Message ?? \"OK\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** Verifies the Postmark server token by fetching server info. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(\"https://api.postmarkapp.com/server\", {\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n Accept: \"application/json\",\n },\n });\n\n const payload = (await response.json()) as { Name?: string; Message?: string };\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"postmark\",\n message: payload.Message ?? `HTTP ${response.status}`,\n };\n }\n\n return {\n ok: true,\n provider: \"postmark\",\n ...(payload.Name ? { message: payload.Name } : {}),\n };\n } catch (err) {\n return {\n ok: false,\n provider: \"postmark\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "650BF60942E0759C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MailOptions, PreviewConfig, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Development transport that writes emails to disk instead of sending them.
|
|
4
|
+
*/
|
|
5
|
+
export declare class PreviewTransport implements Transport {
|
|
6
|
+
private readonly outDir;
|
|
7
|
+
private readonly open;
|
|
8
|
+
private readonly format;
|
|
9
|
+
/** Creates a preview transport with optional output directory and format. */
|
|
10
|
+
constructor(config?: PreviewConfig);
|
|
11
|
+
/** Writes the message to disk and returns a synthetic SendResult. */
|
|
12
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
13
|
+
/** Always succeeds — preview transport requires no external connectivity. */
|
|
14
|
+
verify(): Promise<VerifyResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildMIME
|
|
3
|
+
} from "../chunk-j6qw8ms6.js";
|
|
4
|
+
import {
|
|
5
|
+
extractEmails,
|
|
6
|
+
resolveAttachments
|
|
7
|
+
} from "../chunk-bvxkmq94.js";
|
|
8
|
+
import"../chunk-794hc3m4.js";
|
|
9
|
+
import {
|
|
10
|
+
__require
|
|
11
|
+
} from "../chunk-v0bahtg2.js";
|
|
12
|
+
|
|
13
|
+
// src/transports/preview.ts
|
|
14
|
+
function sanitizeSubject(subject) {
|
|
15
|
+
const sanitized = subject.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
16
|
+
return sanitized.slice(0, 40) || "no-subject";
|
|
17
|
+
}
|
|
18
|
+
function getOpenCommand(platform) {
|
|
19
|
+
if (platform === "darwin") {
|
|
20
|
+
return "open";
|
|
21
|
+
}
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
return "start";
|
|
24
|
+
}
|
|
25
|
+
return "xdg-open";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class PreviewTransport {
|
|
29
|
+
outDir;
|
|
30
|
+
open;
|
|
31
|
+
format;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.outDir = config?.outDir ?? "./.emails";
|
|
34
|
+
this.open = config?.open ?? false;
|
|
35
|
+
this.format = config?.format ?? "eml";
|
|
36
|
+
}
|
|
37
|
+
async send(options) {
|
|
38
|
+
const attachments = await resolveAttachments(options.attachments);
|
|
39
|
+
const resolvedOptions = { ...options, attachments };
|
|
40
|
+
const mime = await buildMIME(resolvedOptions);
|
|
41
|
+
const fs = await import("node:fs/promises");
|
|
42
|
+
await fs.mkdir(this.outDir, { recursive: true });
|
|
43
|
+
const filename = `${Date.now()}-${sanitizeSubject(options.subject)}.${this.format}`;
|
|
44
|
+
const filepath = `${this.outDir}/${filename}`;
|
|
45
|
+
if (this.format === "html") {
|
|
46
|
+
const html = options.html ?? (options.text ? `<pre>${options.text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</pre>` : "");
|
|
47
|
+
await fs.writeFile(filepath, html, "utf8");
|
|
48
|
+
} else {
|
|
49
|
+
await fs.writeFile(filepath, mime.raw);
|
|
50
|
+
}
|
|
51
|
+
console.log(`[sently preview] Written: ${filepath}`);
|
|
52
|
+
if (this.open) {
|
|
53
|
+
const { spawn } = await import("node:child_process");
|
|
54
|
+
const command = getOpenCommand(process.platform);
|
|
55
|
+
spawn(command, [filepath], { detached: true, stdio: "ignore" }).unref();
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
messageId: mime.messageId,
|
|
59
|
+
accepted: extractEmails(options.to),
|
|
60
|
+
rejected: [],
|
|
61
|
+
response: `preview: ${filepath}`,
|
|
62
|
+
envelope: mime.envelope
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async verify() {
|
|
66
|
+
return { ok: true, provider: "preview" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
PreviewTransport
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
//# debugId=A9DB64292AD1542264756E2164756E21
|