@cmdsend/nodemailer 0.1.0 → 0.1.1
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 +11 -0
- package/README.md +11 -8
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.1] - 2026-09-10
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Docs: attachments and inline CID images are now a documented, supported part of
|
|
13
|
+
cmdsend's send API — the payload this transport already sends is the official
|
|
14
|
+
shape, so the "unconfirmed" caveat is gone. Documented the real limits
|
|
15
|
+
(20 attachments, 40 MB per message after base64, executable types rejected).
|
|
16
|
+
- Docs: confirmed that custom headers (`mail.headers`) are **dropped** by
|
|
17
|
+
cmdsend's send endpoint rather than honored. Don't depend on them.
|
|
18
|
+
|
|
8
19
|
## [0.1.0] - 2026-08-08
|
|
9
20
|
|
|
10
21
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @cmdsend/nodemailer
|
|
2
2
|
|
|
3
|
+
npm: https://www.npmjs.com/package/@cmdsend/nodemailer
|
|
4
|
+
|
|
3
5
|
Nodemailer transport for [cmdsend.com](https://cmdsend.com), a transactional email API built on Amazon SES.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
@@ -161,23 +163,24 @@ Retried automatically (see [Options](#options)). If it still fails after retries
|
|
|
161
163
|
The request didn't complete within `timeout`. Raise the `timeout` option, or check outbound network access from wherever this is running (e.g. a locked-down VPC or container).
|
|
162
164
|
|
|
163
165
|
**Attachment rejected / request too large**
|
|
164
|
-
cmdsend
|
|
166
|
+
cmdsend accepts up to 20 attachments totalling 40 MB per message (after base64 encoding), and rejects executable file types (`.exe`, `.bat`, `.js`, `.jar`, `.vbs`, …) with a `400 InvalidAttachment`. This transport doesn't enforce a client-side cap, so the message in the thrown `CmdsendError` is the source of truth for what the API actually refused.
|
|
165
167
|
|
|
166
168
|
**Getting a `CmdsendError` instead of a generic `Error`**
|
|
167
169
|
That's intentional — every failure from this transport (network error, timeout, non-2xx response) is normalized into a `CmdsendError` with `.statusCode` and `.code` set when available, instead of a raw `fetch`/`AbortError`.
|
|
168
170
|
|
|
169
171
|
## A note on API coverage
|
|
170
172
|
|
|
171
|
-
This transport was built against cmdsend's structured JSON send endpoint (`POST /v1/emails/send`) and the
|
|
173
|
+
This transport was built against cmdsend's structured JSON send endpoint (`POST /v1/emails/send`) and the `cmdsend` npm SDK. Where it stands against the documented request schema today:
|
|
172
174
|
|
|
173
|
-
- **Attachments and inline CID images** are sent as an `attachments` array (`filename`, base64 `content`, `content_type`, `content_id`)
|
|
174
|
-
- **Custom headers** (`mail.headers`) are sent as a `headers` object on the request body,
|
|
175
|
-
- **`Idempotency-Key`** — see [Idempotency](#idempotency) above.
|
|
176
|
-
- There is no
|
|
175
|
+
- **Attachments and inline CID images** are supported. They're sent as an `attachments` array (`filename`, base64 `content`, `content_type`, `content_id`) — the exact shape cmdsend documents, so Nodemailer attachments map straight through, CID images included.
|
|
176
|
+
- **Custom headers** (`mail.headers`) are still sent as a `headers` object on the request body. cmdsend's send endpoint does **not** accept this field: unrecognized fields are dropped, so the request succeeds but your headers don't reach the message. Don't depend on them.
|
|
177
|
+
- **`Idempotency-Key`** — see [Idempotency](#idempotency) above; still undocumented.
|
|
178
|
+
- There is no raw-MIME send endpoint, so this transport always sends structured JSON rather than a raw `message/rfc822` body. cmdsend composes the MIME server-side.
|
|
177
179
|
- `verify()` has no dedicated auth-check endpoint to call, so it does a `GET` on a placeholder email id and treats `401`/`403` as failure, anything else as success.
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
Everything except custom headers and idempotency maps onto documented API fields.
|
|
180
182
|
|
|
181
183
|
## Learn more
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
- npm package: https://www.npmjs.com/package/@cmdsend/nodemailer
|
|
186
|
+
- cmdsend API docs: https://cmdsend.com/docs
|
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/http.ts","../src/payload.ts","../src/transport.ts"],"sourcesContent":["import { CmdsendTransport, type CmdsendTransportOptions } from \"./transport.js\";\n\nexport { CmdsendTransport } from \"./transport.js\";\nexport type { CmdsendTransportOptions, CmdsendSentMessageInfo } from \"./transport.js\";\nexport { CmdsendError, type CmdsendErrorOptions } from \"./errors.js\";\nexport type { CmdsendSendPayload, CmdsendAttachmentPayload } from \"./payload.js\";\n\n/**\n * Creates a cmdsend transport for Nodemailer:\n *\n * ```ts\n * import nodemailer from \"nodemailer\";\n * import { cmdsendTransport } from \"nodemailer-cmdsend\";\n *\n * const transporter = nodemailer.createTransport(\n * cmdsendTransport({ apiKey: process.env.CMDSEND_API_KEY }),\n * );\n * ```\n */\nexport function cmdsendTransport(options?: CmdsendTransportOptions): CmdsendTransport {\n return new CmdsendTransport(options);\n}\n","export interface CmdsendErrorOptions {\n /** HTTP status code returned by the cmdsend API, if this error came from a response. */\n statusCode?: number;\n /** Machine-readable error code from the cmdsend API's error body (e.g. \"Unauthorized\"). */\n code?: string;\n cause?: unknown;\n}\n\n/**\n * Error type thrown by this transport. Network failures, timeouts, and\n * non-2xx API responses are all normalized into this shape instead of\n * leaking raw fetch/AbortError objects to callers.\n */\nexport class CmdsendError extends Error {\n readonly statusCode?: number;\n readonly code?: string;\n\n constructor(message: string, options: CmdsendErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"CmdsendError\";\n this.statusCode = options.statusCode;\n this.code = options.code;\n }\n}\n","import { CmdsendError } from \"./errors.js\";\n\n// 429 and 5xx are treated as transient and safe to retry. 4xx auth/validation\n// errors (400, 401, 403, 404, 422, ...) are never retried.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\n\nexport type FetchLike = typeof fetch;\n\nexport interface RequestJsonOptions {\n fetchImpl: FetchLike;\n maxRetries: number;\n timeoutMs: number;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff (base 1s, capped at 16s) with up to 50% jitter. */\nfunction backoffDelayMs(attempt: number): number {\n const base = Math.min(1000 * 2 ** attempt, 16_000);\n return base + Math.random() * base * 0.5;\n}\n\nfunction retryAfterDelayMs(retryAfter: string | null): number | undefined {\n if (!retryAfter) return undefined;\n const seconds = Number(retryAfter);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const dateMs = Date.parse(retryAfter);\n if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());\n return undefined;\n}\n\nasync function parseErrorBody(res: Response): Promise<{ message?: string; code?: string }> {\n try {\n const body = (await res.json()) as Record<string, unknown>;\n return {\n message: typeof body.message === \"string\" ? body.message : undefined,\n code: typeof body.error === \"string\" ? body.error : typeof body.code === \"string\" ? body.code : undefined,\n };\n } catch {\n return {};\n }\n}\n\n/**\n * POSTs/GETs JSON against the cmdsend API with retry on 429/5xx (respecting\n * Retry-After) and on transport-level failures (timeout, DNS, connection\n * reset). 4xx auth/validation errors fail immediately without retrying.\n */\nexport async function requestJson(\n url: string,\n init: { method: string; headers: Record<string, string>; body?: string },\n options: RequestJsonOptions,\n): Promise<unknown> {\n const { fetchImpl, maxRetries, timeoutMs } = options;\n let lastError: CmdsendError | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n\n try {\n res = await fetchImpl(url, { ...init, signal: controller.signal });\n } catch (err) {\n clearTimeout(timer);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = new CmdsendError(\n isAbort ? `cmdsend request timed out after ${timeoutMs}ms` : `cmdsend request failed: ${(err as Error).message}`,\n { cause: err },\n );\n if (attempt < maxRetries) {\n lastError = error;\n await sleep(backoffDelayMs(attempt));\n continue;\n }\n throw error;\n }\n\n clearTimeout(timer);\n\n if (res.ok) {\n return await res.json().catch(() => ({}));\n }\n\n const { message, code } = await parseErrorBody(res);\n const error = new CmdsendError(message ?? `cmdsend request failed with status ${res.status}`, {\n statusCode: res.status,\n code,\n });\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastError = error;\n const delay = retryAfterDelayMs(res.headers.get(\"retry-after\")) ?? backoffDelayMs(attempt);\n await sleep(delay);\n continue;\n }\n\n throw error;\n }\n\n throw lastError ?? new CmdsendError(\"cmdsend request failed\");\n}\n","import { CmdsendError } from \"./errors.js\";\n\ninterface NormalizedAddress {\n name?: string;\n address: string;\n}\n\ninterface NormalizedAttachment {\n content?: string | Buffer;\n encoding?: string;\n filename?: string | false;\n contentType?: string;\n cid?: string;\n}\n\n/** Shape of `mail.data` after `MailMessage#normalize()` has resolved it. */\nexport interface NormalizedMail {\n from?: NormalizedAddress | null;\n to?: NormalizedAddress[] | null;\n cc?: NormalizedAddress[] | null;\n bcc?: NormalizedAddress[] | null;\n replyTo?: NormalizedAddress[] | null;\n subject?: string;\n html?: string;\n text?: string;\n attachments?: NormalizedAttachment[];\n normalizedHeaders?: Record<string, string>;\n}\n\nexport interface CmdsendAttachmentPayload {\n filename: string;\n content: string;\n content_type?: string;\n content_id?: string;\n}\n\n/**\n * Body sent to POST /emails/send. Only `from`/`to`/`subject`/`html`/`text`/\n * `cc`/`bcc`/`reply_to` are confirmed fields (mirrored from the `cmdsend`\n * npm SDK). `attachments` and `headers` are NOT part of that confirmed\n * contract — see the \"API assumptions\" section of the README.\n */\nexport interface CmdsendSendPayload {\n from: string;\n to: string[];\n subject: string;\n html?: string;\n text?: string;\n cc?: string[];\n bcc?: string[];\n reply_to?: string;\n attachments?: CmdsendAttachmentPayload[];\n headers?: Record<string, string>;\n}\n\nfunction formatAddress(addr: NormalizedAddress): string {\n if (!addr.name) return addr.address;\n const needsQuoting = /[\",;:<>()@]/.test(addr.name) || /^\\s|\\s$/.test(addr.name);\n const name = needsQuoting ? `\"${addr.name.replace(/\"/g, '\\\\\"')}\"` : addr.name;\n return `${name} <${addr.address}>`;\n}\n\nfunction formatAddressList(list: NormalizedAddress[] | null | undefined): string[] | undefined {\n if (!list || list.length === 0) return undefined;\n return list.map(formatAddress);\n}\n\nfunction encodeAttachmentContent(attachment: NormalizedAttachment): string {\n const content = attachment.content;\n if (Buffer.isBuffer(content)) return content.toString(\"base64\");\n if (typeof content === \"string\") {\n // normalize() already base64-encodes Buffer-sourced content (and sets\n // encoding: \"base64\"); a plain string here means the user passed a raw\n // utf8 string, which we still need to encode for JSON transport.\n if (attachment.encoding === \"base64\") return content;\n return Buffer.from(content, \"utf8\").toString(\"base64\");\n }\n return \"\";\n}\n\nexport function buildSendPayload(data: NormalizedMail): CmdsendSendPayload {\n if (!data.from) {\n throw new CmdsendError('\"from\" address is required to send with cmdsend.');\n }\n const to = formatAddressList(data.to);\n if (!to) {\n throw new CmdsendError('At least one \"to\" address is required to send with cmdsend.');\n }\n\n const payload: CmdsendSendPayload = {\n from: formatAddress(data.from),\n to,\n subject: data.subject ?? \"\",\n };\n\n if (data.html) payload.html = data.html;\n if (data.text) payload.text = data.text;\n if (!payload.html && !payload.text) {\n throw new CmdsendError('Either \"html\" or \"text\" body is required to send with cmdsend.');\n }\n\n const cc = formatAddressList(data.cc);\n if (cc) payload.cc = cc;\n\n const bcc = formatAddressList(data.bcc);\n if (bcc) payload.bcc = bcc;\n\n const replyTo = formatAddressList(data.replyTo);\n if (replyTo) payload.reply_to = replyTo.join(\", \");\n\n if (data.attachments && data.attachments.length > 0) {\n payload.attachments = data.attachments.map((attachment) => {\n const mapped: CmdsendAttachmentPayload = {\n filename: typeof attachment.filename === \"string\" ? attachment.filename : \"attachment\",\n content: encodeAttachmentContent(attachment),\n };\n if (attachment.contentType) mapped.content_type = attachment.contentType;\n if (attachment.cid) mapped.content_id = attachment.cid;\n return mapped;\n });\n }\n\n if (data.normalizedHeaders && Object.keys(data.normalizedHeaders).length > 0) {\n payload.headers = data.normalizedHeaders;\n }\n\n return payload;\n}\n","import type MailMessage from \"nodemailer/lib/mailer/mail-message.js\";\nimport type { Transport } from \"nodemailer\";\n\nimport { CmdsendError } from \"./errors.js\";\nimport { requestJson, type FetchLike } from \"./http.js\";\nimport { buildSendPayload, type NormalizedMail } from \"./payload.js\";\n\n// Keep in sync with package.json \"version\".\nconst VERSION = \"0.1.0\";\n\nconst DEFAULT_BASE_URL = \"https://api.cmdsend.com/v1\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 3;\n\nexport interface CmdsendSentMessageInfo {\n /** cmdsend's email id when available, otherwise the generated Message-ID. */\n messageId: string;\n envelope: { from: string | false; to: string[] };\n /** Short human-readable summary of the API response, for logging. */\n response: string;\n}\n\nexport interface CmdsendTransportOptions {\n /** cmdsend API key. Falls back to the CMDSEND_API_KEY environment variable. */\n apiKey?: string;\n /** Override the API base URL (default: \"https://api.cmdsend.com/v1\"). */\n baseUrl?: string;\n /** Max retry attempts for 429/5xx responses and transport-level failures. Default: 3. */\n maxRetries?: number;\n /** Per-request timeout in milliseconds. Default: 30000. */\n timeout?: number;\n /** Inject a custom fetch implementation (e.g. for testing). Defaults to global fetch. */\n fetch?: FetchLike;\n /** Extra headers sent with every request. */\n headers?: Record<string, string>;\n /**\n * Controls the `Idempotency-Key` header sent with each send request.\n *\n * `true` (default): derive a key from the message's Message-ID header, so\n * retried requests (e.g. a serverless function retried after a timeout)\n * don't risk a duplicate send. `false`: don't send the header. Or pass a\n * function to compute your own key per message.\n *\n * NOTE: cmdsend's public API reference does not document idempotency key\n * support. This header is sent defensively; verify with cmdsend whether\n * it's honored server-side before relying on it to prevent duplicate\n * sends in production.\n */\n idempotencyKey?: boolean | ((mail: MailMessage) => string | undefined);\n}\n\ninterface CmdsendSendResponse {\n id?: string;\n status?: string;\n}\n\nfunction resolveApiKey(options: CmdsendTransportOptions): string {\n const apiKey = options.apiKey ?? process.env.CMDSEND_API_KEY;\n if (!apiKey) {\n throw new CmdsendError(\n \"cmdsend API key is required. Pass { apiKey } to the transport, or set the CMDSEND_API_KEY environment variable.\",\n );\n }\n return apiKey;\n}\n\nfunction resolveFetch(options: CmdsendTransportOptions): FetchLike {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new CmdsendError(\n \"No fetch implementation available. Use Node.js 18+ (which has a global fetch), or pass { fetch } explicitly.\",\n );\n }\n return fetchImpl;\n}\n\nfunction normalizeMail(mail: MailMessage): Promise<NormalizedMail> {\n return new Promise((resolve, reject) => {\n mail.normalize((err, data) => {\n if (err) {\n reject(err instanceof Error ? err : new CmdsendError(String(err)));\n return;\n }\n resolve(data as unknown as NormalizedMail);\n });\n });\n}\n\nfunction resolveIdempotencyKey(\n option: boolean | ((mail: MailMessage) => string | undefined) | undefined,\n mail: MailMessage,\n fallbackMessageId: string,\n): string | undefined {\n if (option === false) return undefined;\n if (typeof option === \"function\") return option(mail);\n // Default: derive from the Message-ID header (stripped of angle brackets)\n // so retrying the same message doesn't mint a new key.\n return fallbackMessageId.replace(/^<|>$/g, \"\") || undefined;\n}\n\n/**\n * Nodemailer transport for cmdsend.com. Sends via cmdsend's structured JSON\n * endpoint (POST /emails/send) — see README \"API assumptions\" for exactly\n * which fields are confirmed vs. best-effort.\n */\nexport class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {\n public readonly name = \"Cmdsend\";\n public readonly version = VERSION;\n\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #maxRetries: number;\n readonly #timeout: number;\n readonly #fetch: FetchLike;\n readonly #headers: Record<string, string>;\n readonly #idempotencyKey: boolean | ((mail: MailMessage) => string | undefined) | undefined;\n\n constructor(options: CmdsendTransportOptions = {}) {\n this.#apiKey = resolveApiKey(options);\n this.#fetch = resolveFetch(options);\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n this.#headers = options.headers ?? {};\n this.#idempotencyKey = options.idempotencyKey;\n }\n\n send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void {\n this.#send(mail).then(\n (info) => callback(null, info),\n (err: unknown) =>\n callback(err instanceof Error ? err : new CmdsendError(String(err)), undefined as unknown as CmdsendSentMessageInfo),\n );\n }\n\n async #send(mail: MailMessage): Promise<CmdsendSentMessageInfo> {\n const envelope = mail.message.getEnvelope();\n const fallbackMessageId = mail.message.messageId();\n const data = await normalizeMail(mail);\n const payload = buildSendPayload(data);\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Content-Type\": \"application/json\",\n ...this.#headers,\n };\n\n const idempotencyKey = resolveIdempotencyKey(this.#idempotencyKey, mail, fallbackMessageId);\n if (idempotencyKey) {\n headers[\"Idempotency-Key\"] = idempotencyKey;\n }\n\n const body = (await requestJson(\n `${this.#baseUrl}/emails/send`,\n { method: \"POST\", headers, body: JSON.stringify(payload) },\n { fetchImpl: this.#fetch, maxRetries: this.#maxRetries, timeoutMs: this.#timeout },\n )) as CmdsendSendResponse;\n\n const messageId = body?.id ?? fallbackMessageId;\n return {\n messageId,\n envelope,\n response: `${body?.status ?? \"accepted\"} id=${messageId}`,\n };\n }\n\n /**\n * Cheap auth check. cmdsend has no dedicated \"whoami\"/health endpoint, so\n * this issues a GET against the one other confirmed route (fetch-email-\n * by-id) with a placeholder id — a request that can never send mail. A\n * 401/403 response means the key is rejected; any other status (404\n * \"not found\", 200, ...) means the key was accepted by auth middleware.\n * This ordering assumption (auth checked before the id lookup) is not\n * confirmed against cmdsend's source — see the README.\n */\n verify(): Promise<true>;\n verify(callback: (err: Error | null, success: true) => void): void;\n verify(callback?: (err: Error | null, success: true) => void): void | Promise<true> {\n const promise = this.#verify();\n if (!callback) return promise;\n promise.then(\n (ok) => callback(null, ok),\n (err: unknown) => callback(err instanceof Error ? err : new CmdsendError(String(err)), true),\n );\n return undefined;\n }\n\n async #verify(): Promise<true> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeout);\n let res: Response;\n try {\n res = await this.#fetch(`${this.#baseUrl}/emails/00000000-0000-0000-0000-000000000000`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${this.#apiKey}`, ...this.#headers },\n signal: controller.signal,\n });\n } catch (err) {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n throw new CmdsendError(\n isAbort\n ? `cmdsend verification timed out after ${this.#timeout}ms`\n : `cmdsend verification failed: ${(err as Error).message}`,\n { cause: err },\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (res.status === 401 || res.status === 403) {\n const body = (await res.json().catch(() => ({}))) as { message?: string; error?: string };\n throw new CmdsendError(body.message ?? \"cmdsend authentication failed\", {\n statusCode: res.status,\n code: body.error,\n });\n }\n\n return true;\n }\n}\n"],"mappings":"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","names":["error"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/http.ts","../src/payload.ts","../src/transport.ts"],"sourcesContent":["import { CmdsendTransport, type CmdsendTransportOptions } from \"./transport.js\";\n\nexport { CmdsendTransport } from \"./transport.js\";\nexport type { CmdsendTransportOptions, CmdsendSentMessageInfo } from \"./transport.js\";\nexport { CmdsendError, type CmdsendErrorOptions } from \"./errors.js\";\nexport type { CmdsendSendPayload, CmdsendAttachmentPayload } from \"./payload.js\";\n\n/**\n * Creates a cmdsend transport for Nodemailer:\n *\n * ```ts\n * import nodemailer from \"nodemailer\";\n * import { cmdsendTransport } from \"nodemailer-cmdsend\";\n *\n * const transporter = nodemailer.createTransport(\n * cmdsendTransport({ apiKey: process.env.CMDSEND_API_KEY }),\n * );\n * ```\n */\nexport function cmdsendTransport(options?: CmdsendTransportOptions): CmdsendTransport {\n return new CmdsendTransport(options);\n}\n","export interface CmdsendErrorOptions {\n /** HTTP status code returned by the cmdsend API, if this error came from a response. */\n statusCode?: number;\n /** Machine-readable error code from the cmdsend API's error body (e.g. \"Unauthorized\"). */\n code?: string;\n cause?: unknown;\n}\n\n/**\n * Error type thrown by this transport. Network failures, timeouts, and\n * non-2xx API responses are all normalized into this shape instead of\n * leaking raw fetch/AbortError objects to callers.\n */\nexport class CmdsendError extends Error {\n readonly statusCode?: number;\n readonly code?: string;\n\n constructor(message: string, options: CmdsendErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"CmdsendError\";\n this.statusCode = options.statusCode;\n this.code = options.code;\n }\n}\n","import { CmdsendError } from \"./errors.js\";\n\n// 429 and 5xx are treated as transient and safe to retry. 4xx auth/validation\n// errors (400, 401, 403, 404, 422, ...) are never retried.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\n\nexport type FetchLike = typeof fetch;\n\nexport interface RequestJsonOptions {\n fetchImpl: FetchLike;\n maxRetries: number;\n timeoutMs: number;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff (base 1s, capped at 16s) with up to 50% jitter. */\nfunction backoffDelayMs(attempt: number): number {\n const base = Math.min(1000 * 2 ** attempt, 16_000);\n return base + Math.random() * base * 0.5;\n}\n\nfunction retryAfterDelayMs(retryAfter: string | null): number | undefined {\n if (!retryAfter) return undefined;\n const seconds = Number(retryAfter);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const dateMs = Date.parse(retryAfter);\n if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());\n return undefined;\n}\n\nasync function parseErrorBody(res: Response): Promise<{ message?: string; code?: string }> {\n try {\n const body = (await res.json()) as Record<string, unknown>;\n return {\n message: typeof body.message === \"string\" ? body.message : undefined,\n code: typeof body.error === \"string\" ? body.error : typeof body.code === \"string\" ? body.code : undefined,\n };\n } catch {\n return {};\n }\n}\n\n/**\n * POSTs/GETs JSON against the cmdsend API with retry on 429/5xx (respecting\n * Retry-After) and on transport-level failures (timeout, DNS, connection\n * reset). 4xx auth/validation errors fail immediately without retrying.\n */\nexport async function requestJson(\n url: string,\n init: { method: string; headers: Record<string, string>; body?: string },\n options: RequestJsonOptions,\n): Promise<unknown> {\n const { fetchImpl, maxRetries, timeoutMs } = options;\n let lastError: CmdsendError | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n\n try {\n res = await fetchImpl(url, { ...init, signal: controller.signal });\n } catch (err) {\n clearTimeout(timer);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = new CmdsendError(\n isAbort ? `cmdsend request timed out after ${timeoutMs}ms` : `cmdsend request failed: ${(err as Error).message}`,\n { cause: err },\n );\n if (attempt < maxRetries) {\n lastError = error;\n await sleep(backoffDelayMs(attempt));\n continue;\n }\n throw error;\n }\n\n clearTimeout(timer);\n\n if (res.ok) {\n return await res.json().catch(() => ({}));\n }\n\n const { message, code } = await parseErrorBody(res);\n const error = new CmdsendError(message ?? `cmdsend request failed with status ${res.status}`, {\n statusCode: res.status,\n code,\n });\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastError = error;\n const delay = retryAfterDelayMs(res.headers.get(\"retry-after\")) ?? backoffDelayMs(attempt);\n await sleep(delay);\n continue;\n }\n\n throw error;\n }\n\n throw lastError ?? new CmdsendError(\"cmdsend request failed\");\n}\n","import { CmdsendError } from \"./errors.js\";\n\ninterface NormalizedAddress {\n name?: string;\n address: string;\n}\n\ninterface NormalizedAttachment {\n content?: string | Buffer;\n encoding?: string;\n filename?: string | false;\n contentType?: string;\n cid?: string;\n}\n\n/** Shape of `mail.data` after `MailMessage#normalize()` has resolved it. */\nexport interface NormalizedMail {\n from?: NormalizedAddress | null;\n to?: NormalizedAddress[] | null;\n cc?: NormalizedAddress[] | null;\n bcc?: NormalizedAddress[] | null;\n replyTo?: NormalizedAddress[] | null;\n subject?: string;\n html?: string;\n text?: string;\n attachments?: NormalizedAttachment[];\n normalizedHeaders?: Record<string, string>;\n}\n\nexport interface CmdsendAttachmentPayload {\n filename: string;\n content: string;\n content_type?: string;\n content_id?: string;\n}\n\n/**\n * Body sent to POST /emails/send. Only `from`/`to`/`subject`/`html`/`text`/\n * `cc`/`bcc`/`reply_to` are confirmed fields (mirrored from the `cmdsend`\n * npm SDK). `attachments` and `headers` are NOT part of that confirmed\n * contract — see the \"API assumptions\" section of the README.\n */\nexport interface CmdsendSendPayload {\n from: string;\n to: string[];\n subject: string;\n html?: string;\n text?: string;\n cc?: string[];\n bcc?: string[];\n reply_to?: string;\n attachments?: CmdsendAttachmentPayload[];\n headers?: Record<string, string>;\n}\n\nfunction formatAddress(addr: NormalizedAddress): string {\n if (!addr.name) return addr.address;\n const needsQuoting = /[\",;:<>()@]/.test(addr.name) || /^\\s|\\s$/.test(addr.name);\n const name = needsQuoting ? `\"${addr.name.replace(/\"/g, '\\\\\"')}\"` : addr.name;\n return `${name} <${addr.address}>`;\n}\n\nfunction formatAddressList(list: NormalizedAddress[] | null | undefined): string[] | undefined {\n if (!list || list.length === 0) return undefined;\n return list.map(formatAddress);\n}\n\nfunction encodeAttachmentContent(attachment: NormalizedAttachment): string {\n const content = attachment.content;\n if (Buffer.isBuffer(content)) return content.toString(\"base64\");\n if (typeof content === \"string\") {\n // normalize() already base64-encodes Buffer-sourced content (and sets\n // encoding: \"base64\"); a plain string here means the user passed a raw\n // utf8 string, which we still need to encode for JSON transport.\n if (attachment.encoding === \"base64\") return content;\n return Buffer.from(content, \"utf8\").toString(\"base64\");\n }\n return \"\";\n}\n\nexport function buildSendPayload(data: NormalizedMail): CmdsendSendPayload {\n if (!data.from) {\n throw new CmdsendError('\"from\" address is required to send with cmdsend.');\n }\n const to = formatAddressList(data.to);\n if (!to) {\n throw new CmdsendError('At least one \"to\" address is required to send with cmdsend.');\n }\n\n const payload: CmdsendSendPayload = {\n from: formatAddress(data.from),\n to,\n subject: data.subject ?? \"\",\n };\n\n if (data.html) payload.html = data.html;\n if (data.text) payload.text = data.text;\n if (!payload.html && !payload.text) {\n throw new CmdsendError('Either \"html\" or \"text\" body is required to send with cmdsend.');\n }\n\n const cc = formatAddressList(data.cc);\n if (cc) payload.cc = cc;\n\n const bcc = formatAddressList(data.bcc);\n if (bcc) payload.bcc = bcc;\n\n const replyTo = formatAddressList(data.replyTo);\n if (replyTo) payload.reply_to = replyTo.join(\", \");\n\n if (data.attachments && data.attachments.length > 0) {\n payload.attachments = data.attachments.map((attachment) => {\n const mapped: CmdsendAttachmentPayload = {\n filename: typeof attachment.filename === \"string\" ? attachment.filename : \"attachment\",\n content: encodeAttachmentContent(attachment),\n };\n if (attachment.contentType) mapped.content_type = attachment.contentType;\n if (attachment.cid) mapped.content_id = attachment.cid;\n return mapped;\n });\n }\n\n if (data.normalizedHeaders && Object.keys(data.normalizedHeaders).length > 0) {\n payload.headers = data.normalizedHeaders;\n }\n\n return payload;\n}\n","import type MailMessage from \"nodemailer/lib/mailer/mail-message.js\";\nimport type { Transport } from \"nodemailer\";\n\nimport { CmdsendError } from \"./errors.js\";\nimport { requestJson, type FetchLike } from \"./http.js\";\nimport { buildSendPayload, type NormalizedMail } from \"./payload.js\";\n\n// Keep in sync with package.json \"version\".\nconst VERSION = \"0.1.1\";\n\nconst DEFAULT_BASE_URL = \"https://api.cmdsend.com/v1\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 3;\n\nexport interface CmdsendSentMessageInfo {\n /** cmdsend's email id when available, otherwise the generated Message-ID. */\n messageId: string;\n envelope: { from: string | false; to: string[] };\n /** Short human-readable summary of the API response, for logging. */\n response: string;\n}\n\nexport interface CmdsendTransportOptions {\n /** cmdsend API key. Falls back to the CMDSEND_API_KEY environment variable. */\n apiKey?: string;\n /** Override the API base URL (default: \"https://api.cmdsend.com/v1\"). */\n baseUrl?: string;\n /** Max retry attempts for 429/5xx responses and transport-level failures. Default: 3. */\n maxRetries?: number;\n /** Per-request timeout in milliseconds. Default: 30000. */\n timeout?: number;\n /** Inject a custom fetch implementation (e.g. for testing). Defaults to global fetch. */\n fetch?: FetchLike;\n /** Extra headers sent with every request. */\n headers?: Record<string, string>;\n /**\n * Controls the `Idempotency-Key` header sent with each send request.\n *\n * `true` (default): derive a key from the message's Message-ID header, so\n * retried requests (e.g. a serverless function retried after a timeout)\n * don't risk a duplicate send. `false`: don't send the header. Or pass a\n * function to compute your own key per message.\n *\n * NOTE: cmdsend's public API reference does not document idempotency key\n * support. This header is sent defensively; verify with cmdsend whether\n * it's honored server-side before relying on it to prevent duplicate\n * sends in production.\n */\n idempotencyKey?: boolean | ((mail: MailMessage) => string | undefined);\n}\n\ninterface CmdsendSendResponse {\n id?: string;\n status?: string;\n}\n\nfunction resolveApiKey(options: CmdsendTransportOptions): string {\n const apiKey = options.apiKey ?? process.env.CMDSEND_API_KEY;\n if (!apiKey) {\n throw new CmdsendError(\n \"cmdsend API key is required. Pass { apiKey } to the transport, or set the CMDSEND_API_KEY environment variable.\",\n );\n }\n return apiKey;\n}\n\nfunction resolveFetch(options: CmdsendTransportOptions): FetchLike {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new CmdsendError(\n \"No fetch implementation available. Use Node.js 18+ (which has a global fetch), or pass { fetch } explicitly.\",\n );\n }\n return fetchImpl;\n}\n\nfunction normalizeMail(mail: MailMessage): Promise<NormalizedMail> {\n return new Promise((resolve, reject) => {\n mail.normalize((err, data) => {\n if (err) {\n reject(err instanceof Error ? err : new CmdsendError(String(err)));\n return;\n }\n resolve(data as unknown as NormalizedMail);\n });\n });\n}\n\nfunction resolveIdempotencyKey(\n option: boolean | ((mail: MailMessage) => string | undefined) | undefined,\n mail: MailMessage,\n fallbackMessageId: string,\n): string | undefined {\n if (option === false) return undefined;\n if (typeof option === \"function\") return option(mail);\n // Default: derive from the Message-ID header (stripped of angle brackets)\n // so retrying the same message doesn't mint a new key.\n return fallbackMessageId.replace(/^<|>$/g, \"\") || undefined;\n}\n\n/**\n * Nodemailer transport for cmdsend.com. Sends via cmdsend's structured JSON\n * endpoint (POST /emails/send) — see README \"API assumptions\" for exactly\n * which fields are confirmed vs. best-effort.\n */\nexport class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {\n public readonly name = \"Cmdsend\";\n public readonly version = VERSION;\n\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #maxRetries: number;\n readonly #timeout: number;\n readonly #fetch: FetchLike;\n readonly #headers: Record<string, string>;\n readonly #idempotencyKey: boolean | ((mail: MailMessage) => string | undefined) | undefined;\n\n constructor(options: CmdsendTransportOptions = {}) {\n this.#apiKey = resolveApiKey(options);\n this.#fetch = resolveFetch(options);\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n this.#headers = options.headers ?? {};\n this.#idempotencyKey = options.idempotencyKey;\n }\n\n send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void {\n this.#send(mail).then(\n (info) => callback(null, info),\n (err: unknown) =>\n callback(err instanceof Error ? err : new CmdsendError(String(err)), undefined as unknown as CmdsendSentMessageInfo),\n );\n }\n\n async #send(mail: MailMessage): Promise<CmdsendSentMessageInfo> {\n const envelope = mail.message.getEnvelope();\n const fallbackMessageId = mail.message.messageId();\n const data = await normalizeMail(mail);\n const payload = buildSendPayload(data);\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Content-Type\": \"application/json\",\n ...this.#headers,\n };\n\n const idempotencyKey = resolveIdempotencyKey(this.#idempotencyKey, mail, fallbackMessageId);\n if (idempotencyKey) {\n headers[\"Idempotency-Key\"] = idempotencyKey;\n }\n\n const body = (await requestJson(\n `${this.#baseUrl}/emails/send`,\n { method: \"POST\", headers, body: JSON.stringify(payload) },\n { fetchImpl: this.#fetch, maxRetries: this.#maxRetries, timeoutMs: this.#timeout },\n )) as CmdsendSendResponse;\n\n const messageId = body?.id ?? fallbackMessageId;\n return {\n messageId,\n envelope,\n response: `${body?.status ?? \"accepted\"} id=${messageId}`,\n };\n }\n\n /**\n * Cheap auth check. cmdsend has no dedicated \"whoami\"/health endpoint, so\n * this issues a GET against the one other confirmed route (fetch-email-\n * by-id) with a placeholder id — a request that can never send mail. A\n * 401/403 response means the key is rejected; any other status (404\n * \"not found\", 200, ...) means the key was accepted by auth middleware.\n * This ordering assumption (auth checked before the id lookup) is not\n * confirmed against cmdsend's source — see the README.\n */\n verify(): Promise<true>;\n verify(callback: (err: Error | null, success: true) => void): void;\n verify(callback?: (err: Error | null, success: true) => void): void | Promise<true> {\n const promise = this.#verify();\n if (!callback) return promise;\n promise.then(\n (ok) => callback(null, ok),\n (err: unknown) => callback(err instanceof Error ? err : new CmdsendError(String(err)), true),\n );\n return undefined;\n }\n\n async #verify(): Promise<true> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeout);\n let res: Response;\n try {\n res = await this.#fetch(`${this.#baseUrl}/emails/00000000-0000-0000-0000-000000000000`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${this.#apiKey}`, ...this.#headers },\n signal: controller.signal,\n });\n } catch (err) {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n throw new CmdsendError(\n isAbort\n ? `cmdsend verification timed out after ${this.#timeout}ms`\n : `cmdsend verification failed: ${(err as Error).message}`,\n { cause: err },\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (res.status === 401 || res.status === 403) {\n const body = (await res.json().catch(() => ({}))) as { message?: string; error?: string };\n throw new CmdsendError(body.message ?? \"cmdsend authentication failed\", {\n statusCode: res.status,\n code: body.error,\n });\n }\n\n return true;\n }\n}\n"],"mappings":"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","names":["error"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -49,7 +49,7 @@ interface CmdsendTransportOptions {
|
|
|
49
49
|
declare class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {
|
|
50
50
|
#private;
|
|
51
51
|
readonly name = "Cmdsend";
|
|
52
|
-
readonly version = "0.1.
|
|
52
|
+
readonly version = "0.1.1";
|
|
53
53
|
constructor(options?: CmdsendTransportOptions);
|
|
54
54
|
send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void;
|
|
55
55
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ interface CmdsendTransportOptions {
|
|
|
49
49
|
declare class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {
|
|
50
50
|
#private;
|
|
51
51
|
readonly name = "Cmdsend";
|
|
52
|
-
readonly version = "0.1.
|
|
52
|
+
readonly version = "0.1.1";
|
|
53
53
|
constructor(options?: CmdsendTransportOptions);
|
|
54
54
|
send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void;
|
|
55
55
|
/**
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/payload.ts","../src/transport.ts","../src/index.ts"],"sourcesContent":["export interface CmdsendErrorOptions {\n /** HTTP status code returned by the cmdsend API, if this error came from a response. */\n statusCode?: number;\n /** Machine-readable error code from the cmdsend API's error body (e.g. \"Unauthorized\"). */\n code?: string;\n cause?: unknown;\n}\n\n/**\n * Error type thrown by this transport. Network failures, timeouts, and\n * non-2xx API responses are all normalized into this shape instead of\n * leaking raw fetch/AbortError objects to callers.\n */\nexport class CmdsendError extends Error {\n readonly statusCode?: number;\n readonly code?: string;\n\n constructor(message: string, options: CmdsendErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"CmdsendError\";\n this.statusCode = options.statusCode;\n this.code = options.code;\n }\n}\n","import { CmdsendError } from \"./errors.js\";\n\n// 429 and 5xx are treated as transient and safe to retry. 4xx auth/validation\n// errors (400, 401, 403, 404, 422, ...) are never retried.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\n\nexport type FetchLike = typeof fetch;\n\nexport interface RequestJsonOptions {\n fetchImpl: FetchLike;\n maxRetries: number;\n timeoutMs: number;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff (base 1s, capped at 16s) with up to 50% jitter. */\nfunction backoffDelayMs(attempt: number): number {\n const base = Math.min(1000 * 2 ** attempt, 16_000);\n return base + Math.random() * base * 0.5;\n}\n\nfunction retryAfterDelayMs(retryAfter: string | null): number | undefined {\n if (!retryAfter) return undefined;\n const seconds = Number(retryAfter);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const dateMs = Date.parse(retryAfter);\n if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());\n return undefined;\n}\n\nasync function parseErrorBody(res: Response): Promise<{ message?: string; code?: string }> {\n try {\n const body = (await res.json()) as Record<string, unknown>;\n return {\n message: typeof body.message === \"string\" ? body.message : undefined,\n code: typeof body.error === \"string\" ? body.error : typeof body.code === \"string\" ? body.code : undefined,\n };\n } catch {\n return {};\n }\n}\n\n/**\n * POSTs/GETs JSON against the cmdsend API with retry on 429/5xx (respecting\n * Retry-After) and on transport-level failures (timeout, DNS, connection\n * reset). 4xx auth/validation errors fail immediately without retrying.\n */\nexport async function requestJson(\n url: string,\n init: { method: string; headers: Record<string, string>; body?: string },\n options: RequestJsonOptions,\n): Promise<unknown> {\n const { fetchImpl, maxRetries, timeoutMs } = options;\n let lastError: CmdsendError | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n\n try {\n res = await fetchImpl(url, { ...init, signal: controller.signal });\n } catch (err) {\n clearTimeout(timer);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = new CmdsendError(\n isAbort ? `cmdsend request timed out after ${timeoutMs}ms` : `cmdsend request failed: ${(err as Error).message}`,\n { cause: err },\n );\n if (attempt < maxRetries) {\n lastError = error;\n await sleep(backoffDelayMs(attempt));\n continue;\n }\n throw error;\n }\n\n clearTimeout(timer);\n\n if (res.ok) {\n return await res.json().catch(() => ({}));\n }\n\n const { message, code } = await parseErrorBody(res);\n const error = new CmdsendError(message ?? `cmdsend request failed with status ${res.status}`, {\n statusCode: res.status,\n code,\n });\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastError = error;\n const delay = retryAfterDelayMs(res.headers.get(\"retry-after\")) ?? backoffDelayMs(attempt);\n await sleep(delay);\n continue;\n }\n\n throw error;\n }\n\n throw lastError ?? new CmdsendError(\"cmdsend request failed\");\n}\n","import { CmdsendError } from \"./errors.js\";\n\ninterface NormalizedAddress {\n name?: string;\n address: string;\n}\n\ninterface NormalizedAttachment {\n content?: string | Buffer;\n encoding?: string;\n filename?: string | false;\n contentType?: string;\n cid?: string;\n}\n\n/** Shape of `mail.data` after `MailMessage#normalize()` has resolved it. */\nexport interface NormalizedMail {\n from?: NormalizedAddress | null;\n to?: NormalizedAddress[] | null;\n cc?: NormalizedAddress[] | null;\n bcc?: NormalizedAddress[] | null;\n replyTo?: NormalizedAddress[] | null;\n subject?: string;\n html?: string;\n text?: string;\n attachments?: NormalizedAttachment[];\n normalizedHeaders?: Record<string, string>;\n}\n\nexport interface CmdsendAttachmentPayload {\n filename: string;\n content: string;\n content_type?: string;\n content_id?: string;\n}\n\n/**\n * Body sent to POST /emails/send. Only `from`/`to`/`subject`/`html`/`text`/\n * `cc`/`bcc`/`reply_to` are confirmed fields (mirrored from the `cmdsend`\n * npm SDK). `attachments` and `headers` are NOT part of that confirmed\n * contract — see the \"API assumptions\" section of the README.\n */\nexport interface CmdsendSendPayload {\n from: string;\n to: string[];\n subject: string;\n html?: string;\n text?: string;\n cc?: string[];\n bcc?: string[];\n reply_to?: string;\n attachments?: CmdsendAttachmentPayload[];\n headers?: Record<string, string>;\n}\n\nfunction formatAddress(addr: NormalizedAddress): string {\n if (!addr.name) return addr.address;\n const needsQuoting = /[\",;:<>()@]/.test(addr.name) || /^\\s|\\s$/.test(addr.name);\n const name = needsQuoting ? `\"${addr.name.replace(/\"/g, '\\\\\"')}\"` : addr.name;\n return `${name} <${addr.address}>`;\n}\n\nfunction formatAddressList(list: NormalizedAddress[] | null | undefined): string[] | undefined {\n if (!list || list.length === 0) return undefined;\n return list.map(formatAddress);\n}\n\nfunction encodeAttachmentContent(attachment: NormalizedAttachment): string {\n const content = attachment.content;\n if (Buffer.isBuffer(content)) return content.toString(\"base64\");\n if (typeof content === \"string\") {\n // normalize() already base64-encodes Buffer-sourced content (and sets\n // encoding: \"base64\"); a plain string here means the user passed a raw\n // utf8 string, which we still need to encode for JSON transport.\n if (attachment.encoding === \"base64\") return content;\n return Buffer.from(content, \"utf8\").toString(\"base64\");\n }\n return \"\";\n}\n\nexport function buildSendPayload(data: NormalizedMail): CmdsendSendPayload {\n if (!data.from) {\n throw new CmdsendError('\"from\" address is required to send with cmdsend.');\n }\n const to = formatAddressList(data.to);\n if (!to) {\n throw new CmdsendError('At least one \"to\" address is required to send with cmdsend.');\n }\n\n const payload: CmdsendSendPayload = {\n from: formatAddress(data.from),\n to,\n subject: data.subject ?? \"\",\n };\n\n if (data.html) payload.html = data.html;\n if (data.text) payload.text = data.text;\n if (!payload.html && !payload.text) {\n throw new CmdsendError('Either \"html\" or \"text\" body is required to send with cmdsend.');\n }\n\n const cc = formatAddressList(data.cc);\n if (cc) payload.cc = cc;\n\n const bcc = formatAddressList(data.bcc);\n if (bcc) payload.bcc = bcc;\n\n const replyTo = formatAddressList(data.replyTo);\n if (replyTo) payload.reply_to = replyTo.join(\", \");\n\n if (data.attachments && data.attachments.length > 0) {\n payload.attachments = data.attachments.map((attachment) => {\n const mapped: CmdsendAttachmentPayload = {\n filename: typeof attachment.filename === \"string\" ? attachment.filename : \"attachment\",\n content: encodeAttachmentContent(attachment),\n };\n if (attachment.contentType) mapped.content_type = attachment.contentType;\n if (attachment.cid) mapped.content_id = attachment.cid;\n return mapped;\n });\n }\n\n if (data.normalizedHeaders && Object.keys(data.normalizedHeaders).length > 0) {\n payload.headers = data.normalizedHeaders;\n }\n\n return payload;\n}\n","import type MailMessage from \"nodemailer/lib/mailer/mail-message.js\";\nimport type { Transport } from \"nodemailer\";\n\nimport { CmdsendError } from \"./errors.js\";\nimport { requestJson, type FetchLike } from \"./http.js\";\nimport { buildSendPayload, type NormalizedMail } from \"./payload.js\";\n\n// Keep in sync with package.json \"version\".\nconst VERSION = \"0.1.0\";\n\nconst DEFAULT_BASE_URL = \"https://api.cmdsend.com/v1\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 3;\n\nexport interface CmdsendSentMessageInfo {\n /** cmdsend's email id when available, otherwise the generated Message-ID. */\n messageId: string;\n envelope: { from: string | false; to: string[] };\n /** Short human-readable summary of the API response, for logging. */\n response: string;\n}\n\nexport interface CmdsendTransportOptions {\n /** cmdsend API key. Falls back to the CMDSEND_API_KEY environment variable. */\n apiKey?: string;\n /** Override the API base URL (default: \"https://api.cmdsend.com/v1\"). */\n baseUrl?: string;\n /** Max retry attempts for 429/5xx responses and transport-level failures. Default: 3. */\n maxRetries?: number;\n /** Per-request timeout in milliseconds. Default: 30000. */\n timeout?: number;\n /** Inject a custom fetch implementation (e.g. for testing). Defaults to global fetch. */\n fetch?: FetchLike;\n /** Extra headers sent with every request. */\n headers?: Record<string, string>;\n /**\n * Controls the `Idempotency-Key` header sent with each send request.\n *\n * `true` (default): derive a key from the message's Message-ID header, so\n * retried requests (e.g. a serverless function retried after a timeout)\n * don't risk a duplicate send. `false`: don't send the header. Or pass a\n * function to compute your own key per message.\n *\n * NOTE: cmdsend's public API reference does not document idempotency key\n * support. This header is sent defensively; verify with cmdsend whether\n * it's honored server-side before relying on it to prevent duplicate\n * sends in production.\n */\n idempotencyKey?: boolean | ((mail: MailMessage) => string | undefined);\n}\n\ninterface CmdsendSendResponse {\n id?: string;\n status?: string;\n}\n\nfunction resolveApiKey(options: CmdsendTransportOptions): string {\n const apiKey = options.apiKey ?? process.env.CMDSEND_API_KEY;\n if (!apiKey) {\n throw new CmdsendError(\n \"cmdsend API key is required. Pass { apiKey } to the transport, or set the CMDSEND_API_KEY environment variable.\",\n );\n }\n return apiKey;\n}\n\nfunction resolveFetch(options: CmdsendTransportOptions): FetchLike {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new CmdsendError(\n \"No fetch implementation available. Use Node.js 18+ (which has a global fetch), or pass { fetch } explicitly.\",\n );\n }\n return fetchImpl;\n}\n\nfunction normalizeMail(mail: MailMessage): Promise<NormalizedMail> {\n return new Promise((resolve, reject) => {\n mail.normalize((err, data) => {\n if (err) {\n reject(err instanceof Error ? err : new CmdsendError(String(err)));\n return;\n }\n resolve(data as unknown as NormalizedMail);\n });\n });\n}\n\nfunction resolveIdempotencyKey(\n option: boolean | ((mail: MailMessage) => string | undefined) | undefined,\n mail: MailMessage,\n fallbackMessageId: string,\n): string | undefined {\n if (option === false) return undefined;\n if (typeof option === \"function\") return option(mail);\n // Default: derive from the Message-ID header (stripped of angle brackets)\n // so retrying the same message doesn't mint a new key.\n return fallbackMessageId.replace(/^<|>$/g, \"\") || undefined;\n}\n\n/**\n * Nodemailer transport for cmdsend.com. Sends via cmdsend's structured JSON\n * endpoint (POST /emails/send) — see README \"API assumptions\" for exactly\n * which fields are confirmed vs. best-effort.\n */\nexport class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {\n public readonly name = \"Cmdsend\";\n public readonly version = VERSION;\n\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #maxRetries: number;\n readonly #timeout: number;\n readonly #fetch: FetchLike;\n readonly #headers: Record<string, string>;\n readonly #idempotencyKey: boolean | ((mail: MailMessage) => string | undefined) | undefined;\n\n constructor(options: CmdsendTransportOptions = {}) {\n this.#apiKey = resolveApiKey(options);\n this.#fetch = resolveFetch(options);\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n this.#headers = options.headers ?? {};\n this.#idempotencyKey = options.idempotencyKey;\n }\n\n send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void {\n this.#send(mail).then(\n (info) => callback(null, info),\n (err: unknown) =>\n callback(err instanceof Error ? err : new CmdsendError(String(err)), undefined as unknown as CmdsendSentMessageInfo),\n );\n }\n\n async #send(mail: MailMessage): Promise<CmdsendSentMessageInfo> {\n const envelope = mail.message.getEnvelope();\n const fallbackMessageId = mail.message.messageId();\n const data = await normalizeMail(mail);\n const payload = buildSendPayload(data);\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Content-Type\": \"application/json\",\n ...this.#headers,\n };\n\n const idempotencyKey = resolveIdempotencyKey(this.#idempotencyKey, mail, fallbackMessageId);\n if (idempotencyKey) {\n headers[\"Idempotency-Key\"] = idempotencyKey;\n }\n\n const body = (await requestJson(\n `${this.#baseUrl}/emails/send`,\n { method: \"POST\", headers, body: JSON.stringify(payload) },\n { fetchImpl: this.#fetch, maxRetries: this.#maxRetries, timeoutMs: this.#timeout },\n )) as CmdsendSendResponse;\n\n const messageId = body?.id ?? fallbackMessageId;\n return {\n messageId,\n envelope,\n response: `${body?.status ?? \"accepted\"} id=${messageId}`,\n };\n }\n\n /**\n * Cheap auth check. cmdsend has no dedicated \"whoami\"/health endpoint, so\n * this issues a GET against the one other confirmed route (fetch-email-\n * by-id) with a placeholder id — a request that can never send mail. A\n * 401/403 response means the key is rejected; any other status (404\n * \"not found\", 200, ...) means the key was accepted by auth middleware.\n * This ordering assumption (auth checked before the id lookup) is not\n * confirmed against cmdsend's source — see the README.\n */\n verify(): Promise<true>;\n verify(callback: (err: Error | null, success: true) => void): void;\n verify(callback?: (err: Error | null, success: true) => void): void | Promise<true> {\n const promise = this.#verify();\n if (!callback) return promise;\n promise.then(\n (ok) => callback(null, ok),\n (err: unknown) => callback(err instanceof Error ? err : new CmdsendError(String(err)), true),\n );\n return undefined;\n }\n\n async #verify(): Promise<true> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeout);\n let res: Response;\n try {\n res = await this.#fetch(`${this.#baseUrl}/emails/00000000-0000-0000-0000-000000000000`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${this.#apiKey}`, ...this.#headers },\n signal: controller.signal,\n });\n } catch (err) {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n throw new CmdsendError(\n isAbort\n ? `cmdsend verification timed out after ${this.#timeout}ms`\n : `cmdsend verification failed: ${(err as Error).message}`,\n { cause: err },\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (res.status === 401 || res.status === 403) {\n const body = (await res.json().catch(() => ({}))) as { message?: string; error?: string };\n throw new CmdsendError(body.message ?? \"cmdsend authentication failed\", {\n statusCode: res.status,\n code: body.error,\n });\n }\n\n return true;\n }\n}\n","import { CmdsendTransport, type CmdsendTransportOptions } from \"./transport.js\";\n\nexport { CmdsendTransport } from \"./transport.js\";\nexport type { CmdsendTransportOptions, CmdsendSentMessageInfo } from \"./transport.js\";\nexport { CmdsendError, type CmdsendErrorOptions } from \"./errors.js\";\nexport type { CmdsendSendPayload, CmdsendAttachmentPayload } from \"./payload.js\";\n\n/**\n * Creates a cmdsend transport for Nodemailer:\n *\n * ```ts\n * import nodemailer from \"nodemailer\";\n * import { cmdsendTransport } from \"nodemailer-cmdsend\";\n *\n * const transporter = nodemailer.createTransport(\n * cmdsendTransport({ apiKey: process.env.CMDSEND_API_KEY }),\n * );\n * ```\n */\nexport function cmdsendTransport(options?: CmdsendTransportOptions): CmdsendTransport {\n return new CmdsendTransport(options);\n}\n"],"mappings":"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","names":["error"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/payload.ts","../src/transport.ts","../src/index.ts"],"sourcesContent":["export interface CmdsendErrorOptions {\n /** HTTP status code returned by the cmdsend API, if this error came from a response. */\n statusCode?: number;\n /** Machine-readable error code from the cmdsend API's error body (e.g. \"Unauthorized\"). */\n code?: string;\n cause?: unknown;\n}\n\n/**\n * Error type thrown by this transport. Network failures, timeouts, and\n * non-2xx API responses are all normalized into this shape instead of\n * leaking raw fetch/AbortError objects to callers.\n */\nexport class CmdsendError extends Error {\n readonly statusCode?: number;\n readonly code?: string;\n\n constructor(message: string, options: CmdsendErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"CmdsendError\";\n this.statusCode = options.statusCode;\n this.code = options.code;\n }\n}\n","import { CmdsendError } from \"./errors.js\";\n\n// 429 and 5xx are treated as transient and safe to retry. 4xx auth/validation\n// errors (400, 401, 403, 404, 422, ...) are never retried.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\n\nexport type FetchLike = typeof fetch;\n\nexport interface RequestJsonOptions {\n fetchImpl: FetchLike;\n maxRetries: number;\n timeoutMs: number;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff (base 1s, capped at 16s) with up to 50% jitter. */\nfunction backoffDelayMs(attempt: number): number {\n const base = Math.min(1000 * 2 ** attempt, 16_000);\n return base + Math.random() * base * 0.5;\n}\n\nfunction retryAfterDelayMs(retryAfter: string | null): number | undefined {\n if (!retryAfter) return undefined;\n const seconds = Number(retryAfter);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const dateMs = Date.parse(retryAfter);\n if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());\n return undefined;\n}\n\nasync function parseErrorBody(res: Response): Promise<{ message?: string; code?: string }> {\n try {\n const body = (await res.json()) as Record<string, unknown>;\n return {\n message: typeof body.message === \"string\" ? body.message : undefined,\n code: typeof body.error === \"string\" ? body.error : typeof body.code === \"string\" ? body.code : undefined,\n };\n } catch {\n return {};\n }\n}\n\n/**\n * POSTs/GETs JSON against the cmdsend API with retry on 429/5xx (respecting\n * Retry-After) and on transport-level failures (timeout, DNS, connection\n * reset). 4xx auth/validation errors fail immediately without retrying.\n */\nexport async function requestJson(\n url: string,\n init: { method: string; headers: Record<string, string>; body?: string },\n options: RequestJsonOptions,\n): Promise<unknown> {\n const { fetchImpl, maxRetries, timeoutMs } = options;\n let lastError: CmdsendError | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n\n try {\n res = await fetchImpl(url, { ...init, signal: controller.signal });\n } catch (err) {\n clearTimeout(timer);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = new CmdsendError(\n isAbort ? `cmdsend request timed out after ${timeoutMs}ms` : `cmdsend request failed: ${(err as Error).message}`,\n { cause: err },\n );\n if (attempt < maxRetries) {\n lastError = error;\n await sleep(backoffDelayMs(attempt));\n continue;\n }\n throw error;\n }\n\n clearTimeout(timer);\n\n if (res.ok) {\n return await res.json().catch(() => ({}));\n }\n\n const { message, code } = await parseErrorBody(res);\n const error = new CmdsendError(message ?? `cmdsend request failed with status ${res.status}`, {\n statusCode: res.status,\n code,\n });\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastError = error;\n const delay = retryAfterDelayMs(res.headers.get(\"retry-after\")) ?? backoffDelayMs(attempt);\n await sleep(delay);\n continue;\n }\n\n throw error;\n }\n\n throw lastError ?? new CmdsendError(\"cmdsend request failed\");\n}\n","import { CmdsendError } from \"./errors.js\";\n\ninterface NormalizedAddress {\n name?: string;\n address: string;\n}\n\ninterface NormalizedAttachment {\n content?: string | Buffer;\n encoding?: string;\n filename?: string | false;\n contentType?: string;\n cid?: string;\n}\n\n/** Shape of `mail.data` after `MailMessage#normalize()` has resolved it. */\nexport interface NormalizedMail {\n from?: NormalizedAddress | null;\n to?: NormalizedAddress[] | null;\n cc?: NormalizedAddress[] | null;\n bcc?: NormalizedAddress[] | null;\n replyTo?: NormalizedAddress[] | null;\n subject?: string;\n html?: string;\n text?: string;\n attachments?: NormalizedAttachment[];\n normalizedHeaders?: Record<string, string>;\n}\n\nexport interface CmdsendAttachmentPayload {\n filename: string;\n content: string;\n content_type?: string;\n content_id?: string;\n}\n\n/**\n * Body sent to POST /emails/send. Only `from`/`to`/`subject`/`html`/`text`/\n * `cc`/`bcc`/`reply_to` are confirmed fields (mirrored from the `cmdsend`\n * npm SDK). `attachments` and `headers` are NOT part of that confirmed\n * contract — see the \"API assumptions\" section of the README.\n */\nexport interface CmdsendSendPayload {\n from: string;\n to: string[];\n subject: string;\n html?: string;\n text?: string;\n cc?: string[];\n bcc?: string[];\n reply_to?: string;\n attachments?: CmdsendAttachmentPayload[];\n headers?: Record<string, string>;\n}\n\nfunction formatAddress(addr: NormalizedAddress): string {\n if (!addr.name) return addr.address;\n const needsQuoting = /[\",;:<>()@]/.test(addr.name) || /^\\s|\\s$/.test(addr.name);\n const name = needsQuoting ? `\"${addr.name.replace(/\"/g, '\\\\\"')}\"` : addr.name;\n return `${name} <${addr.address}>`;\n}\n\nfunction formatAddressList(list: NormalizedAddress[] | null | undefined): string[] | undefined {\n if (!list || list.length === 0) return undefined;\n return list.map(formatAddress);\n}\n\nfunction encodeAttachmentContent(attachment: NormalizedAttachment): string {\n const content = attachment.content;\n if (Buffer.isBuffer(content)) return content.toString(\"base64\");\n if (typeof content === \"string\") {\n // normalize() already base64-encodes Buffer-sourced content (and sets\n // encoding: \"base64\"); a plain string here means the user passed a raw\n // utf8 string, which we still need to encode for JSON transport.\n if (attachment.encoding === \"base64\") return content;\n return Buffer.from(content, \"utf8\").toString(\"base64\");\n }\n return \"\";\n}\n\nexport function buildSendPayload(data: NormalizedMail): CmdsendSendPayload {\n if (!data.from) {\n throw new CmdsendError('\"from\" address is required to send with cmdsend.');\n }\n const to = formatAddressList(data.to);\n if (!to) {\n throw new CmdsendError('At least one \"to\" address is required to send with cmdsend.');\n }\n\n const payload: CmdsendSendPayload = {\n from: formatAddress(data.from),\n to,\n subject: data.subject ?? \"\",\n };\n\n if (data.html) payload.html = data.html;\n if (data.text) payload.text = data.text;\n if (!payload.html && !payload.text) {\n throw new CmdsendError('Either \"html\" or \"text\" body is required to send with cmdsend.');\n }\n\n const cc = formatAddressList(data.cc);\n if (cc) payload.cc = cc;\n\n const bcc = formatAddressList(data.bcc);\n if (bcc) payload.bcc = bcc;\n\n const replyTo = formatAddressList(data.replyTo);\n if (replyTo) payload.reply_to = replyTo.join(\", \");\n\n if (data.attachments && data.attachments.length > 0) {\n payload.attachments = data.attachments.map((attachment) => {\n const mapped: CmdsendAttachmentPayload = {\n filename: typeof attachment.filename === \"string\" ? attachment.filename : \"attachment\",\n content: encodeAttachmentContent(attachment),\n };\n if (attachment.contentType) mapped.content_type = attachment.contentType;\n if (attachment.cid) mapped.content_id = attachment.cid;\n return mapped;\n });\n }\n\n if (data.normalizedHeaders && Object.keys(data.normalizedHeaders).length > 0) {\n payload.headers = data.normalizedHeaders;\n }\n\n return payload;\n}\n","import type MailMessage from \"nodemailer/lib/mailer/mail-message.js\";\nimport type { Transport } from \"nodemailer\";\n\nimport { CmdsendError } from \"./errors.js\";\nimport { requestJson, type FetchLike } from \"./http.js\";\nimport { buildSendPayload, type NormalizedMail } from \"./payload.js\";\n\n// Keep in sync with package.json \"version\".\nconst VERSION = \"0.1.1\";\n\nconst DEFAULT_BASE_URL = \"https://api.cmdsend.com/v1\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 3;\n\nexport interface CmdsendSentMessageInfo {\n /** cmdsend's email id when available, otherwise the generated Message-ID. */\n messageId: string;\n envelope: { from: string | false; to: string[] };\n /** Short human-readable summary of the API response, for logging. */\n response: string;\n}\n\nexport interface CmdsendTransportOptions {\n /** cmdsend API key. Falls back to the CMDSEND_API_KEY environment variable. */\n apiKey?: string;\n /** Override the API base URL (default: \"https://api.cmdsend.com/v1\"). */\n baseUrl?: string;\n /** Max retry attempts for 429/5xx responses and transport-level failures. Default: 3. */\n maxRetries?: number;\n /** Per-request timeout in milliseconds. Default: 30000. */\n timeout?: number;\n /** Inject a custom fetch implementation (e.g. for testing). Defaults to global fetch. */\n fetch?: FetchLike;\n /** Extra headers sent with every request. */\n headers?: Record<string, string>;\n /**\n * Controls the `Idempotency-Key` header sent with each send request.\n *\n * `true` (default): derive a key from the message's Message-ID header, so\n * retried requests (e.g. a serverless function retried after a timeout)\n * don't risk a duplicate send. `false`: don't send the header. Or pass a\n * function to compute your own key per message.\n *\n * NOTE: cmdsend's public API reference does not document idempotency key\n * support. This header is sent defensively; verify with cmdsend whether\n * it's honored server-side before relying on it to prevent duplicate\n * sends in production.\n */\n idempotencyKey?: boolean | ((mail: MailMessage) => string | undefined);\n}\n\ninterface CmdsendSendResponse {\n id?: string;\n status?: string;\n}\n\nfunction resolveApiKey(options: CmdsendTransportOptions): string {\n const apiKey = options.apiKey ?? process.env.CMDSEND_API_KEY;\n if (!apiKey) {\n throw new CmdsendError(\n \"cmdsend API key is required. Pass { apiKey } to the transport, or set the CMDSEND_API_KEY environment variable.\",\n );\n }\n return apiKey;\n}\n\nfunction resolveFetch(options: CmdsendTransportOptions): FetchLike {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new CmdsendError(\n \"No fetch implementation available. Use Node.js 18+ (which has a global fetch), or pass { fetch } explicitly.\",\n );\n }\n return fetchImpl;\n}\n\nfunction normalizeMail(mail: MailMessage): Promise<NormalizedMail> {\n return new Promise((resolve, reject) => {\n mail.normalize((err, data) => {\n if (err) {\n reject(err instanceof Error ? err : new CmdsendError(String(err)));\n return;\n }\n resolve(data as unknown as NormalizedMail);\n });\n });\n}\n\nfunction resolveIdempotencyKey(\n option: boolean | ((mail: MailMessage) => string | undefined) | undefined,\n mail: MailMessage,\n fallbackMessageId: string,\n): string | undefined {\n if (option === false) return undefined;\n if (typeof option === \"function\") return option(mail);\n // Default: derive from the Message-ID header (stripped of angle brackets)\n // so retrying the same message doesn't mint a new key.\n return fallbackMessageId.replace(/^<|>$/g, \"\") || undefined;\n}\n\n/**\n * Nodemailer transport for cmdsend.com. Sends via cmdsend's structured JSON\n * endpoint (POST /emails/send) — see README \"API assumptions\" for exactly\n * which fields are confirmed vs. best-effort.\n */\nexport class CmdsendTransport implements Transport<CmdsendSentMessageInfo> {\n public readonly name = \"Cmdsend\";\n public readonly version = VERSION;\n\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #maxRetries: number;\n readonly #timeout: number;\n readonly #fetch: FetchLike;\n readonly #headers: Record<string, string>;\n readonly #idempotencyKey: boolean | ((mail: MailMessage) => string | undefined) | undefined;\n\n constructor(options: CmdsendTransportOptions = {}) {\n this.#apiKey = resolveApiKey(options);\n this.#fetch = resolveFetch(options);\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n this.#headers = options.headers ?? {};\n this.#idempotencyKey = options.idempotencyKey;\n }\n\n send(mail: MailMessage, callback: (err: Error | null, info: CmdsendSentMessageInfo) => void): void {\n this.#send(mail).then(\n (info) => callback(null, info),\n (err: unknown) =>\n callback(err instanceof Error ? err : new CmdsendError(String(err)), undefined as unknown as CmdsendSentMessageInfo),\n );\n }\n\n async #send(mail: MailMessage): Promise<CmdsendSentMessageInfo> {\n const envelope = mail.message.getEnvelope();\n const fallbackMessageId = mail.message.messageId();\n const data = await normalizeMail(mail);\n const payload = buildSendPayload(data);\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Content-Type\": \"application/json\",\n ...this.#headers,\n };\n\n const idempotencyKey = resolveIdempotencyKey(this.#idempotencyKey, mail, fallbackMessageId);\n if (idempotencyKey) {\n headers[\"Idempotency-Key\"] = idempotencyKey;\n }\n\n const body = (await requestJson(\n `${this.#baseUrl}/emails/send`,\n { method: \"POST\", headers, body: JSON.stringify(payload) },\n { fetchImpl: this.#fetch, maxRetries: this.#maxRetries, timeoutMs: this.#timeout },\n )) as CmdsendSendResponse;\n\n const messageId = body?.id ?? fallbackMessageId;\n return {\n messageId,\n envelope,\n response: `${body?.status ?? \"accepted\"} id=${messageId}`,\n };\n }\n\n /**\n * Cheap auth check. cmdsend has no dedicated \"whoami\"/health endpoint, so\n * this issues a GET against the one other confirmed route (fetch-email-\n * by-id) with a placeholder id — a request that can never send mail. A\n * 401/403 response means the key is rejected; any other status (404\n * \"not found\", 200, ...) means the key was accepted by auth middleware.\n * This ordering assumption (auth checked before the id lookup) is not\n * confirmed against cmdsend's source — see the README.\n */\n verify(): Promise<true>;\n verify(callback: (err: Error | null, success: true) => void): void;\n verify(callback?: (err: Error | null, success: true) => void): void | Promise<true> {\n const promise = this.#verify();\n if (!callback) return promise;\n promise.then(\n (ok) => callback(null, ok),\n (err: unknown) => callback(err instanceof Error ? err : new CmdsendError(String(err)), true),\n );\n return undefined;\n }\n\n async #verify(): Promise<true> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeout);\n let res: Response;\n try {\n res = await this.#fetch(`${this.#baseUrl}/emails/00000000-0000-0000-0000-000000000000`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${this.#apiKey}`, ...this.#headers },\n signal: controller.signal,\n });\n } catch (err) {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n throw new CmdsendError(\n isAbort\n ? `cmdsend verification timed out after ${this.#timeout}ms`\n : `cmdsend verification failed: ${(err as Error).message}`,\n { cause: err },\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (res.status === 401 || res.status === 403) {\n const body = (await res.json().catch(() => ({}))) as { message?: string; error?: string };\n throw new CmdsendError(body.message ?? \"cmdsend authentication failed\", {\n statusCode: res.status,\n code: body.error,\n });\n }\n\n return true;\n }\n}\n","import { CmdsendTransport, type CmdsendTransportOptions } from \"./transport.js\";\n\nexport { CmdsendTransport } from \"./transport.js\";\nexport type { CmdsendTransportOptions, CmdsendSentMessageInfo } from \"./transport.js\";\nexport { CmdsendError, type CmdsendErrorOptions } from \"./errors.js\";\nexport type { CmdsendSendPayload, CmdsendAttachmentPayload } from \"./payload.js\";\n\n/**\n * Creates a cmdsend transport for Nodemailer:\n *\n * ```ts\n * import nodemailer from \"nodemailer\";\n * import { cmdsendTransport } from \"nodemailer-cmdsend\";\n *\n * const transporter = nodemailer.createTransport(\n * cmdsendTransport({ apiKey: process.env.CMDSEND_API_KEY }),\n * );\n * ```\n */\nexport function cmdsendTransport(options?: CmdsendTransportOptions): CmdsendTransport {\n return new CmdsendTransport(options);\n}\n"],"mappings":"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","names":["error"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmdsend/nodemailer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Nodemailer transport for cmdsend.com
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Nodemailer transport for cmdsend.com \u2014 transactional email API on Amazon SES",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|