@core-tecnologias-empresariales/core-mail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @core-tecnologias-empresariales/core-mail
2
+
3
+ Envío de correo del ecosistema Core Tecnología Empresarial. Motor de transporte: [Nodemailer](https://nodemailer.com/) (SMTP, sin dependencia de un proveedor específico).
4
+
5
+ ## Para qué sirve
6
+
7
+ **Fallback multi-SMTP** (CLAUDE.md §13/§74) — lo que este paquete aporta sobre usar Nodemailer directo:
8
+
9
+ ```text
10
+ SMTP principal → falló → SMTP secundario → falló → SMTP terciario
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ import { createMailer, createSmtpTransport } from "@core-tecnologias-empresariales/core-mail";
17
+
18
+ const mailer = createMailer([
19
+ createSmtpTransport({ name: "principal", host: "smtp1.example.com", port: 587, auth: { user, pass } }),
20
+ createSmtpTransport({ name: "backup", host: "smtp2.example.com", port: 587, auth: { user: user2, pass: pass2 } }),
21
+ ]);
22
+
23
+ const { transport } = await mailer.send({
24
+ to: "user@example.com",
25
+ from: "noreply@core.dev",
26
+ subject: "Recuperación de contraseña",
27
+ html: "<p>Tu código: 483920</p>",
28
+ });
29
+ // transport === "principal" (o "backup" si el principal falló)
30
+ ```
31
+
32
+ Si todos los transportes fallan, `mailer.send()` lanza `MailDeliveryError` con `attemptedTransports` (los nombres, en orden) y `cause` (el último error real) — nunca se pierde el correo silenciosamente.
33
+
34
+ ### Seguridad
35
+
36
+ Las credenciales SMTP (`auth.user`/`auth.pass`) nunca se hardcodean: se inyectan desde environment variables o un secret manager al llamar `createSmtpTransport()` — este paquete no las persiste ni las expone.
37
+
38
+ ## Para qué NO sirve (`ponytail:` diferido)
39
+
40
+ - **Templates.** CLAUDE.md deja templates como responsabilidad de cada producto cuando el contenido es específico del negocio; v0.1 no incluye un motor de templates especulativo.
41
+ - **Categorización por tipo de correo** (transactional/security/notification) y **priorización de correos críticos** — se agregan cuando exista un consumidor real (probablemente junto con `core-notifications`).
42
+
43
+ ## Instalación
44
+
45
+ ```bash
46
+ pnpm add @core-tecnologias-empresariales/core-mail
47
+ ```
48
+
49
+ ## Desarrollo
50
+
51
+ ```bash
52
+ pnpm build
53
+ pnpm test
54
+ ```
@@ -0,0 +1,5 @@
1
+ export { createMailer, MailDeliveryError } from "./mailer.js";
2
+ export type { Mailer } from "./mailer.js";
3
+ export { createSmtpTransport } from "./smtp-transport.js";
4
+ export type { SmtpConfig } from "./smtp-transport.js";
5
+ export type { MailMessage, MailTransport } from "./transport.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createMailer, MailDeliveryError } from "./mailer.js";
2
+ export { createSmtpTransport } from "./smtp-transport.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { MailMessage, MailTransport } from "./transport.js";
2
+ export declare class MailDeliveryError extends Error {
3
+ readonly attemptedTransports: string[];
4
+ readonly cause: unknown;
5
+ constructor(attemptedTransports: string[], cause: unknown);
6
+ }
7
+ export interface Mailer {
8
+ send(message: MailMessage): Promise<{
9
+ transport: string;
10
+ }>;
11
+ }
12
+ /**
13
+ * Orquesta varios `MailTransport` con fallback en orden (CLAUDE.md §13/§74):
14
+ * SMTP 1 → falló → SMTP 2 → falló → SMTP 3. Se detiene en el primero que envía con éxito.
15
+ */
16
+ export declare function createMailer(transports: MailTransport[]): Mailer;
package/dist/mailer.js ADDED
@@ -0,0 +1,35 @@
1
+ export class MailDeliveryError extends Error {
2
+ attemptedTransports;
3
+ cause;
4
+ constructor(attemptedTransports, cause) {
5
+ super(`No se pudo enviar el correo tras intentar: ${attemptedTransports.join(", ")}`);
6
+ this.name = "MailDeliveryError";
7
+ this.attemptedTransports = attemptedTransports;
8
+ this.cause = cause;
9
+ }
10
+ }
11
+ /**
12
+ * Orquesta varios `MailTransport` con fallback en orden (CLAUDE.md §13/§74):
13
+ * SMTP 1 → falló → SMTP 2 → falló → SMTP 3. Se detiene en el primero que envía con éxito.
14
+ */
15
+ export function createMailer(transports) {
16
+ if (transports.length === 0) {
17
+ throw new Error("createMailer requiere al menos un MailTransport");
18
+ }
19
+ return {
20
+ async send(message) {
21
+ let lastError;
22
+ for (const transport of transports) {
23
+ try {
24
+ await transport.send(message);
25
+ return { transport: transport.name };
26
+ }
27
+ catch (error) {
28
+ lastError = error;
29
+ }
30
+ }
31
+ throw new MailDeliveryError(transports.map((t) => t.name), lastError);
32
+ },
33
+ };
34
+ }
35
+ //# sourceMappingURL=mailer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer.js","sourceRoot":"","sources":["../src/mailer.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,mBAAmB,CAAW;IACrB,KAAK,CAAU;IAEjC,YAAY,mBAA6B,EAAE,KAAc;QACvD,KAAK,CAAC,8CAA8C,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAMD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,UAA2B;IACtD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,IAAI,SAAkB,CAAC;YACvB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,IAAI,CAAC;oBACH,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAC9B,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC;gBACvC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,SAAS,GAAG,KAAK,CAAC;gBACpB,CAAC;YACH,CAAC;YACD,MAAM,IAAI,iBAAiB,CACzB,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAC7B,SAAS,CACV,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createMailer, MailDeliveryError } from "./mailer.js";
4
+ function fakeTransport(name, behavior) {
5
+ return {
6
+ name,
7
+ async send() {
8
+ if (behavior === "fail")
9
+ throw new Error(`${name} down`);
10
+ },
11
+ };
12
+ }
13
+ const message = { to: "user@example.com", from: "noreply@core.dev", subject: "hola" };
14
+ test("sends via the first working transport", async () => {
15
+ const mailer = createMailer([fakeTransport("primary", "ok"), fakeTransport("secondary", "ok")]);
16
+ const result = await mailer.send(message);
17
+ assert.equal(result.transport, "primary");
18
+ });
19
+ test("falls back to the next transport when the first fails", async () => {
20
+ const mailer = createMailer([fakeTransport("primary", "fail"), fakeTransport("secondary", "ok")]);
21
+ const result = await mailer.send(message);
22
+ assert.equal(result.transport, "secondary");
23
+ });
24
+ test("throws MailDeliveryError listing every attempted transport when all fail", async () => {
25
+ const mailer = createMailer([fakeTransport("primary", "fail"), fakeTransport("secondary", "fail")]);
26
+ await assert.rejects(() => mailer.send(message), (error) => {
27
+ assert.ok(error instanceof MailDeliveryError);
28
+ assert.deepEqual(error.attemptedTransports, ["primary", "secondary"]);
29
+ return true;
30
+ });
31
+ });
32
+ test("createMailer requires at least one transport", () => {
33
+ assert.throws(() => createMailer([]));
34
+ });
35
+ //# sourceMappingURL=mailer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer.test.js","sourceRoot":"","sources":["../src/mailer.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAG9D,SAAS,aAAa,CAAC,IAAY,EAAE,QAAuB;IAC1D,OAAO;QACL,IAAI;QACJ,KAAK,CAAC,IAAI;YACR,IAAI,QAAQ,KAAK,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC;QAC3D,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,GAAgB,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAEnG,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAChG,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;IACvE,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAClG,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;IAC1F,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAc,EAAE,EAAE;QAClE,MAAM,CAAC,EAAE,CAAC,KAAK,YAAY,iBAAiB,CAAC,CAAC;QAC9C,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,mBAAmB,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;IACxD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { MailTransport } from "./transport.js";
2
+ export interface SmtpConfig {
3
+ name: string;
4
+ host: string;
5
+ port: number;
6
+ secure?: boolean;
7
+ /** Credenciales: nunca hardcodear — inyectar desde environment variables o secret manager (CLAUDE.md §76). */
8
+ auth?: {
9
+ user: string;
10
+ pass: string;
11
+ };
12
+ }
13
+ /** Transporte SMTP concreto sobre Nodemailer. */
14
+ export declare function createSmtpTransport(config: SmtpConfig): MailTransport;
@@ -0,0 +1,17 @@
1
+ import nodemailer from "nodemailer";
2
+ /** Transporte SMTP concreto sobre Nodemailer. */
3
+ export function createSmtpTransport(config) {
4
+ const transporter = nodemailer.createTransport({
5
+ host: config.host,
6
+ port: config.port,
7
+ secure: config.secure,
8
+ auth: config.auth,
9
+ });
10
+ return {
11
+ name: config.name,
12
+ async send(message) {
13
+ await transporter.sendMail(message);
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=smtp-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-transport.js","sourceRoot":"","sources":["../src/smtp-transport.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAYpC,iDAAiD;AACjD,MAAM,UAAU,mBAAmB,CAAC,MAAkB;IACpD,MAAM,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;QAC7C,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI;KAClB,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,MAAM,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createSmtpTransport } from "./smtp-transport.js";
4
+ test("createSmtpTransport returns a MailTransport with the given name", () => {
5
+ const transport = createSmtpTransport({ name: "principal", host: "smtp.example.com", port: 587 });
6
+ assert.equal(transport.name, "principal");
7
+ assert.equal(typeof transport.send, "function");
8
+ });
9
+ //# sourceMappingURL=smtp-transport.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-transport.test.js","sourceRoot":"","sources":["../src/smtp-transport.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,MAAM,SAAS,GAAG,mBAAmB,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IAClG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC1C,MAAM,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface MailMessage {
2
+ to: string | string[];
3
+ from: string;
4
+ subject: string;
5
+ html?: string;
6
+ text?: string;
7
+ replyTo?: string;
8
+ }
9
+ /** Un canal de envío concreto (un SMTP configurado). `core-mail` orquesta varios vía `createMailer()`. */
10
+ export interface MailTransport {
11
+ name: string;
12
+ send(message: MailMessage): Promise<void>;
13
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@core-tecnologias-empresariales/core-mail",
3
+ "version": "0.1.0",
4
+ "description": "Envío de correo con múltiples SMTP y fallback para el ecosistema Core Tecnología Empresarial.",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": "./dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "nodemailer": "^6.9.16"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.7.0",
23
+ "@types/node": "^20.14.0",
24
+ "@types/nodemailer": "^6.4.16"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "test": "tsc -p tsconfig.json && node --test dist"
29
+ }
30
+ }