@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 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 has not published a specific attachment size limit as of this writing. This transport doesn't enforce a client-side cap if the API rejects a payload as too large, the error message in the thrown `CmdsendError` is the source of truth; treat it as such rather than a fixed number documented here.
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 existing `cmdsend` npm SDK, which is the closest thing to a contract available at the time of writing. A few things it supports are **not** confirmed against cmdsend's public API reference and should be verified before you depend on them in production:
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`). This field isn't part of the documented request schema.
174
- - **Custom headers** (`mail.headers`) are sent as a `headers` object on the request body, also undocumented.
175
- - **`Idempotency-Key`** — see [Idempotency](#idempotency) above.
176
- - There is no known raw-MIME send endpoint, so this transport always sends structured JSON rather than a raw `message/rfc822` body.
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
- A send that only uses documented fields (`from`/`to`/`cc`/`bcc`/`subject`/`html`/`text`/`reply_to`) isn't affected by any of this. But if you rely on attachments, custom headers, or idempotency: we don't know whether cmdsend's request validation ignores unrecognized fields or rejects the whole request, so test against your own account before depending on them in production.
181
+ Everything except custom headers and idempotency maps onto documented API fields.
180
182
 
181
183
  ## Learn more
182
184
 
183
- cmdsend API docs: https://cmdsend.com/docs
185
+ - npm package: https://www.npmjs.com/package/@cmdsend/nodemailer
186
+ - cmdsend API docs: https://cmdsend.com/docs
package/dist/index.cjs CHANGED
@@ -171,7 +171,7 @@ function buildSendPayload(data) {
171
171
  }
172
172
 
173
173
  // src/transport.ts
174
- var VERSION = "0.1.0";
174
+ var VERSION = "0.1.1";
175
175
  var DEFAULT_BASE_URL = "https://api.cmdsend.com/v1";
176
176
  var DEFAULT_TIMEOUT_MS = 3e4;
177
177
  var DEFAULT_MAX_RETRIES = 3;
@@ -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.0";
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.0";
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
@@ -143,7 +143,7 @@ function buildSendPayload(data) {
143
143
  }
144
144
 
145
145
  // src/transport.ts
146
- var VERSION = "0.1.0";
146
+ var VERSION = "0.1.1";
147
147
  var DEFAULT_BASE_URL = "https://api.cmdsend.com/v1";
148
148
  var DEFAULT_TIMEOUT_MS = 3e4;
149
149
  var DEFAULT_MAX_RETRIES = 3;
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.0",
4
- "description": "Nodemailer transport for cmdsend.com transactional email API on Amazon SES",
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",