@iskra-bun/mailer-kit 0.1.0 → 0.2.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 +56 -0
- package/README.md +1 -1
- package/dist/{chunk-P5HWUNVS.js → chunk-4OJHFX3J.js} +26 -29
- package/dist/chunk-4OJHFX3J.js.map +1 -0
- package/dist/{chunk-H6SSV4G4.js → chunk-IACNO3YI.js} +31 -8
- package/dist/chunk-IACNO3YI.js.map +1 -0
- package/dist/chunk-U4ZHXPGW.js +65 -0
- package/dist/chunk-U4ZHXPGW.js.map +1 -0
- package/dist/chunk-URXCRWND.js +74 -0
- package/dist/chunk-URXCRWND.js.map +1 -0
- package/dist/{chunk-4Y2FQY2L.js → chunk-VRF3QYGZ.js} +24 -7
- package/dist/chunk-VRF3QYGZ.js.map +1 -0
- package/dist/index.d.ts +40 -10
- package/dist/index.js +24 -10
- package/dist/index.js.map +1 -1
- package/dist/mailgun-4CTSFUTK.js +8 -0
- package/dist/sendgrid-T6ZZERVV.js +8 -0
- package/dist/ses-WL4MNF4F.js +8 -0
- package/dist/smtp-J33QQBJ7.js +8 -0
- package/package.json +6 -3
- package/src/factory.ts +11 -11
- package/src/headers.ts +112 -0
- package/src/index.ts +9 -8
- package/src/mock.ts +14 -4
- package/src/providers/mailgun.ts +47 -54
- package/src/providers/sendgrid.ts +47 -19
- package/src/providers/ses.ts +44 -18
- package/src/providers/smtp.ts +35 -17
- package/src/types.ts +23 -6
- package/dist/chunk-4256VN6L.js +0 -40
- package/dist/chunk-4256VN6L.js.map +0 -1
- package/dist/chunk-4Y2FQY2L.js.map +0 -1
- package/dist/chunk-H6SSV4G4.js.map +0 -1
- package/dist/chunk-P5HWUNVS.js.map +0 -1
- package/dist/mailgun-CKVSFBJE.js +0 -7
- package/dist/sendgrid-EYSREU2M.js +0 -7
- package/dist/ses-KGJG5IB5.js +0 -7
- package/dist/smtp-GZ4DZMCQ.js +0 -7
- /package/dist/{mailgun-CKVSFBJE.js.map → mailgun-4CTSFUTK.js.map} +0 -0
- /package/dist/{sendgrid-EYSREU2M.js.map → sendgrid-T6ZZERVV.js.map} +0 -0
- /package/dist/{ses-KGJG5IB5.js.map → ses-WL4MNF4F.js.map} +0 -0
- /package/dist/{smtp-GZ4DZMCQ.js.map → smtp-J33QQBJ7.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @iskra-bun/mailer-kit
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [e86ed55]
|
|
8
|
+
- Updated dependencies [ae7c798]
|
|
9
|
+
- Updated dependencies [ef10372]
|
|
10
|
+
- @iskra-bun/core@0.3.0
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- ef2009b: **Security:** update dependencies with known vulnerabilities (`bun audit` went from 67 findings, 1 critical and 38 high, to one accepted dev-only finding).
|
|
17
|
+
|
|
18
|
+
- `better-auth` ^1.6.33 (account takeover via pre-account hijacking), `hono` ^4.12.34, `ajv` ^8.20.0, `mysql2` ^3.24.4 (web-kit, auth-kit, db-kit).
|
|
19
|
+
- `drizzle-orm` ^0.45.2 (SQL injection via improperly escaped identifiers). **db-kit moves from 0.30 to 0.45**, the same line web-kit and auth-kit already used, so schemas are shared across kits again; `drizzle-kit` ^0.31.11 now matches it (0.30 exited with "requires newer version of drizzle-orm", so migrations never ran), and `@libsql/client` ^0.18.0 satisfies drizzle's peer range.
|
|
20
|
+
- `c12` ^3.3.4 in core (drops the vulnerable `tar` 6 pulled in through `giget` 1).
|
|
21
|
+
- `nodemailer` ^10.0.10 in mailer-kit (arbitrary file read / SSRF via the raw option, SMTP command and header injection).
|
|
22
|
+
|
|
23
|
+
- 31a3a94: **Breaking (0.x):** `Reply-To` is no longer accepted in a message's `headers`: set it with `message.replyTo`. The header's value was only cut at CR/LF, so it could carry several addresses or a display name that `replyTo`'s checks refuse. A `Reply-To` in `headers` now throws `Header "Reply-To" is not allowed: use message.replyTo`.
|
|
24
|
+
- 938dd41: **Security** fixes from the data-kits audit (round 2).
|
|
25
|
+
|
|
26
|
+
- `storage-kit` (**breaking**): files are stored and served with a type from their extension (`contentTypeFor`), and anything but a raster image as a download. The S3 adapter stores a `Content-Disposition` with each object (`attachment` unless it is a PNG, JPEG, GIF, WebP, AVIF, BMP or ICO image; `put(..., { contentDisposition })` to choose), and `url()` signs `response-content-type` and `response-content-disposition` into presigned URLs whatever the object was stored with (`url(path, expiresIn, { contentType, contentDisposition })` to choose): an upload named `logo.svg` or `invoice.html` was stored as `image/svg+xml`/`text/html` and ran its scripts on the bucket's origin. HTML, SVG, XML and JavaScript are now `application/octet-stream`. `put(..., { overwrite: false })` throws the new `FileExistsError` instead of replacing a stored file (S3 `If-None-Match: *`, an exclusive create locally); `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` now need 3.635 or later, the first releases that send `If-None-Match` on a put (earlier ones dropped it, and the file was replaced). The plaintext-endpoint guard parses the endpoint as a URL, as the SDK does: `http:/minio:9000`, `http:minio:9000` and `http:\\minio:9000` were accepted without `useSSL: false`; an endpoint that is not an `http(s)` URL is rejected.
|
|
27
|
+
- `web-kit` uploads (**breaking**): the upload route stores a file with the type of its extension, never `File.type` (which Bun derives from the name), and `uploadFromRequest()` too. Downloads are streamed with `getStream()` (each one was buffered twice), typed by extension, `attachment` unless a raster image, and sandboxed (`Content-Security-Policy: sandbox`). Without `allowedExtensions`, active web content (`.html`, `.svg`, `.xml`, `.js`...) is refused (400) unless listed. `authorize(c, action, target)` receives what the action touches (`{ key, subfolder, filename, size, type }`), and `upload` is asked again with it before the file is written. An upload no longer replaces a stored file: **409** unless `overwrite: true`.
|
|
28
|
+
- `mailer-kit` (**breaking**): every `to`, `cc`, `bcc` and `replyTo` entry must be one bare address, or a new `{ name, address }` object for a display name, in every adapter (the mock too); only `from` was checked. One value such as `"bob@example.com <attacker@evil.test>, x@example.com"`, a group (`"undisclosed: a@evil.test; b@x.com"`, `"a@evil.test:b@x.com"`) or `{ address: "bob@example.com\r\nBcc: …" }` mailed other recipients than the ones an allowlist checked. Addresses may not contain whitespace, control characters or `<>()[]\,;:"` and need exactly one `@`; `replyTo` takes one recipient. `checkRecipients()` is exported. Mailgun cuts the `subject` at a CR/LF, as it does header values.
|
|
29
|
+
- `web-kit` email: `EmailFeature`'s adapter checks recipients with mailer-kit's rules (object recipients were tested as `"[object Object]"`), and rejects through the returned promise instead of throwing synchronously.
|
|
30
|
+
- `kv-kit`: the `KVAdapter` contract gains an optional `clear(prefix?)` and expiring sets (`sadd(key, member, ttl?)`, `sdrain(key)`), implemented by both adapters and `KVManager`. `KVManager.clear()` deletes its namespace's keys; with Redis it uses `SCAN` + `DEL` (within ioredis' `keyPrefix` too) and, without a namespace, refuses to empty the whole database unless `new KVManager({ flushDb: true })`. Expiring sets are sorted sets scored by expiry, updated by one atomic script: cache-kit's tag index. The memory adapter stores and returns copies (`structuredClone`), as Redis does (**breaking** for values that cannot be cloned, such as functions): it returned the stored object itself, so one request's mutation showed up in every other.
|
|
31
|
+
- `cache-kit` (**breaking**): `clear()` runs the adapter's `clear()` with the cache's namespace instead of `disconnect()`/`connect()` of the shared adapter, which on Redis deleted nothing (cached permissions stayed), failed concurrent operations meanwhile, and left the adapter dead when the reconnect failed during a Redis blip. A namespaced cache now clears its own entries (it used to throw); an adapter without `clear()` makes it throw. The tag index is kv-kit's expiring set when the adapter has one (one atomic `sadd` per tagged `set()`; each one read and rewrote the whole index, and the last 10k of 40k tagged sets took 30 s), and otherwise a JSON list that drops expired keys, expires with its last entry and keeps at most 10,000 (the oldest are deleted with their data); indexes written before are still drained by `invalidateTag()`. A value with a `__proto__`/`constructor`/`prototype` key is a miss (deleted when read; not stored by `set()`), so `remember()` refetches instead of every read throwing until the TTL ran out. Data keys and namespaces containing `__cache_tag__:`/`__cache_tags__:` (at the start or after a `:`) are rejected: a caller-chosen key could rewrite a tag index, and `invalidateTag()` deleted whatever it listed.
|
|
32
|
+
- `db-kit` (**breaking**): `MigrationHelper` and the CLI run only the drizzle-kit installed in the project (`node_modules/.bin` of the working directory or a parent), with `bunx --no-install drizzle-kit`, and fail with a `MigrationError` where it is not installed. drizzle-kit is a devDependency, so in a production install `bunx drizzle-kit` downloaded its latest release from npm and ran it with `DATABASE_URL` in its environment.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 840439a: Packages declare the runtime they are tested on: `engines.bun` `>=1.3.0` (the monorepo now builds and tests on Bun 1.3). `create-iskra`, a CLI that also runs under `npm create iskra`, declares `engines.node` `>=18`.
|
|
37
|
+
|
|
38
|
+
Every package is published with an npm provenance attestation (`publishConfig.provenance`), linking each version to the commit and CI run that built it.
|
|
39
|
+
|
|
40
|
+
- d1b8652: The Mailgun adapter requires a sender, like the SMTP, SendGrid and SES adapters: with no `from` on the message or in the config, `send()` throws `From address required` before calling Mailgun. It used to post the message without one, which Mailgun rejects with a 400.
|
|
41
|
+
- 7e89103: Each SendGrid adapter uses its own client: the shared default client meant the last adapter created set the API key for all of them. The `from` display name is quoted or encoded by every adapter, so a name with quotes can no longer add another sender, and malformed addresses are rejected. SMTP and SendGrid forward `headers` (with the Mailgun allowlist) instead of dropping them; SendGrid base64-encodes string attachments. SES rejects messages with attachments or headers instead of silently sending them without.
|
|
42
|
+
- 916b58d: The SES adapter builds one `SESv2Client` on its first send and reuses it, instead of building a new client (and its connection pool) for every message. A failed SDK import is not cached: the next send retries it.
|
|
43
|
+
- Updated dependencies [620da18]
|
|
44
|
+
- Updated dependencies [b635a2c]
|
|
45
|
+
- Updated dependencies [5b2b0fd]
|
|
46
|
+
- Updated dependencies [58d4a8f]
|
|
47
|
+
- Updated dependencies [5c70c5b]
|
|
48
|
+
- Updated dependencies [ec198d4]
|
|
49
|
+
- Updated dependencies [cb3ec43]
|
|
50
|
+
- Updated dependencies [ef2009b]
|
|
51
|
+
- Updated dependencies [840439a]
|
|
52
|
+
- Updated dependencies [dbf8817]
|
|
53
|
+
- Updated dependencies [3dc5581]
|
|
54
|
+
- Updated dependencies [9872d30]
|
|
55
|
+
- Updated dependencies [f2346f5]
|
|
56
|
+
- Updated dependencies [3579944]
|
|
57
|
+
- @iskra-bun/core@0.2.0
|
|
58
|
+
|
|
3
59
|
## 0.1.0
|
|
4
60
|
|
|
5
61
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Cambia `provider` entre `mock`, `smtp`, `sendgrid`, `mailgun` o `ses`; la API (`
|
|
|
30
30
|
|
|
31
31
|
## Documentacion
|
|
32
32
|
|
|
33
|
-
Guia completa: [
|
|
33
|
+
Guia completa: [@iskra-bun/mailer-kit](https://iskra-docs.fly.dev/es/packages/mailer-kit/)
|
|
34
34
|
|
|
35
35
|
## Licencia
|
|
36
36
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALLOWED_HEADERS,
|
|
3
|
+
checkHeaders,
|
|
4
|
+
checkRecipients,
|
|
5
|
+
checkReplyTo,
|
|
6
|
+
formatAddress,
|
|
7
|
+
formatRecipient,
|
|
8
|
+
stripCrlf
|
|
9
|
+
} from "./chunk-URXCRWND.js";
|
|
10
|
+
|
|
1
11
|
// src/providers/mailgun.ts
|
|
2
|
-
var
|
|
3
|
-
"reply-to",
|
|
4
|
-
"in-reply-to",
|
|
5
|
-
"references",
|
|
6
|
-
"list-unsubscribe",
|
|
7
|
-
"list-unsubscribe-post",
|
|
8
|
-
"list-id",
|
|
9
|
-
"x-mailgun-variables",
|
|
10
|
-
"x-mailgun-tag"
|
|
11
|
-
]);
|
|
12
|
-
var stripCrlf = (value) => value.split(/[\r\n]/)[0] ?? "";
|
|
12
|
+
var MAILGUN_HEADERS = [...ALLOWED_HEADERS, "x-mailgun-variables", "x-mailgun-tag"];
|
|
13
13
|
var MailgunEmailAdapter = class {
|
|
14
14
|
apiKey;
|
|
15
15
|
domain;
|
|
@@ -24,26 +24,23 @@ var MailgunEmailAdapter = class {
|
|
|
24
24
|
this.defaultFrom = config.from;
|
|
25
25
|
}
|
|
26
26
|
async send(message) {
|
|
27
|
-
const form = new FormData();
|
|
28
27
|
const from = message.from || this.defaultFrom;
|
|
29
|
-
if (from)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
if (!from) throw new Error("From address required");
|
|
29
|
+
const form = new FormData();
|
|
30
|
+
form.append("from", formatAddress(from));
|
|
31
|
+
const list = (field, label) => checkRecipients(field, label).map(formatRecipient).join(",");
|
|
32
|
+
const cc = list(message.cc, "cc recipient");
|
|
33
|
+
const bcc = list(message.bcc, "bcc recipient");
|
|
34
|
+
const replyTo = checkReplyTo(message.replyTo);
|
|
35
|
+
form.append("to", list(message.to));
|
|
36
|
+
form.append("subject", stripCrlf(message.subject));
|
|
35
37
|
if (message.text) form.append("text", message.text);
|
|
36
38
|
if (message.html) form.append("html", message.html);
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!ALLOWED_HEADERS.has(key.toLowerCase())) {
|
|
43
|
-
throw new Error(`Header "${key}" is not allowed`);
|
|
44
|
-
}
|
|
45
|
-
form.append(`h:${key}`, stripCrlf(value));
|
|
46
|
-
}
|
|
39
|
+
if (cc) form.append("cc", cc);
|
|
40
|
+
if (bcc) form.append("bcc", bcc);
|
|
41
|
+
if (replyTo) form.append("h:Reply-To", formatRecipient(replyTo));
|
|
42
|
+
for (const [key, value] of Object.entries(checkHeaders(message.headers, MAILGUN_HEADERS) ?? {})) {
|
|
43
|
+
form.append(`h:${key}`, value);
|
|
47
44
|
}
|
|
48
45
|
if (message.attachments) {
|
|
49
46
|
for (const att of message.attachments) {
|
|
@@ -75,4 +72,4 @@ var MailgunEmailAdapter = class {
|
|
|
75
72
|
export {
|
|
76
73
|
MailgunEmailAdapter
|
|
77
74
|
};
|
|
78
|
-
//# sourceMappingURL=chunk-
|
|
75
|
+
//# sourceMappingURL=chunk-4OJHFX3J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/mailgun.ts"],"sourcesContent":["import type { EmailAdapter, EmailMessage, EmailConfig, TemplateData } from '../types';\nimport {\n ALLOWED_HEADERS,\n checkHeaders,\n checkRecipients,\n checkReplyTo,\n formatAddress,\n formatRecipient,\n stripCrlf,\n} from '../headers';\n\n/** Mailgun's own headers, on top of the ones every provider allows. */\nconst MAILGUN_HEADERS = [...ALLOWED_HEADERS, 'x-mailgun-variables', 'x-mailgun-tag'];\n\nexport class MailgunEmailAdapter implements EmailAdapter {\n private apiKey: string;\n private domain: string;\n private baseUrl: string;\n private defaultFrom?: { name?: string; email: string };\n\n constructor(config: EmailConfig) {\n if (!config.apiKey) throw new Error('Mailgun requires apiKey');\n if (!config.domain) throw new Error('Mailgun requires domain');\n\n this.apiKey = config.apiKey;\n this.domain = config.domain;\n this.baseUrl = config.baseUrl || 'https://api.mailgun.net/v3';\n this.defaultFrom = config.from;\n }\n\n async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {\n const from = message.from || this.defaultFrom;\n if (!from) throw new Error('From address required');\n\n const form = new FormData();\n form.append('from', formatAddress(from));\n\n // Mailgun parses each field as an address list and builds the headers\n // itself: every value is one checked mailbox, and nothing carries CR/LF.\n const list = (field: EmailMessage['to'] | undefined, label?: string) =>\n checkRecipients(field, label).map(formatRecipient).join(',');\n const cc = list(message.cc, 'cc recipient');\n const bcc = list(message.bcc, 'bcc recipient');\n const replyTo = checkReplyTo(message.replyTo);\n\n form.append('to', list(message.to));\n form.append('subject', stripCrlf(message.subject));\n\n if (message.text) form.append('text', message.text);\n if (message.html) form.append('html', message.html);\n if (cc) form.append('cc', cc);\n if (bcc) form.append('bcc', bcc);\n if (replyTo) form.append('h:Reply-To', formatRecipient(replyTo));\n\n for (const [key, value] of Object.entries(checkHeaders(message.headers, MAILGUN_HEADERS) ?? {})) {\n form.append(`h:${key}`, value);\n }\n\n if (message.attachments) {\n for (const att of message.attachments) {\n const content = typeof att.content === 'string' ? new TextEncoder().encode(att.content) : att.content;\n const bytes = new Uint8Array(content);\n const blob = new Blob([bytes], { type: att.contentType || 'application/octet-stream' });\n form.append('attachment', blob, att.filename);\n }\n }\n\n const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {\n method: 'POST',\n headers: {\n Authorization: 'Basic ' + btoa(`api:${this.apiKey}`),\n },\n body: form,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Mailgun API error (${response.status}): ${errorText}`);\n }\n\n const result = (await response.json()) as { id: string; message: string };\n return { messageId: result.id, success: true };\n }\n\n async sendTemplate(\n _templateName: string,\n _to: string | string[],\n _data: TemplateData,\n ): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder that looks like a real send.\n throw new Error('sendTemplate not supported by mailgun');\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAM,kBAAkB,CAAC,GAAG,iBAAiB,uBAAuB,eAAe;AAE5E,IAAM,sBAAN,MAAkD;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAqB;AAC7B,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAC7D,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAE7D,SAAK,SAAS,OAAO;AACrB,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO,WAAW;AACjC,SAAK,cAAc,OAAO;AAAA,EAC9B;AAAA,EAEA,MAAM,KAAK,SAAyE;AAChF,UAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAElD,UAAM,OAAO,IAAI,SAAS;AAC1B,SAAK,OAAO,QAAQ,cAAc,IAAI,CAAC;AAIvC,UAAM,OAAO,CAAC,OAAuC,UACjD,gBAAgB,OAAO,KAAK,EAAE,IAAI,eAAe,EAAE,KAAK,GAAG;AAC/D,UAAM,KAAK,KAAK,QAAQ,IAAI,cAAc;AAC1C,UAAM,MAAM,KAAK,QAAQ,KAAK,eAAe;AAC7C,UAAM,UAAU,aAAa,QAAQ,OAAO;AAE5C,SAAK,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;AAClC,SAAK,OAAO,WAAW,UAAU,QAAQ,OAAO,CAAC;AAEjD,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ,QAAQ,IAAI;AAClD,QAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ,QAAQ,IAAI;AAClD,QAAI,GAAI,MAAK,OAAO,MAAM,EAAE;AAC5B,QAAI,IAAK,MAAK,OAAO,OAAO,GAAG;AAC/B,QAAI,QAAS,MAAK,OAAO,cAAc,gBAAgB,OAAO,CAAC;AAE/D,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,aAAa,QAAQ,SAAS,eAAe,KAAK,CAAC,CAAC,GAAG;AAC7F,WAAK,OAAO,KAAK,GAAG,IAAI,KAAK;AAAA,IACjC;AAEA,QAAI,QAAQ,aAAa;AACrB,iBAAW,OAAO,QAAQ,aAAa;AACnC,cAAM,UAAU,OAAO,IAAI,YAAY,WAAW,IAAI,YAAY,EAAE,OAAO,IAAI,OAAO,IAAI,IAAI;AAC9F,cAAM,QAAQ,IAAI,WAAW,OAAO;AACpC,cAAM,OAAO,IAAI,KAAK,CAAC,KAAK,GAAG,EAAE,MAAM,IAAI,eAAe,2BAA2B,CAAC;AACtF,aAAK,OAAO,cAAc,MAAM,IAAI,QAAQ;AAAA,MAChD;AAAA,IACJ;AAEA,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,IAAI,KAAK,MAAM,aAAa;AAAA,MACpE,QAAQ;AAAA,MACR,SAAS;AAAA,QACL,eAAe,WAAW,KAAK,OAAO,KAAK,MAAM,EAAE;AAAA,MACvD;AAAA,MACA,MAAM;AAAA,IACV,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AACd,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,YAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,MAAM,SAAS,EAAE;AAAA,IAC1E;AAEA,UAAM,SAAU,MAAM,SAAS,KAAK;AACpC,WAAO,EAAE,WAAW,OAAO,IAAI,SAAS,KAAK;AAAA,EACjD;AAAA,EAEA,MAAM,aACF,eACA,KACA,OACgD;AAGhD,UAAM,IAAI,MAAM,uCAAuC;AAAA,EAC3D;AACJ;","names":[]}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkRecipients,
|
|
3
|
+
checkReplyTo,
|
|
4
|
+
formatAddress,
|
|
5
|
+
formatRecipient
|
|
6
|
+
} from "./chunk-URXCRWND.js";
|
|
7
|
+
|
|
1
8
|
// src/providers/ses.ts
|
|
2
9
|
var SesEmailAdapter = class {
|
|
3
10
|
region;
|
|
4
11
|
defaultFrom;
|
|
5
12
|
injectedClient;
|
|
6
13
|
injectedCommand;
|
|
14
|
+
sdk;
|
|
7
15
|
constructor(config, deps = {}) {
|
|
8
16
|
this.region = config.region;
|
|
9
17
|
this.defaultFrom = config.from;
|
|
10
18
|
this.injectedClient = deps.client;
|
|
11
19
|
this.injectedCommand = deps.sendEmailCommand;
|
|
12
20
|
}
|
|
13
|
-
|
|
21
|
+
resolveSdk() {
|
|
14
22
|
if (this.injectedClient && this.injectedCommand) {
|
|
15
|
-
return { client: this.injectedClient, command: this.injectedCommand };
|
|
23
|
+
return Promise.resolve({ client: this.injectedClient, command: this.injectedCommand });
|
|
16
24
|
}
|
|
25
|
+
return this.sdk ??= this.loadSdk().catch((err) => {
|
|
26
|
+
this.sdk = void 0;
|
|
27
|
+
throw err;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async loadSdk() {
|
|
17
31
|
const sdkModule = "@aws-sdk/client-sesv2";
|
|
18
32
|
const mod = await import(sdkModule);
|
|
19
33
|
const SESv2Client = mod.SESv2Client;
|
|
@@ -25,21 +39,30 @@ var SesEmailAdapter = class {
|
|
|
25
39
|
async send(message) {
|
|
26
40
|
const from = message.from || this.defaultFrom;
|
|
27
41
|
if (!from) throw new Error("From address required");
|
|
42
|
+
if (message.attachments?.length) {
|
|
43
|
+
throw new Error("Attachments are not supported by the ses adapter yet");
|
|
44
|
+
}
|
|
45
|
+
if (message.headers && Object.keys(message.headers).length > 0) {
|
|
46
|
+
throw new Error("Custom headers are not supported by the ses adapter yet");
|
|
47
|
+
}
|
|
48
|
+
const toAddresses = checkRecipients(message.to).map(formatRecipient);
|
|
49
|
+
const cc = checkRecipients(message.cc, "cc recipient").map(formatRecipient);
|
|
50
|
+
const bcc = checkRecipients(message.bcc, "bcc recipient").map(formatRecipient);
|
|
51
|
+
const ccAddresses = cc.length > 0 ? cc : void 0;
|
|
52
|
+
const bccAddresses = bcc.length > 0 ? bcc : void 0;
|
|
53
|
+
const replyTo = checkReplyTo(message.replyTo);
|
|
28
54
|
const { client, command } = await this.resolveSdk();
|
|
29
|
-
const toAddresses = Array.isArray(message.to) ? message.to : [message.to];
|
|
30
|
-
const ccAddresses = message.cc ? Array.isArray(message.cc) ? message.cc : [message.cc] : void 0;
|
|
31
|
-
const bccAddresses = message.bcc ? Array.isArray(message.bcc) ? message.bcc : [message.bcc] : void 0;
|
|
32
55
|
const body = {};
|
|
33
56
|
if (message.text) body.Text = { Data: message.text, Charset: "UTF-8" };
|
|
34
57
|
if (message.html) body.Html = { Data: message.html, Charset: "UTF-8" };
|
|
35
58
|
const input = {
|
|
36
|
-
FromEmailAddress: from
|
|
59
|
+
FromEmailAddress: formatAddress(from),
|
|
37
60
|
Destination: {
|
|
38
61
|
ToAddresses: toAddresses,
|
|
39
62
|
...ccAddresses ? { CcAddresses: ccAddresses } : {},
|
|
40
63
|
...bccAddresses ? { BccAddresses: bccAddresses } : {}
|
|
41
64
|
},
|
|
42
|
-
...
|
|
65
|
+
...replyTo ? { ReplyToAddresses: [formatRecipient(replyTo)] } : {},
|
|
43
66
|
Content: {
|
|
44
67
|
Simple: {
|
|
45
68
|
Subject: { Data: message.subject, Charset: "UTF-8" },
|
|
@@ -58,4 +81,4 @@ var SesEmailAdapter = class {
|
|
|
58
81
|
export {
|
|
59
82
|
SesEmailAdapter
|
|
60
83
|
};
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
84
|
+
//# sourceMappingURL=chunk-IACNO3YI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/ses.ts"],"sourcesContent":["import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from '../types';\nimport { checkRecipients, checkReplyTo, formatAddress, formatRecipient } from '../headers';\n\n/**\n * Minimal structural shape of the value returned by\n * `@aws-sdk/client-sesv2`'s `SendEmailCommand` constructor. We only need\n * an opaque object to hand back to `client.send`, so typing it as an\n * unknown-bearing record keeps `tsc` happy without the SDK installed.\n */\nexport interface SesCommand {\n readonly input: unknown;\n}\n\n/**\n * Structural type for the SESv2 client. Declared locally so this module\n * type-checks even when `@aws-sdk/client-sesv2` is not installed; the real\n * SDK client satisfies this shape at runtime.\n */\nexport interface SesClient {\n send(command: SesCommand): Promise<{ MessageId?: string }>;\n}\n\n/** Factory that builds a `SendEmailCommand` from its input object. */\nexport type SesCommandFactory = (input: unknown) => SesCommand;\n\nexport interface SesAdapterDeps {\n /** Pre-built client (used by tests to avoid importing the real SDK). */\n client?: SesClient;\n /** Pre-built command factory (used by tests to avoid importing the real SDK). */\n sendEmailCommand?: SesCommandFactory;\n}\n\nexport class SesEmailAdapter implements EmailAdapter {\n private region?: string;\n private defaultFrom?: { name?: string; email: string };\n private injectedClient?: SesClient;\n private injectedCommand?: SesCommandFactory;\n private sdk?: Promise<{ client: SesClient; command: SesCommandFactory }>;\n\n constructor(config: EmailConfig, deps: SesAdapterDeps = {}) {\n // The effective `from` address is validated at send time (it can come\n // from the message or the config), so construction stays permissive.\n this.region = config.region;\n this.defaultFrom = config.from;\n this.injectedClient = deps.client;\n this.injectedCommand = deps.sendEmailCommand;\n }\n\n private resolveSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {\n if (this.injectedClient && this.injectedCommand) {\n return Promise.resolve({ client: this.injectedClient, command: this.injectedCommand });\n }\n // One client per adapter, built on the first send. A failed import is\n // not cached, so the next send retries it.\n return (this.sdk ??= this.loadSdk().catch((err: unknown) => {\n this.sdk = undefined;\n throw err;\n }));\n }\n\n private async loadSdk(): Promise<{ client: SesClient; command: SesCommandFactory }> {\n // Lazy load only when actually sending so `tsc`/`bun test` stay green\n // without `@aws-sdk/client-sesv2` installed. The specifier is held in a\n // variable so TypeScript does not try to resolve the module at compile\n // time (it is a real runtime dependency, declared in package.json).\n const sdkModule = '@aws-sdk/client-sesv2';\n const mod = (await import(sdkModule)) as Record<string, unknown>;\n const SESv2Client = mod.SESv2Client as new (cfg: { region?: string }) => SesClient;\n const SendEmailCommand = mod.SendEmailCommand as new (input: unknown) => SesCommand;\n\n const client = this.injectedClient ?? new SESv2Client({ region: this.region });\n const command: SesCommandFactory = this.injectedCommand ?? ((input) => new SendEmailCommand(input));\n\n return { client, command };\n }\n\n async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {\n const from = message.from || this.defaultFrom;\n if (!from) throw new Error('From address required');\n // Simple content has no place for them: they used to be dropped while\n // the send reported success.\n if (message.attachments?.length) {\n throw new Error('Attachments are not supported by the ses adapter yet');\n }\n if (message.headers && Object.keys(message.headers).length > 0) {\n throw new Error('Custom headers are not supported by the ses adapter yet');\n }\n\n // SES parses each entry as an address list: one mailbox per entry.\n const toAddresses = checkRecipients(message.to).map(formatRecipient);\n const cc = checkRecipients(message.cc, 'cc recipient').map(formatRecipient);\n const bcc = checkRecipients(message.bcc, 'bcc recipient').map(formatRecipient);\n const ccAddresses = cc.length > 0 ? cc : undefined;\n const bccAddresses = bcc.length > 0 ? bcc : undefined;\n const replyTo = checkReplyTo(message.replyTo);\n\n const { client, command } = await this.resolveSdk();\n\n const body: Record<string, { Data: string; Charset: string }> = {};\n if (message.text) body.Text = { Data: message.text, Charset: 'UTF-8' };\n if (message.html) body.Html = { Data: message.html, Charset: 'UTF-8' };\n\n const input = {\n FromEmailAddress: formatAddress(from),\n Destination: {\n ToAddresses: toAddresses,\n ...(ccAddresses ? { CcAddresses: ccAddresses } : {}),\n ...(bccAddresses ? { BccAddresses: bccAddresses } : {}),\n },\n ...(replyTo ? { ReplyToAddresses: [formatRecipient(replyTo)] } : {}),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: 'UTF-8' },\n Body: body,\n },\n },\n };\n\n const result = await client.send(command(input));\n return { messageId: result.MessageId ?? '', success: true };\n }\n\n async sendTemplate(\n _templateName: string,\n _to: string | string[],\n _data: TemplateData,\n ): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error('sendTemplate not supported by ses');\n }\n}\n"],"mappings":";;;;;;;;AAgCO,IAAM,kBAAN,MAA8C;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAqB,OAAuB,CAAC,GAAG;AAGxD,SAAK,SAAS,OAAO;AACrB,SAAK,cAAc,OAAO;AAC1B,SAAK,iBAAiB,KAAK;AAC3B,SAAK,kBAAkB,KAAK;AAAA,EAChC;AAAA,EAEQ,aAAyE;AAC7E,QAAI,KAAK,kBAAkB,KAAK,iBAAiB;AAC7C,aAAO,QAAQ,QAAQ,EAAE,QAAQ,KAAK,gBAAgB,SAAS,KAAK,gBAAgB,CAAC;AAAA,IACzF;AAGA,WAAQ,KAAK,QAAQ,KAAK,QAAQ,EAAE,MAAM,CAAC,QAAiB;AACxD,WAAK,MAAM;AACX,YAAM;AAAA,IACV,CAAC;AAAA,EACL;AAAA,EAEA,MAAc,UAAsE;AAKhF,UAAM,YAAY;AAClB,UAAM,MAAO,MAAM,OAAO;AAC1B,UAAM,cAAc,IAAI;AACxB,UAAM,mBAAmB,IAAI;AAE7B,UAAM,SAAS,KAAK,kBAAkB,IAAI,YAAY,EAAE,QAAQ,KAAK,OAAO,CAAC;AAC7E,UAAM,UAA6B,KAAK,oBAAoB,CAAC,UAAU,IAAI,iBAAiB,KAAK;AAEjG,WAAO,EAAE,QAAQ,QAAQ;AAAA,EAC7B;AAAA,EAEA,MAAM,KAAK,SAAyE;AAChF,UAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAGlD,QAAI,QAAQ,aAAa,QAAQ;AAC7B,YAAM,IAAI,MAAM,sDAAsD;AAAA,IAC1E;AACA,QAAI,QAAQ,WAAW,OAAO,KAAK,QAAQ,OAAO,EAAE,SAAS,GAAG;AAC5D,YAAM,IAAI,MAAM,yDAAyD;AAAA,IAC7E;AAGA,UAAM,cAAc,gBAAgB,QAAQ,EAAE,EAAE,IAAI,eAAe;AACnE,UAAM,KAAK,gBAAgB,QAAQ,IAAI,cAAc,EAAE,IAAI,eAAe;AAC1E,UAAM,MAAM,gBAAgB,QAAQ,KAAK,eAAe,EAAE,IAAI,eAAe;AAC7E,UAAM,cAAc,GAAG,SAAS,IAAI,KAAK;AACzC,UAAM,eAAe,IAAI,SAAS,IAAI,MAAM;AAC5C,UAAM,UAAU,aAAa,QAAQ,OAAO;AAE5C,UAAM,EAAE,QAAQ,QAAQ,IAAI,MAAM,KAAK,WAAW;AAElD,UAAM,OAA0D,CAAC;AACjE,QAAI,QAAQ,KAAM,MAAK,OAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,QAAQ;AACrE,QAAI,QAAQ,KAAM,MAAK,OAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,QAAQ;AAErE,UAAM,QAAQ;AAAA,MACV,kBAAkB,cAAc,IAAI;AAAA,MACpC,aAAa;AAAA,QACT,aAAa;AAAA,QACb,GAAI,cAAc,EAAE,aAAa,YAAY,IAAI,CAAC;AAAA,QAClD,GAAI,eAAe,EAAE,cAAc,aAAa,IAAI,CAAC;AAAA,MACzD;AAAA,MACA,GAAI,UAAU,EAAE,kBAAkB,CAAC,gBAAgB,OAAO,CAAC,EAAE,IAAI,CAAC;AAAA,MAClE,SAAS;AAAA,QACL,QAAQ;AAAA,UACJ,SAAS,EAAE,MAAM,QAAQ,SAAS,SAAS,QAAQ;AAAA,UACnD,MAAM;AAAA,QACV;AAAA,MACJ;AAAA,IACJ;AAEA,UAAM,SAAS,MAAM,OAAO,KAAK,QAAQ,KAAK,CAAC;AAC/C,WAAO,EAAE,WAAW,OAAO,aAAa,IAAI,SAAS,KAAK;AAAA,EAC9D;AAAA,EAEA,MAAM,aACF,eACA,KACA,OACgD;AAGhD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACvD;AACJ;","names":[]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkEmail,
|
|
3
|
+
checkHeaders,
|
|
4
|
+
checkRecipients,
|
|
5
|
+
checkReplyTo,
|
|
6
|
+
cleanName
|
|
7
|
+
} from "./chunk-URXCRWND.js";
|
|
8
|
+
|
|
9
|
+
// src/providers/sendgrid.ts
|
|
10
|
+
import { MailService } from "@sendgrid/mail";
|
|
11
|
+
var toSendGrid = ({ name, address }) => name ? { email: address, name } : address;
|
|
12
|
+
var SendGridEmailAdapter = class {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
if (!config.apiKey) throw new Error("SendGrid API Key required");
|
|
16
|
+
this.client.setApiKey(config.apiKey);
|
|
17
|
+
}
|
|
18
|
+
config;
|
|
19
|
+
/**
|
|
20
|
+
* A client per adapter: the package's default export is a process-wide
|
|
21
|
+
* singleton, so the last adapter created set the API key for all of them
|
|
22
|
+
* (one tenant's mail sent with another tenant's account).
|
|
23
|
+
*/
|
|
24
|
+
client = new MailService();
|
|
25
|
+
async send(message) {
|
|
26
|
+
const from = message.from || this.config.from;
|
|
27
|
+
if (!from) throw new Error("From address required");
|
|
28
|
+
const cc = checkRecipients(message.cc, "cc recipient").map(toSendGrid);
|
|
29
|
+
const bcc = checkRecipients(message.bcc, "bcc recipient").map(toSendGrid);
|
|
30
|
+
const replyTo = checkReplyTo(message.replyTo);
|
|
31
|
+
const msg = {
|
|
32
|
+
to: checkRecipients(message.to).map(toSendGrid),
|
|
33
|
+
from: from.name ? { email: checkEmail(from.email), name: cleanName(from.name) } : checkEmail(from.email),
|
|
34
|
+
subject: message.subject,
|
|
35
|
+
text: message.text,
|
|
36
|
+
html: message.html,
|
|
37
|
+
cc: cc.length > 0 ? cc : void 0,
|
|
38
|
+
bcc: bcc.length > 0 ? bcc : void 0,
|
|
39
|
+
replyTo: replyTo && toSendGrid(replyTo),
|
|
40
|
+
attachments: message.attachments?.map((a) => ({
|
|
41
|
+
filename: a.filename,
|
|
42
|
+
// SendGrid takes base64; a string is text, as with the other
|
|
43
|
+
// providers (it was sent as is and arrived corrupted).
|
|
44
|
+
content: Buffer.from(
|
|
45
|
+
typeof a.content === "string" ? Buffer.from(a.content, "utf8") : a.content
|
|
46
|
+
).toString("base64"),
|
|
47
|
+
type: a.contentType,
|
|
48
|
+
disposition: "attachment"
|
|
49
|
+
})),
|
|
50
|
+
headers: checkHeaders(message.headers)
|
|
51
|
+
// SendGrid's type wants text or html statically present; here both
|
|
52
|
+
// are optional, and SendGrid's API rejects a mail with neither.
|
|
53
|
+
};
|
|
54
|
+
const [response] = await this.client.send(msg);
|
|
55
|
+
return { messageId: response.headers["x-message-id"], success: true };
|
|
56
|
+
}
|
|
57
|
+
async sendTemplate(_templateName, _to, _data) {
|
|
58
|
+
throw new Error("sendTemplate not supported by sendgrid");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
SendGridEmailAdapter
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=chunk-U4ZHXPGW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/sendgrid.ts"],"sourcesContent":["import type { EmailAdapter, EmailAddress, EmailConfig, EmailMessage, TemplateData } from '../types';\nimport { MailService } from '@sendgrid/mail';\nimport { checkEmail, checkHeaders, checkRecipients, checkReplyTo, cleanName } from '../headers';\n\n/** SendGrid's form of a checked recipient (it parses a string as `Name <address>`). */\nconst toSendGrid = ({ name, address }: EmailAddress) => (name ? { email: address, name } : address);\n\n/** The mail object MailService.send() takes (typed by @sendgrid/helpers, not a direct dependency). */\ntype SendGridMail = Extract<Parameters<MailService['send']>[0], { from: unknown }>;\n\nexport class SendGridEmailAdapter implements EmailAdapter {\n /**\n * A client per adapter: the package's default export is a process-wide\n * singleton, so the last adapter created set the API key for all of them\n * (one tenant's mail sent with another tenant's account).\n */\n private readonly client = new MailService();\n\n constructor(private config: EmailConfig) {\n if (!config.apiKey) throw new Error('SendGrid API Key required');\n this.client.setApiKey(config.apiKey);\n }\n\n async send(message: EmailMessage) {\n const from = message.from || this.config.from;\n if (!from) throw new Error('From address required');\n const cc = checkRecipients(message.cc, 'cc recipient').map(toSendGrid);\n const bcc = checkRecipients(message.bcc, 'bcc recipient').map(toSendGrid);\n const replyTo = checkReplyTo(message.replyTo);\n\n const msg = {\n to: checkRecipients(message.to).map(toSendGrid),\n from: from.name ? { email: checkEmail(from.email), name: cleanName(from.name) } : checkEmail(from.email),\n subject: message.subject,\n text: message.text,\n html: message.html,\n cc: cc.length > 0 ? cc : undefined,\n bcc: bcc.length > 0 ? bcc : undefined,\n replyTo: replyTo && toSendGrid(replyTo),\n attachments: message.attachments?.map((a) => ({\n filename: a.filename,\n // SendGrid takes base64; a string is text, as with the other\n // providers (it was sent as is and arrived corrupted).\n content: Buffer.from(\n typeof a.content === 'string' ? Buffer.from(a.content, 'utf8') : a.content,\n ).toString('base64'),\n type: a.contentType,\n disposition: 'attachment',\n })),\n headers: checkHeaders(message.headers),\n // SendGrid's type wants text or html statically present; here both\n // are optional, and SendGrid's API rejects a mail with neither.\n } as SendGridMail;\n\n const [response] = await this.client.send(msg);\n return { messageId: response.headers['x-message-id'] as string, success: true };\n }\n\n async sendTemplate(\n _templateName: string,\n _to: string | string[],\n _data: TemplateData,\n ): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error('sendTemplate not supported by sendgrid');\n }\n}\n"],"mappings":";;;;;;;;;AACA,SAAS,mBAAmB;AAI5B,IAAM,aAAa,CAAC,EAAE,MAAM,QAAQ,MAAqB,OAAO,EAAE,OAAO,SAAS,KAAK,IAAI;AAKpF,IAAM,uBAAN,MAAmD;AAAA,EAQtD,YAAoB,QAAqB;AAArB;AAChB,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AAC/D,SAAK,OAAO,UAAU,OAAO,MAAM;AAAA,EACvC;AAAA,EAHoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAFH,SAAS,IAAI,YAAY;AAAA,EAO1C,MAAM,KAAK,SAAuB;AAC9B,UAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;AACzC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAClD,UAAM,KAAK,gBAAgB,QAAQ,IAAI,cAAc,EAAE,IAAI,UAAU;AACrE,UAAM,MAAM,gBAAgB,QAAQ,KAAK,eAAe,EAAE,IAAI,UAAU;AACxE,UAAM,UAAU,aAAa,QAAQ,OAAO;AAE5C,UAAM,MAAM;AAAA,MACR,IAAI,gBAAgB,QAAQ,EAAE,EAAE,IAAI,UAAU;AAAA,MAC9C,MAAM,KAAK,OAAO,EAAE,OAAO,WAAW,KAAK,KAAK,GAAG,MAAM,UAAU,KAAK,IAAI,EAAE,IAAI,WAAW,KAAK,KAAK;AAAA,MACvG,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,MACd,IAAI,GAAG,SAAS,IAAI,KAAK;AAAA,MACzB,KAAK,IAAI,SAAS,IAAI,MAAM;AAAA,MAC5B,SAAS,WAAW,WAAW,OAAO;AAAA,MACtC,aAAa,QAAQ,aAAa,IAAI,CAAC,OAAO;AAAA,QAC1C,UAAU,EAAE;AAAA;AAAA;AAAA,QAGZ,SAAS,OAAO;AAAA,UACZ,OAAO,EAAE,YAAY,WAAW,OAAO,KAAK,EAAE,SAAS,MAAM,IAAI,EAAE;AAAA,QACvE,EAAE,SAAS,QAAQ;AAAA,QACnB,MAAM,EAAE;AAAA,QACR,aAAa;AAAA,MACjB,EAAE;AAAA,MACF,SAAS,aAAa,QAAQ,OAAO;AAAA;AAAA;AAAA,IAGzC;AAEA,UAAM,CAAC,QAAQ,IAAI,MAAM,KAAK,OAAO,KAAK,GAAG;AAC7C,WAAO,EAAE,WAAW,SAAS,QAAQ,cAAc,GAAa,SAAS,KAAK;AAAA,EAClF;AAAA,EAEA,MAAM,aACF,eACA,KACA,OACgD;AAGhD,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC5D;AACJ;","names":[]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/headers.ts
|
|
2
|
+
var ALLOWED_HEADERS = [
|
|
3
|
+
"in-reply-to",
|
|
4
|
+
"references",
|
|
5
|
+
"list-unsubscribe",
|
|
6
|
+
"list-unsubscribe-post",
|
|
7
|
+
"list-id"
|
|
8
|
+
];
|
|
9
|
+
var stripCrlf = (value) => value.split(/[\r\n]/)[0] ?? "";
|
|
10
|
+
function checkHeaders(headers, allowed = ALLOWED_HEADERS) {
|
|
11
|
+
if (!headers) return void 0;
|
|
12
|
+
const checked = {};
|
|
13
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
14
|
+
if (key.toLowerCase() === "reply-to") {
|
|
15
|
+
throw new Error(`Header "${key}" is not allowed: use message.replyTo`);
|
|
16
|
+
}
|
|
17
|
+
if (!allowed.includes(key.toLowerCase())) {
|
|
18
|
+
throw new Error(`Header "${key}" is not allowed`);
|
|
19
|
+
}
|
|
20
|
+
checked[key] = stripCrlf(value);
|
|
21
|
+
}
|
|
22
|
+
return checked;
|
|
23
|
+
}
|
|
24
|
+
var ADDRESS_SPECIALS = /[\s\p{Cc}<>()[\]\\,;:"]/u;
|
|
25
|
+
function checkEmail(email) {
|
|
26
|
+
const trimmed = typeof email === "string" ? email.trim() : "";
|
|
27
|
+
const at = trimmed.indexOf("@");
|
|
28
|
+
if (at < 1 || at !== trimmed.lastIndexOf("@") || at === trimmed.length - 1 || ADDRESS_SPECIALS.test(trimmed)) {
|
|
29
|
+
throw new Error(`Invalid email address: ${JSON.stringify(email)}`);
|
|
30
|
+
}
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
var cleanName = (name) => name.replace(/[\r\n]+/g, " ").trim();
|
|
34
|
+
function checkRecipients(value, field = "recipient") {
|
|
35
|
+
if (value === void 0 || value === null) return [];
|
|
36
|
+
return (Array.isArray(value) ? value : [value]).map((recipient) => {
|
|
37
|
+
if (typeof recipient === "string") return { address: checkEmail(recipient) };
|
|
38
|
+
if (typeof recipient !== "object" || recipient === null || typeof recipient.address !== "string") {
|
|
39
|
+
throw new Error(`Invalid ${field}: expected an email address or { name, address }`);
|
|
40
|
+
}
|
|
41
|
+
const { name, address } = recipient;
|
|
42
|
+
if (name !== void 0 && (typeof name !== "string" || new RegExp("\\p{Cc}", "u").test(name))) {
|
|
43
|
+
throw new Error(`Invalid ${field} name: control characters (CR/LF) are not allowed`);
|
|
44
|
+
}
|
|
45
|
+
return name ? { name, address: checkEmail(address) } : { address: checkEmail(address) };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function checkReplyTo(value) {
|
|
49
|
+
const list = checkRecipients(value, "replyTo");
|
|
50
|
+
if (list.length > 1) throw new Error("Invalid replyTo: expected a single address");
|
|
51
|
+
return list[0];
|
|
52
|
+
}
|
|
53
|
+
var formatRecipient = ({ name, address }) => formatAddress({ name, email: address });
|
|
54
|
+
function formatAddress(from) {
|
|
55
|
+
const email = checkEmail(from.email);
|
|
56
|
+
const name = from.name ? cleanName(from.name) : "";
|
|
57
|
+
if (!name) return email;
|
|
58
|
+
if (/^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~ ]+$/.test(name)) return `${name} <${email}>`;
|
|
59
|
+
if (/^[\x20-\x7e]*$/.test(name)) return `"${name.replace(/(["\\])/g, "\\$1")}" <${email}>`;
|
|
60
|
+
return `=?UTF-8?B?${Buffer.from(name, "utf8").toString("base64")}?= <${email}>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
ALLOWED_HEADERS,
|
|
65
|
+
stripCrlf,
|
|
66
|
+
checkHeaders,
|
|
67
|
+
checkEmail,
|
|
68
|
+
cleanName,
|
|
69
|
+
checkRecipients,
|
|
70
|
+
checkReplyTo,
|
|
71
|
+
formatRecipient,
|
|
72
|
+
formatAddress
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=chunk-URXCRWND.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/headers.ts"],"sourcesContent":["import type { EmailAddress, EmailRecipient } from './types';\n\n/**\n * Outbound custom mail headers callers may set on every provider. Anything\n * else is rejected so a caller cannot spoof Sender / routing headers via the\n * generic `headers` map. Reply-To is not one: `message.replyTo` sets it, with\n * its address checked like a recipient's.\n */\nexport const ALLOWED_HEADERS = [\n 'in-reply-to',\n 'references',\n 'list-unsubscribe',\n 'list-unsubscribe-post',\n 'list-id',\n] as const;\n\n/**\n * Truncate a header value at the first CR/LF. Anything after a line break is an\n * injected header (or folded continuation) and must be dropped, not preserved.\n */\nexport const stripCrlf = (value: string): string => value.split(/[\\r\\n]/)[0] ?? '';\n\n/** The `headers` of a message, checked against `allowed` and without CR/LF. */\nexport function checkHeaders(\n headers: Record<string, string> | undefined,\n allowed: readonly string[] = ALLOWED_HEADERS,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const checked: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === 'reply-to') {\n throw new Error(`Header \"${key}\" is not allowed: use message.replyTo`);\n }\n if (!allowed.includes(key.toLowerCase())) {\n throw new Error(`Header \"${key}\" is not allowed`);\n }\n checked[key] = stripCrlf(value);\n }\n return checked;\n}\n\n/**\n * What an address may not contain unquoted: whitespace and control characters,\n * and what starts a display name, comment, group or list (`<>()[]\\,;:\"`).\n * `a@evil.test:b@x.com` is a group that mails b@x.com only.\n */\nconst ADDRESS_SPECIALS = /[\\s\\p{Cc}<>()[\\]\\\\,;:\"]/u;\n\n/** A bare addr-spec: one `@`, no display name, brackets, separators, quotes or whitespace. */\nexport function checkEmail(email: string): string {\n const trimmed = typeof email === 'string' ? email.trim() : '';\n const at = trimmed.indexOf('@');\n if (at < 1 || at !== trimmed.lastIndexOf('@') || at === trimmed.length - 1 || ADDRESS_SPECIALS.test(trimmed)) {\n throw new Error(`Invalid email address: ${JSON.stringify(email)}`);\n }\n return trimmed;\n}\n\n/** A display name on one line (CR/LF would start a new header). */\nexport const cleanName = (name: string): string => name.replace(/[\\r\\n]+/g, ' ').trim();\n\n/**\n * The recipients of a `to`/`cc`/`bcc`/`replyTo` field, one mailbox each. A\n * string is a single bare address: one value such as\n * `\"bob@x.com <spy@evil.test>, y@x.com\"` or `\"list: spy@evil.test;\"` mailed\n * other people than the ones an allowlist checked. An object's address is\n * checked the same way and its name may not hold control characters.\n */\nexport function checkRecipients(\n value: EmailRecipient | EmailRecipient[] | undefined,\n field = 'recipient',\n): EmailAddress[] {\n // null too: an optional field often arrives as null from JSON.\n if (value === undefined || value === null) return [];\n return (Array.isArray(value) ? value : [value]).map((recipient) => {\n if (typeof recipient === 'string') return { address: checkEmail(recipient) };\n if (typeof recipient !== 'object' || recipient === null || typeof recipient.address !== 'string') {\n throw new Error(`Invalid ${field}: expected an email address or { name, address }`);\n }\n const { name, address } = recipient;\n if (name !== undefined && (typeof name !== 'string' || /\\p{Cc}/u.test(name))) {\n throw new Error(`Invalid ${field} name: control characters (CR/LF) are not allowed`);\n }\n return name ? { name, address: checkEmail(address) } : { address: checkEmail(address) };\n });\n}\n\n/** `replyTo`, checked like the recipients: one address at most. */\nexport function checkReplyTo(value: EmailRecipient | undefined): EmailAddress | undefined {\n const list = checkRecipients(value, 'replyTo');\n if (list.length > 1) throw new Error('Invalid replyTo: expected a single address');\n return list[0];\n}\n\n/** A checked recipient as an RFC 5322 mailbox (see {@link formatAddress}). */\nexport const formatRecipient = ({ name, address }: EmailAddress): string => formatAddress({ name, email: address });\n\n/**\n * `from` as an RFC 5322 mailbox. A display name with special characters is\n * quoted (with `\"` and `\\` escaped), or RFC 2047-encoded when it is not\n * ASCII. Interpolated as is, a name such as `Ana\" <ceo@bank.com>, \"x` added a\n * sender of the caller's choice.\n */\nexport function formatAddress(from: { name?: string; email: string }): string {\n const email = checkEmail(from.email);\n const name = from.name ? cleanName(from.name) : '';\n if (!name) return email;\n // Words of RFC 5322 atext need no quoting.\n if (/^[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~ ]+$/.test(name)) return `${name} <${email}>`;\n if (/^[\\x20-\\x7e]*$/.test(name)) return `\"${name.replace(/([\"\\\\])/g, '\\\\$1')}\" <${email}>`;\n return `=?UTF-8?B?${Buffer.from(name, 'utf8').toString('base64')}?= <${email}>`;\n}\n"],"mappings":";AAQO,IAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAMO,IAAM,YAAY,CAAC,UAA0B,MAAM,MAAM,QAAQ,EAAE,CAAC,KAAK;AAGzE,SAAS,aACZ,SACA,UAA6B,iBACK;AAClC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,UAAkC,CAAC;AACzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAChD,QAAI,IAAI,YAAY,MAAM,YAAY;AAClC,YAAM,IAAI,MAAM,WAAW,GAAG,uCAAuC;AAAA,IACzE;AACA,QAAI,CAAC,QAAQ,SAAS,IAAI,YAAY,CAAC,GAAG;AACtC,YAAM,IAAI,MAAM,WAAW,GAAG,kBAAkB;AAAA,IACpD;AACA,YAAQ,GAAG,IAAI,UAAU,KAAK;AAAA,EAClC;AACA,SAAO;AACX;AAOA,IAAM,mBAAmB;AAGlB,SAAS,WAAW,OAAuB;AAC9C,QAAM,UAAU,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AAC3D,QAAM,KAAK,QAAQ,QAAQ,GAAG;AAC9B,MAAI,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG,KAAK,OAAO,QAAQ,SAAS,KAAK,iBAAiB,KAAK,OAAO,GAAG;AAC1G,UAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EACrE;AACA,SAAO;AACX;AAGO,IAAM,YAAY,CAAC,SAAyB,KAAK,QAAQ,YAAY,GAAG,EAAE,KAAK;AAS/E,SAAS,gBACZ,OACA,QAAQ,aACM;AAEd,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO,CAAC;AACnD,UAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,cAAc;AAC/D,QAAI,OAAO,cAAc,SAAU,QAAO,EAAE,SAAS,WAAW,SAAS,EAAE;AAC3E,QAAI,OAAO,cAAc,YAAY,cAAc,QAAQ,OAAO,UAAU,YAAY,UAAU;AAC9F,YAAM,IAAI,MAAM,WAAW,KAAK,kDAAkD;AAAA,IACtF;AACA,UAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAI,SAAS,WAAc,OAAO,SAAS,YAAY,WAAC,WAAO,GAAC,EAAC,KAAK,IAAI,IAAI;AAC1E,YAAM,IAAI,MAAM,WAAW,KAAK,mDAAmD;AAAA,IACvF;AACA,WAAO,OAAO,EAAE,MAAM,SAAS,WAAW,OAAO,EAAE,IAAI,EAAE,SAAS,WAAW,OAAO,EAAE;AAAA,EAC1F,CAAC;AACL;AAGO,SAAS,aAAa,OAA6D;AACtF,QAAM,OAAO,gBAAgB,OAAO,SAAS;AAC7C,MAAI,KAAK,SAAS,EAAG,OAAM,IAAI,MAAM,4CAA4C;AACjF,SAAO,KAAK,CAAC;AACjB;AAGO,IAAM,kBAAkB,CAAC,EAAE,MAAM,QAAQ,MAA4B,cAAc,EAAE,MAAM,OAAO,QAAQ,CAAC;AAQ3G,SAAS,cAAc,MAAgD;AAC1E,QAAM,QAAQ,WAAW,KAAK,KAAK;AACnC,QAAM,OAAO,KAAK,OAAO,UAAU,KAAK,IAAI,IAAI;AAChD,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,sCAAsC,KAAK,IAAI,EAAG,QAAO,GAAG,IAAI,KAAK,KAAK;AAC9E,MAAI,iBAAiB,KAAK,IAAI,EAAG,QAAO,IAAI,KAAK,QAAQ,YAAY,MAAM,CAAC,MAAM,KAAK;AACvF,SAAO,aAAa,OAAO,KAAK,MAAM,MAAM,EAAE,SAAS,QAAQ,CAAC,OAAO,KAAK;AAChF;","names":[]}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkEmail,
|
|
3
|
+
checkHeaders,
|
|
4
|
+
checkRecipients,
|
|
5
|
+
checkReplyTo,
|
|
6
|
+
cleanName
|
|
7
|
+
} from "./chunk-URXCRWND.js";
|
|
8
|
+
|
|
1
9
|
// src/providers/smtp.ts
|
|
2
10
|
import * as nodemailer from "nodemailer";
|
|
11
|
+
var toMailbox = ({ name, address }) => name ? { name, address } : address;
|
|
3
12
|
var SmtpEmailAdapter = class {
|
|
4
13
|
constructor(config) {
|
|
5
14
|
this.config = config;
|
|
@@ -22,20 +31,28 @@ var SmtpEmailAdapter = class {
|
|
|
22
31
|
async send(message) {
|
|
23
32
|
const from = message.from || this.config.from;
|
|
24
33
|
if (!from) throw new Error("From address required");
|
|
34
|
+
const cc = checkRecipients(message.cc, "cc recipient").map(toMailbox);
|
|
35
|
+
const bcc = checkRecipients(message.bcc, "bcc recipient").map(toMailbox);
|
|
36
|
+
const replyTo = checkReplyTo(message.replyTo);
|
|
25
37
|
const info = await this.transporter.sendMail({
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
// An address object: nodemailer quotes or encodes the name, which
|
|
39
|
+
// interpolated into `"name" <email>` could add another sender.
|
|
40
|
+
from: { name: from.name ? cleanName(from.name) : "", address: checkEmail(from.email) },
|
|
41
|
+
to: checkRecipients(message.to).map(toMailbox),
|
|
28
42
|
subject: message.subject,
|
|
29
43
|
text: message.text,
|
|
30
44
|
html: message.html,
|
|
31
|
-
cc:
|
|
32
|
-
bcc:
|
|
33
|
-
replyTo:
|
|
45
|
+
cc: cc.length > 0 ? cc : void 0,
|
|
46
|
+
bcc: bcc.length > 0 ? bcc : void 0,
|
|
47
|
+
replyTo: replyTo && toMailbox(replyTo),
|
|
34
48
|
attachments: message.attachments?.map((a) => ({
|
|
35
49
|
filename: a.filename,
|
|
36
50
|
content: typeof a.content === "string" ? a.content : Buffer.from(a.content),
|
|
37
51
|
contentType: a.contentType
|
|
38
|
-
}))
|
|
52
|
+
})),
|
|
53
|
+
// Forwarded with the same allowlist as the other providers (it
|
|
54
|
+
// used to be dropped).
|
|
55
|
+
headers: checkHeaders(message.headers)
|
|
39
56
|
});
|
|
40
57
|
return { messageId: info.messageId, success: true };
|
|
41
58
|
}
|
|
@@ -47,4 +64,4 @@ var SmtpEmailAdapter = class {
|
|
|
47
64
|
export {
|
|
48
65
|
SmtpEmailAdapter
|
|
49
66
|
};
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
67
|
+
//# sourceMappingURL=chunk-VRF3QYGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/smtp.ts"],"sourcesContent":["import type { EmailAdapter, EmailAddress, EmailConfig, EmailMessage, TemplateData } from '../types';\nimport * as nodemailer from 'nodemailer';\nimport { checkEmail, checkHeaders, checkRecipients, checkReplyTo, cleanName } from '../headers';\n\n/** nodemailer's form of a checked recipient: it quotes or encodes the name itself. */\nconst toMailbox = ({ name, address }: EmailAddress) => (name ? { name, address } : address);\n\nexport class SmtpEmailAdapter implements EmailAdapter {\n private transporter: nodemailer.Transporter;\n\n constructor(private config: EmailConfig) {\n if (!config.smtp) throw new Error('SMTP config required');\n // Implicit TLS on port 465; STARTTLS enforced (requireTLS) elsewhere so\n // credentials never transit in cleartext. An explicit `secure` wins.\n const secure = config.smtp.secure ?? config.smtp.port === 465;\n this.transporter = nodemailer.createTransport({\n host: config.smtp.host,\n port: config.smtp.port,\n secure,\n requireTLS: secure ? undefined : true,\n tls: { rejectUnauthorized: true },\n auth: {\n user: config.smtp.username,\n pass: config.smtp.password,\n },\n });\n }\n\n async send(message: EmailMessage) {\n const from = message.from || this.config.from;\n if (!from) throw new Error('From address required');\n // Joined into one string, nodemailer parsed each value as an address\n // list: one entry could add recipients or a group.\n const cc = checkRecipients(message.cc, 'cc recipient').map(toMailbox);\n const bcc = checkRecipients(message.bcc, 'bcc recipient').map(toMailbox);\n const replyTo = checkReplyTo(message.replyTo);\n\n const info = await this.transporter.sendMail({\n // An address object: nodemailer quotes or encodes the name, which\n // interpolated into `\"name\" <email>` could add another sender.\n from: { name: from.name ? cleanName(from.name) : '', address: checkEmail(from.email) },\n to: checkRecipients(message.to).map(toMailbox),\n subject: message.subject,\n text: message.text,\n html: message.html,\n cc: cc.length > 0 ? cc : undefined,\n bcc: bcc.length > 0 ? bcc : undefined,\n replyTo: replyTo && toMailbox(replyTo),\n attachments: message.attachments?.map((a) => ({\n filename: a.filename,\n content: typeof a.content === 'string' ? a.content : Buffer.from(a.content),\n contentType: a.contentType,\n })),\n // Forwarded with the same allowlist as the other providers (it\n // used to be dropped).\n headers: checkHeaders(message.headers),\n });\n\n return { messageId: info.messageId, success: true };\n }\n\n async sendTemplate(\n _templateName: string,\n _to: string | string[],\n _data: TemplateData,\n ): Promise<{ messageId: string; success: boolean }> {\n // No template engine is implemented yet; fail loudly rather than\n // silently sending a placeholder body that looks like a real send.\n throw new Error('sendTemplate not supported by smtp');\n }\n}\n"],"mappings":";;;;;;;;;AACA,YAAY,gBAAgB;AAI5B,IAAM,YAAY,CAAC,EAAE,MAAM,QAAQ,MAAqB,OAAO,EAAE,MAAM,QAAQ,IAAI;AAE5E,IAAM,mBAAN,MAA+C;AAAA,EAGlD,YAAoB,QAAqB;AAArB;AAChB,QAAI,CAAC,OAAO,KAAM,OAAM,IAAI,MAAM,sBAAsB;AAGxD,UAAM,SAAS,OAAO,KAAK,UAAU,OAAO,KAAK,SAAS;AAC1D,SAAK,cAAyB,2BAAgB;AAAA,MAC1C,MAAM,OAAO,KAAK;AAAA,MAClB,MAAM,OAAO,KAAK;AAAA,MAClB;AAAA,MACA,YAAY,SAAS,SAAY;AAAA,MACjC,KAAK,EAAE,oBAAoB,KAAK;AAAA,MAChC,MAAM;AAAA,QACF,MAAM,OAAO,KAAK;AAAA,QAClB,MAAM,OAAO,KAAK;AAAA,MACtB;AAAA,IACJ,CAAC;AAAA,EACL;AAAA,EAhBoB;AAAA,EAFZ;AAAA,EAoBR,MAAM,KAAK,SAAuB;AAC9B,UAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;AACzC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAGlD,UAAM,KAAK,gBAAgB,QAAQ,IAAI,cAAc,EAAE,IAAI,SAAS;AACpE,UAAM,MAAM,gBAAgB,QAAQ,KAAK,eAAe,EAAE,IAAI,SAAS;AACvE,UAAM,UAAU,aAAa,QAAQ,OAAO;AAE5C,UAAM,OAAO,MAAM,KAAK,YAAY,SAAS;AAAA;AAAA;AAAA,MAGzC,MAAM,EAAE,MAAM,KAAK,OAAO,UAAU,KAAK,IAAI,IAAI,IAAI,SAAS,WAAW,KAAK,KAAK,EAAE;AAAA,MACrF,IAAI,gBAAgB,QAAQ,EAAE,EAAE,IAAI,SAAS;AAAA,MAC7C,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,MACd,IAAI,GAAG,SAAS,IAAI,KAAK;AAAA,MACzB,KAAK,IAAI,SAAS,IAAI,MAAM;AAAA,MAC5B,SAAS,WAAW,UAAU,OAAO;AAAA,MACrC,aAAa,QAAQ,aAAa,IAAI,CAAC,OAAO;AAAA,QAC1C,UAAU,EAAE;AAAA,QACZ,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,OAAO,KAAK,EAAE,OAAO;AAAA,QAC1E,aAAa,EAAE;AAAA,MACnB,EAAE;AAAA;AAAA;AAAA,MAGF,SAAS,aAAa,QAAQ,OAAO;AAAA,IACzC,CAAC;AAED,WAAO,EAAE,WAAW,KAAK,WAAW,SAAS,KAAK;AAAA,EACtD;AAAA,EAEA,MAAM,aACF,eACA,KACA,OACgD;AAGhD,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACxD;AACJ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface EmailConfig {
|
|
2
|
-
provider:
|
|
2
|
+
provider: 'smtp' | 'sendgrid' | 'mock' | 'mailgun' | 'ses';
|
|
3
3
|
smtp?: {
|
|
4
4
|
host: string;
|
|
5
5
|
port: number;
|
|
@@ -18,8 +18,19 @@ interface EmailConfig {
|
|
|
18
18
|
};
|
|
19
19
|
templateDir?: string;
|
|
20
20
|
}
|
|
21
|
+
/** An address with an optional display name. */
|
|
22
|
+
interface EmailAddress {
|
|
23
|
+
name?: string;
|
|
24
|
+
address: string;
|
|
25
|
+
}
|
|
26
|
+
/** A recipient: a bare address, or `{ name, address }` to give it a display name. */
|
|
27
|
+
type EmailRecipient = string | EmailAddress;
|
|
21
28
|
interface EmailMessage {
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Each entry is one bare address (`a@x.com`), or `{ name, address }` for a
|
|
31
|
+
* display name: a string with a name, a list or a group is rejected.
|
|
32
|
+
*/
|
|
33
|
+
to: EmailRecipient | EmailRecipient[];
|
|
23
34
|
from?: {
|
|
24
35
|
name?: string;
|
|
25
36
|
email: string;
|
|
@@ -27,9 +38,9 @@ interface EmailMessage {
|
|
|
27
38
|
subject: string;
|
|
28
39
|
text?: string;
|
|
29
40
|
html?: string;
|
|
30
|
-
cc?:
|
|
31
|
-
bcc?:
|
|
32
|
-
replyTo?:
|
|
41
|
+
cc?: EmailRecipient | EmailRecipient[];
|
|
42
|
+
bcc?: EmailRecipient | EmailRecipient[];
|
|
43
|
+
replyTo?: EmailRecipient;
|
|
33
44
|
attachments?: Array<{
|
|
34
45
|
filename: string;
|
|
35
46
|
content: Uint8Array | string;
|
|
@@ -45,23 +56,34 @@ interface EmailAdapter {
|
|
|
45
56
|
messageId: string;
|
|
46
57
|
success: boolean;
|
|
47
58
|
}>;
|
|
48
|
-
sendTemplate(templateName: string, to:
|
|
59
|
+
sendTemplate(templateName: string, to: EmailRecipient | EmailRecipient[], data: TemplateData): Promise<{
|
|
49
60
|
messageId: string;
|
|
50
61
|
success: boolean;
|
|
51
62
|
}>;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The recipients of a `to`/`cc`/`bcc`/`replyTo` field, one mailbox each. A
|
|
67
|
+
* string is a single bare address: one value such as
|
|
68
|
+
* `"bob@x.com <spy@evil.test>, y@x.com"` or `"list: spy@evil.test;"` mailed
|
|
69
|
+
* other people than the ones an allowlist checked. An object's address is
|
|
70
|
+
* checked the same way and its name may not hold control characters.
|
|
71
|
+
*/
|
|
72
|
+
declare function checkRecipients(value: EmailRecipient | EmailRecipient[] | undefined, field?: string): EmailAddress[];
|
|
73
|
+
/** `replyTo`, checked like the recipients: one address at most. */
|
|
74
|
+
declare function checkReplyTo(value: EmailRecipient | undefined): EmailAddress | undefined;
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
77
|
* In-memory email adapter used for tests and local development.
|
|
56
78
|
* It performs no network I/O and stays silent (no stdout) so it is
|
|
57
79
|
* safe to use inside library code and background workers.
|
|
58
80
|
*/
|
|
59
81
|
declare class MockEmailAdapter implements EmailAdapter {
|
|
60
|
-
send(
|
|
82
|
+
send(message: EmailMessage): Promise<{
|
|
61
83
|
messageId: string;
|
|
62
84
|
success: boolean;
|
|
63
85
|
}>;
|
|
64
|
-
sendTemplate(name: string, to:
|
|
86
|
+
sendTemplate(name: string, to: EmailRecipient | EmailRecipient[], data: TemplateData): Promise<{
|
|
65
87
|
messageId: string;
|
|
66
88
|
success: boolean;
|
|
67
89
|
}>;
|
|
@@ -79,7 +101,7 @@ declare class SmtpEmailAdapter implements EmailAdapter {
|
|
|
79
101
|
private transporter;
|
|
80
102
|
constructor(config: EmailConfig);
|
|
81
103
|
send(message: EmailMessage): Promise<{
|
|
82
|
-
messageId:
|
|
104
|
+
messageId: string;
|
|
83
105
|
success: boolean;
|
|
84
106
|
}>;
|
|
85
107
|
sendTemplate(_templateName: string, _to: string | string[], _data: TemplateData): Promise<{
|
|
@@ -90,6 +112,12 @@ declare class SmtpEmailAdapter implements EmailAdapter {
|
|
|
90
112
|
|
|
91
113
|
declare class SendGridEmailAdapter implements EmailAdapter {
|
|
92
114
|
private config;
|
|
115
|
+
/**
|
|
116
|
+
* A client per adapter: the package's default export is a process-wide
|
|
117
|
+
* singleton, so the last adapter created set the API key for all of them
|
|
118
|
+
* (one tenant's mail sent with another tenant's account).
|
|
119
|
+
*/
|
|
120
|
+
private readonly client;
|
|
93
121
|
constructor(config: EmailConfig);
|
|
94
122
|
send(message: EmailMessage): Promise<{
|
|
95
123
|
messageId: string;
|
|
@@ -149,8 +177,10 @@ declare class SesEmailAdapter implements EmailAdapter {
|
|
|
149
177
|
private defaultFrom?;
|
|
150
178
|
private injectedClient?;
|
|
151
179
|
private injectedCommand?;
|
|
180
|
+
private sdk?;
|
|
152
181
|
constructor(config: EmailConfig, deps?: SesAdapterDeps);
|
|
153
182
|
private resolveSdk;
|
|
183
|
+
private loadSdk;
|
|
154
184
|
send(message: EmailMessage): Promise<{
|
|
155
185
|
messageId: string;
|
|
156
186
|
success: boolean;
|
|
@@ -161,4 +191,4 @@ declare class SesEmailAdapter implements EmailAdapter {
|
|
|
161
191
|
}>;
|
|
162
192
|
}
|
|
163
193
|
|
|
164
|
-
export { type EmailAdapter, type EmailConfig, type EmailMessage, MailgunEmailAdapter, MockEmailAdapter, SendGridEmailAdapter, type SesAdapterDeps, type SesClient, type SesCommand, type SesCommandFactory, SesEmailAdapter, SmtpEmailAdapter, type TemplateData, createEmailAdapter };
|
|
194
|
+
export { type EmailAdapter, type EmailAddress, type EmailConfig, type EmailMessage, type EmailRecipient, MailgunEmailAdapter, MockEmailAdapter, SendGridEmailAdapter, type SesAdapterDeps, type SesClient, type SesCommand, type SesCommandFactory, SesEmailAdapter, SmtpEmailAdapter, type TemplateData, checkRecipients, checkReplyTo, createEmailAdapter };
|