From a3a136c65d20337edbf6c7fffdd62924a51aa14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fre=CC=81de=CC=81ric=20Jean?= Date: Wed, 1 Apr 2026 15:16:25 +0200 Subject: [PATCH] first commit --- README.md | 30 +++++++++++++++ dist/server/index.d.ts | 27 ++++++++++++++ dist/server/index.js | 52 ++++++++++++++++++++++++++ dist/server/index.js.map | 1 + package.json | 19 ++++++++++ server/index.ts | 2 + server/postal.ts | 79 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 13 +++++++ tsup.config.ts | 13 +++++++ 9 files changed, 236 insertions(+) create mode 100644 README.md create mode 100644 dist/server/index.d.ts create mode 100644 dist/server/index.js create mode 100644 dist/server/index.js.map create mode 100644 package.json create mode 100644 server/index.ts create mode 100644 server/postal.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0338f9 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# @packages/mailing + +Client réutilisable pour l'envoi d'emails via Postal. + +## Export + +- `@packages/mailing/server` + +## API + +- `createPostalClient` +- `createPostalClientFromEnv` + +## Variables d'environnement par défaut + +- `POSTAL_URL` +- `POSTAL_API_KEY` +- `POSTAL_MESSAGE_FROM` + +## Build + +```bash +npm run build --workspace @packages/mailing +``` + +## Watch + +```bash +npm run dev --workspace @packages/mailing +``` diff --git a/dist/server/index.d.ts b/dist/server/index.d.ts new file mode 100644 index 0000000..e167534 --- /dev/null +++ b/dist/server/index.d.ts @@ -0,0 +1,27 @@ +type PostalMessageInput = { + to: string[]; + subject: string; + plainBody: string; + htmlBody: string; +}; +type PostalClient = { + enabled: boolean; + sendMessage: (input: PostalMessageInput) => Promise; +}; +type CreatePostalClientOptions = { + baseUrl: string; + apiKey: string; + from: string; + fetchImpl?: typeof fetch; +}; +type CreatePostalClientFromEnvOptions = { + baseUrlEnv?: string; + apiKeyEnv?: string; + fromEnv?: string; + env?: Record; + fetchImpl?: typeof fetch; +}; +declare function createPostalClient(options: CreatePostalClientOptions): PostalClient; +declare function createPostalClientFromEnv(options?: CreatePostalClientFromEnvOptions): PostalClient; + +export { type CreatePostalClientOptions, type PostalClient, type PostalMessageInput, createPostalClient, createPostalClientFromEnv }; diff --git a/dist/server/index.js b/dist/server/index.js new file mode 100644 index 0000000..142b24c --- /dev/null +++ b/dist/server/index.js @@ -0,0 +1,52 @@ +// server/postal.ts +function createPostalClient(options) { + const baseUrl = options.baseUrl.trim(); + const apiKey = options.apiKey.trim(); + const from = options.from.trim(); + const fetchImpl = options.fetchImpl ?? fetch; + const enabled = baseUrl.length > 0 && apiKey.length > 0 && from.length > 0; + return { + enabled, + async sendMessage(input) { + if (!enabled || input.to.length === 0) { + return; + } + const endpoint = new URL("/api/v1/send/message", ensureTrailingSlash(baseUrl)); + const response = await fetchImpl(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Server-API-Key": apiKey + }, + body: JSON.stringify({ + from, + to: input.to, + subject: input.subject, + plain_body: input.plainBody, + html_body: input.htmlBody + }) + }); + const payload = await response.json().catch(() => null); + if (!response.ok || payload?.status !== "success") { + throw new Error(`postal_send_failed:${response.status}`); + } + } + }; +} +function createPostalClientFromEnv(options = {}) { + const env = options.env ?? process.env; + return createPostalClient({ + baseUrl: env[options.baseUrlEnv ?? "POSTAL_URL"] ?? "", + apiKey: env[options.apiKeyEnv ?? "POSTAL_API_KEY"] ?? "", + from: env[options.fromEnv ?? "POSTAL_MESSAGE_FROM"] ?? "", + fetchImpl: options.fetchImpl + }); +} +function ensureTrailingSlash(value) { + return value.endsWith("/") ? value : `${value}/`; +} +export { + createPostalClient, + createPostalClientFromEnv +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/server/index.js.map b/dist/server/index.js.map new file mode 100644 index 0000000..05fd973 --- /dev/null +++ b/dist/server/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../server/postal.ts"],"sourcesContent":["export type PostalMessageInput = {\n to: string[];\n subject: string;\n plainBody: string;\n htmlBody: string;\n};\n\nexport type PostalClient = {\n enabled: boolean;\n sendMessage: (input: PostalMessageInput) => Promise;\n};\n\nexport type CreatePostalClientOptions = {\n baseUrl: string;\n apiKey: string;\n from: string;\n fetchImpl?: typeof fetch;\n};\n\nexport type CreatePostalClientFromEnvOptions = {\n baseUrlEnv?: string;\n apiKeyEnv?: string;\n fromEnv?: string;\n env?: Record;\n fetchImpl?: typeof fetch;\n};\n\nexport function createPostalClient(options: CreatePostalClientOptions): PostalClient {\n const baseUrl = options.baseUrl.trim();\n const apiKey = options.apiKey.trim();\n const from = options.from.trim();\n const fetchImpl = options.fetchImpl ?? fetch;\n const enabled = baseUrl.length > 0 && apiKey.length > 0 && from.length > 0;\n\n return {\n enabled,\n async sendMessage(input: PostalMessageInput): Promise {\n if (!enabled || input.to.length === 0) {\n return;\n }\n\n const endpoint = new URL(\"/api/v1/send/message\", ensureTrailingSlash(baseUrl));\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Server-API-Key\": apiKey\n },\n body: JSON.stringify({\n from,\n to: input.to,\n subject: input.subject,\n plain_body: input.plainBody,\n html_body: input.htmlBody\n })\n });\n\n const payload = (await response.json().catch(() => null)) as { status?: string } | null;\n if (!response.ok || payload?.status !== \"success\") {\n throw new Error(`postal_send_failed:${response.status}`);\n }\n }\n };\n}\n\nexport function createPostalClientFromEnv(options: CreatePostalClientFromEnvOptions = {}): PostalClient {\n const env = options.env ?? process.env;\n\n return createPostalClient({\n baseUrl: env[options.baseUrlEnv ?? \"POSTAL_URL\"] ?? \"\",\n apiKey: env[options.apiKeyEnv ?? \"POSTAL_API_KEY\"] ?? \"\",\n from: env[options.fromEnv ?? \"POSTAL_MESSAGE_FROM\"] ?? \"\",\n fetchImpl: options.fetchImpl\n });\n}\n\nfunction ensureTrailingSlash(value: string): string {\n return value.endsWith(\"/\") ? value : `${value}/`;\n}\n"],"mappings":";AA2BO,SAAS,mBAAmB,SAAkD;AACnF,QAAM,UAAU,QAAQ,QAAQ,KAAK;AACrC,QAAM,SAAS,QAAQ,OAAO,KAAK;AACnC,QAAM,OAAO,QAAQ,KAAK,KAAK;AAC/B,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,UAAU,QAAQ,SAAS,KAAK,OAAO,SAAS,KAAK,KAAK,SAAS;AAEzE,SAAO;AAAA,IACL;AAAA,IACA,MAAM,YAAY,OAA0C;AAC1D,UAAI,CAAC,WAAW,MAAM,GAAG,WAAW,GAAG;AACrC;AAAA,MACF;AAEA,YAAM,WAAW,IAAI,IAAI,wBAAwB,oBAAoB,OAAO,CAAC;AAC7E,YAAM,WAAW,MAAM,UAAU,UAAU;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,oBAAoB;AAAA,QACtB;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB;AAAA,UACA,IAAI,MAAM;AAAA,UACV,SAAS,MAAM;AAAA,UACf,YAAY,MAAM;AAAA,UAClB,WAAW,MAAM;AAAA,QACnB,CAAC;AAAA,MACH,CAAC;AAED,YAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AACvD,UAAI,CAAC,SAAS,MAAM,SAAS,WAAW,WAAW;AACjD,cAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,EAAE;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,0BAA0B,UAA4C,CAAC,GAAiB;AACtG,QAAM,MAAM,QAAQ,OAAO,QAAQ;AAEnC,SAAO,mBAAmB;AAAA,IACxB,SAAS,IAAI,QAAQ,cAAc,YAAY,KAAK;AAAA,IACpD,QAAQ,IAAI,QAAQ,aAAa,gBAAgB,KAAK;AAAA,IACtD,MAAM,IAAI,QAAQ,WAAW,qBAAqB,KAAK;AAAA,IACvD,WAAW,QAAQ;AAAA,EACrB,CAAC;AACH;AAEA,SAAS,oBAAoB,OAAuB;AAClD,SAAO,MAAM,SAAS,GAAG,IAAI,QAAQ,GAAG,KAAK;AAC/C;","names":[]} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3076e07 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "@packages/mailing", + "version": "0.0.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "exports": { + "./server": { + "types": "./dist/server/index.d.ts", + "default": "./dist/server/index.js" + } + }, + "scripts": { + "build": "tsup --config tsup.config.ts", + "dev": "tsup --config tsup.config.ts --watch" + } +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..61680b5 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,2 @@ +export { createPostalClient, createPostalClientFromEnv } from "./postal.js"; +export type { CreatePostalClientOptions, PostalClient, PostalMessageInput } from "./postal.js"; diff --git a/server/postal.ts b/server/postal.ts new file mode 100644 index 0000000..b04c22c --- /dev/null +++ b/server/postal.ts @@ -0,0 +1,79 @@ +export type PostalMessageInput = { + to: string[]; + subject: string; + plainBody: string; + htmlBody: string; +}; + +export type PostalClient = { + enabled: boolean; + sendMessage: (input: PostalMessageInput) => Promise; +}; + +export type CreatePostalClientOptions = { + baseUrl: string; + apiKey: string; + from: string; + fetchImpl?: typeof fetch; +}; + +export type CreatePostalClientFromEnvOptions = { + baseUrlEnv?: string; + apiKeyEnv?: string; + fromEnv?: string; + env?: Record; + fetchImpl?: typeof fetch; +}; + +export function createPostalClient(options: CreatePostalClientOptions): PostalClient { + const baseUrl = options.baseUrl.trim(); + const apiKey = options.apiKey.trim(); + const from = options.from.trim(); + const fetchImpl = options.fetchImpl ?? fetch; + const enabled = baseUrl.length > 0 && apiKey.length > 0 && from.length > 0; + + return { + enabled, + async sendMessage(input: PostalMessageInput): Promise { + if (!enabled || input.to.length === 0) { + return; + } + + const endpoint = new URL("/api/v1/send/message", ensureTrailingSlash(baseUrl)); + const response = await fetchImpl(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Server-API-Key": apiKey + }, + body: JSON.stringify({ + from, + to: input.to, + subject: input.subject, + plain_body: input.plainBody, + html_body: input.htmlBody + }) + }); + + const payload = (await response.json().catch(() => null)) as { status?: string } | null; + if (!response.ok || payload?.status !== "success") { + throw new Error(`postal_send_failed:${response.status}`); + } + } + }; +} + +export function createPostalClientFromEnv(options: CreatePostalClientFromEnvOptions = {}): PostalClient { + const env = options.env ?? process.env; + + return createPostalClient({ + baseUrl: env[options.baseUrlEnv ?? "POSTAL_URL"] ?? "", + apiKey: env[options.apiKeyEnv ?? "POSTAL_API_KEY"] ?? "", + from: env[options.fromEnv ?? "POSTAL_MESSAGE_FROM"] ?? "", + fetchImpl: options.fetchImpl + }); +} + +function ensureTrailingSlash(value: string): string { + return value.endsWith("/") ? value : `${value}/`; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..adc0534 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "emitDeclarationOnly": false, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["server"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..267b96c --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + tsconfig: "tsconfig.json", + entry: { + "server/index": "server/index.ts" + }, + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + outDir: "dist" +});