@mailzeno/core 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,68 @@
1
+ # @mailzeno/core
2
+
3
+ SMTP engine powering MailZeno.
4
+
5
+ Low-level email sending library with:
6
+
7
+ - SMTP pooling
8
+ - Retry logic
9
+ - Exponential backoff
10
+ - Transient error detection
11
+ - React email rendering
12
+ - Timeout handling
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @mailzeno/core
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { sendEmail } from "@mailzeno/core"
28
+
29
+ await sendEmail(
30
+ {
31
+ id: "smtp_id",
32
+ host: "smtp.example.com",
33
+ port: 587,
34
+ secure: false,
35
+ user: "username",
36
+ pass: "password"
37
+ },
38
+ {
39
+ from: "you@example.com",
40
+ to: "recipient@example.com",
41
+ subject: "Hello",
42
+ html: "<h1>Hello world</h1>"
43
+ }
44
+ )
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Features
50
+
51
+ - SMTP connection pooling
52
+ - AES-safe config handling (handled by parent app)
53
+ - Automatic retries for transient failures
54
+ - React email rendering support
55
+ - Structured error mapping
56
+ - ESM + CommonJS support
57
+
58
+ ---
59
+
60
+ ## Requirements
61
+
62
+ - Node.js >= 18
63
+
64
+ ---
65
+
66
+ ## License
67
+
68
+ MIT © MailZeno
package/dist/index.cjs ADDED
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ sendEmail: () => sendEmail
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/smtp/transporter.ts
38
+ var import_nodemailer = __toESM(require("nodemailer"), 1);
39
+ var transporterCache = /* @__PURE__ */ new Map();
40
+ function getCacheKey(smtp) {
41
+ return `${smtp.id}:${smtp.host}:${smtp.port}:${smtp.user}`;
42
+ }
43
+ function getTransporter(smtp) {
44
+ const key = getCacheKey(smtp);
45
+ if (transporterCache.has(key)) {
46
+ return transporterCache.get(key);
47
+ }
48
+ const transporter = import_nodemailer.default.createTransport({
49
+ host: smtp.host,
50
+ port: smtp.port,
51
+ secure: smtp.secure,
52
+ pool: true,
53
+ maxConnections: smtp.maxConnections ?? 5,
54
+ maxMessages: smtp.maxMessages ?? 1e3,
55
+ connectionTimeout: smtp.connectionTimeout ?? 1e4,
56
+ greetingTimeout: smtp.greetingTimeout ?? 1e4,
57
+ socketTimeout: smtp.socketTimeout ?? 15e3,
58
+ auth: {
59
+ user: smtp.user,
60
+ pass: smtp.pass
61
+ },
62
+ tls: {
63
+ rejectUnauthorized: true
64
+ }
65
+ });
66
+ transporter.on("error", () => {
67
+ transporterCache.delete(key);
68
+ });
69
+ transporterCache.set(key, transporter);
70
+ return transporter;
71
+ }
72
+
73
+ // src/retry/retry-queue.ts
74
+ function isTransientError(error) {
75
+ const responseCode = error?.responseCode;
76
+ if (responseCode && responseCode >= 400 && responseCode < 500) {
77
+ return true;
78
+ }
79
+ if (error?.code === "ETIMEDOUT" || error?.code === "ECONNECTION") {
80
+ return true;
81
+ }
82
+ return false;
83
+ }
84
+ async function retry(fn, attempts = 3) {
85
+ let lastError;
86
+ for (let i = 0; i < attempts; i++) {
87
+ try {
88
+ return await fn();
89
+ } catch (err) {
90
+ lastError = err;
91
+ if (!isTransientError(err) || i === attempts - 1) {
92
+ throw err;
93
+ }
94
+ const delay = Math.pow(2, i) * 1e3 + Math.random() * 300;
95
+ await new Promise((res) => setTimeout(res, delay));
96
+ }
97
+ }
98
+ throw lastError;
99
+ }
100
+
101
+ // src/render/render-html.ts
102
+ function renderHtml(html, text) {
103
+ const hasHtml = html && html.trim().length > 0;
104
+ const hasText = text && text.trim().length > 0;
105
+ if (!hasHtml && !hasText) {
106
+ throw new Error("Email must contain html or text");
107
+ }
108
+ return {
109
+ html: hasHtml ? html : void 0,
110
+ text: hasText ? text : void 0
111
+ };
112
+ }
113
+
114
+ // src/render/render-react.ts
115
+ var import_render = require("@react-email/render");
116
+
117
+ // src/errors.ts
118
+ var MailZenoError = class extends Error {
119
+ constructor(message, code = "UNKNOWN_ERROR", options) {
120
+ super(message);
121
+ this.name = this.constructor.name;
122
+ this.code = code;
123
+ this.statusCode = options?.statusCode;
124
+ this.cause = options?.cause;
125
+ Object.setPrototypeOf(this, new.target.prototype);
126
+ if (Error.captureStackTrace) {
127
+ Error.captureStackTrace(this, this.constructor);
128
+ }
129
+ }
130
+ };
131
+ var ValidationError = class extends MailZenoError {
132
+ constructor(message, cause) {
133
+ super(message, "VALIDATION_ERROR", { statusCode: 400, cause });
134
+ }
135
+ };
136
+ var RenderError = class extends MailZenoError {
137
+ constructor(message, cause) {
138
+ super(message, "RENDER_ERROR", { cause });
139
+ }
140
+ };
141
+ var SMTPConnectionError = class extends MailZenoError {
142
+ constructor(message, cause) {
143
+ super(message, "SMTP_CONNECTION_ERROR", { cause });
144
+ }
145
+ };
146
+ var SMTPAuthError = class extends MailZenoError {
147
+ constructor(message, cause) {
148
+ super(message, "SMTP_AUTH_ERROR", { statusCode: 401, cause });
149
+ }
150
+ };
151
+ var SMTPResponseError = class extends MailZenoError {
152
+ constructor(message, cause) {
153
+ super(message, "SMTP_RESPONSE_ERROR", { cause });
154
+ }
155
+ };
156
+ var SMTPTimeoutError = class extends MailZenoError {
157
+ constructor(message, cause) {
158
+ super(message, "SMTP_TIMEOUT_ERROR", { cause });
159
+ }
160
+ };
161
+
162
+ // src/render/render-react.ts
163
+ async function renderReact(component) {
164
+ try {
165
+ const html = await (0, import_render.render)(component);
166
+ return { html };
167
+ } catch (err) {
168
+ throw new RenderError("React email rendering failed", err);
169
+ }
170
+ }
171
+
172
+ // src/smtp/send.ts
173
+ async function sendEmail(smtp, options) {
174
+ try {
175
+ if (!options.from || !options.to || !options.subject) {
176
+ throw new ValidationError(
177
+ "Missing required fields: from, to, subject"
178
+ );
179
+ }
180
+ let html = options.html;
181
+ let text = options.text;
182
+ if (options.react) {
183
+ try {
184
+ const rendered2 = await renderReact(options.react);
185
+ html = rendered2.html;
186
+ } catch (err) {
187
+ throw new RenderError("React email rendering failed", err);
188
+ }
189
+ }
190
+ const rendered = renderHtml(html, text);
191
+ const transporter = getTransporter(smtp);
192
+ const info = await retry(
193
+ () => transporter.sendMail({
194
+ from: options.from,
195
+ to: options.to,
196
+ subject: options.subject,
197
+ html: rendered.html,
198
+ text: rendered.text
199
+ })
200
+ );
201
+ return {
202
+ success: true,
203
+ messageId: info.messageId,
204
+ accepted: info.accepted,
205
+ rejected: info.rejected,
206
+ response: info.response
207
+ };
208
+ } catch (err) {
209
+ if (err instanceof MailZenoError) {
210
+ return {
211
+ success: false,
212
+ error: err.message
213
+ };
214
+ }
215
+ if (err?.code === "EAUTH") {
216
+ return {
217
+ success: false,
218
+ error: new SMTPAuthError(
219
+ "SMTP authentication failed",
220
+ err
221
+ ).message
222
+ };
223
+ }
224
+ if (err?.code === "ETIMEDOUT") {
225
+ return {
226
+ success: false,
227
+ error: new SMTPTimeoutError(
228
+ "SMTP connection timed out",
229
+ err
230
+ ).message
231
+ };
232
+ }
233
+ if (err?.code === "ECONNECTION") {
234
+ return {
235
+ success: false,
236
+ error: new SMTPConnectionError(
237
+ "SMTP connection failed",
238
+ err
239
+ ).message
240
+ };
241
+ }
242
+ return {
243
+ success: false,
244
+ error: new SMTPResponseError(
245
+ err?.message || "SMTP sending failed",
246
+ err
247
+ ).message
248
+ };
249
+ }
250
+ }
251
+ // Annotate the CommonJS export names for ESM import in node:
252
+ 0 && (module.exports = {
253
+ sendEmail
254
+ });
@@ -0,0 +1,33 @@
1
+ interface SMTPConfig {
2
+ id: string;
3
+ host: string;
4
+ port: number;
5
+ secure: boolean;
6
+ user: string;
7
+ pass: string;
8
+ connectionTimeout?: number;
9
+ greetingTimeout?: number;
10
+ socketTimeout?: number;
11
+ maxConnections?: number;
12
+ maxMessages?: number;
13
+ }
14
+ interface SendEmailOptions {
15
+ from: string;
16
+ to: string | string[];
17
+ subject: string;
18
+ html?: string;
19
+ text?: string;
20
+ react?: React.ReactElement;
21
+ }
22
+ interface SendEmailResponse {
23
+ success: boolean;
24
+ messageId?: string;
25
+ accepted?: string[];
26
+ rejected?: string[];
27
+ response?: string;
28
+ error?: string;
29
+ }
30
+
31
+ declare function sendEmail(smtp: SMTPConfig, options: SendEmailOptions): Promise<SendEmailResponse>;
32
+
33
+ export { type SMTPConfig, type SendEmailOptions, type SendEmailResponse, sendEmail };
@@ -0,0 +1,33 @@
1
+ interface SMTPConfig {
2
+ id: string;
3
+ host: string;
4
+ port: number;
5
+ secure: boolean;
6
+ user: string;
7
+ pass: string;
8
+ connectionTimeout?: number;
9
+ greetingTimeout?: number;
10
+ socketTimeout?: number;
11
+ maxConnections?: number;
12
+ maxMessages?: number;
13
+ }
14
+ interface SendEmailOptions {
15
+ from: string;
16
+ to: string | string[];
17
+ subject: string;
18
+ html?: string;
19
+ text?: string;
20
+ react?: React.ReactElement;
21
+ }
22
+ interface SendEmailResponse {
23
+ success: boolean;
24
+ messageId?: string;
25
+ accepted?: string[];
26
+ rejected?: string[];
27
+ response?: string;
28
+ error?: string;
29
+ }
30
+
31
+ declare function sendEmail(smtp: SMTPConfig, options: SendEmailOptions): Promise<SendEmailResponse>;
32
+
33
+ export { type SMTPConfig, type SendEmailOptions, type SendEmailResponse, sendEmail };
package/dist/index.js ADDED
@@ -0,0 +1,217 @@
1
+ // src/smtp/transporter.ts
2
+ import nodemailer from "nodemailer";
3
+ var transporterCache = /* @__PURE__ */ new Map();
4
+ function getCacheKey(smtp) {
5
+ return `${smtp.id}:${smtp.host}:${smtp.port}:${smtp.user}`;
6
+ }
7
+ function getTransporter(smtp) {
8
+ const key = getCacheKey(smtp);
9
+ if (transporterCache.has(key)) {
10
+ return transporterCache.get(key);
11
+ }
12
+ const transporter = nodemailer.createTransport({
13
+ host: smtp.host,
14
+ port: smtp.port,
15
+ secure: smtp.secure,
16
+ pool: true,
17
+ maxConnections: smtp.maxConnections ?? 5,
18
+ maxMessages: smtp.maxMessages ?? 1e3,
19
+ connectionTimeout: smtp.connectionTimeout ?? 1e4,
20
+ greetingTimeout: smtp.greetingTimeout ?? 1e4,
21
+ socketTimeout: smtp.socketTimeout ?? 15e3,
22
+ auth: {
23
+ user: smtp.user,
24
+ pass: smtp.pass
25
+ },
26
+ tls: {
27
+ rejectUnauthorized: true
28
+ }
29
+ });
30
+ transporter.on("error", () => {
31
+ transporterCache.delete(key);
32
+ });
33
+ transporterCache.set(key, transporter);
34
+ return transporter;
35
+ }
36
+
37
+ // src/retry/retry-queue.ts
38
+ function isTransientError(error) {
39
+ const responseCode = error?.responseCode;
40
+ if (responseCode && responseCode >= 400 && responseCode < 500) {
41
+ return true;
42
+ }
43
+ if (error?.code === "ETIMEDOUT" || error?.code === "ECONNECTION") {
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ async function retry(fn, attempts = 3) {
49
+ let lastError;
50
+ for (let i = 0; i < attempts; i++) {
51
+ try {
52
+ return await fn();
53
+ } catch (err) {
54
+ lastError = err;
55
+ if (!isTransientError(err) || i === attempts - 1) {
56
+ throw err;
57
+ }
58
+ const delay = Math.pow(2, i) * 1e3 + Math.random() * 300;
59
+ await new Promise((res) => setTimeout(res, delay));
60
+ }
61
+ }
62
+ throw lastError;
63
+ }
64
+
65
+ // src/render/render-html.ts
66
+ function renderHtml(html, text) {
67
+ const hasHtml = html && html.trim().length > 0;
68
+ const hasText = text && text.trim().length > 0;
69
+ if (!hasHtml && !hasText) {
70
+ throw new Error("Email must contain html or text");
71
+ }
72
+ return {
73
+ html: hasHtml ? html : void 0,
74
+ text: hasText ? text : void 0
75
+ };
76
+ }
77
+
78
+ // src/render/render-react.ts
79
+ import { render } from "@react-email/render";
80
+
81
+ // src/errors.ts
82
+ var MailZenoError = class extends Error {
83
+ constructor(message, code = "UNKNOWN_ERROR", options) {
84
+ super(message);
85
+ this.name = this.constructor.name;
86
+ this.code = code;
87
+ this.statusCode = options?.statusCode;
88
+ this.cause = options?.cause;
89
+ Object.setPrototypeOf(this, new.target.prototype);
90
+ if (Error.captureStackTrace) {
91
+ Error.captureStackTrace(this, this.constructor);
92
+ }
93
+ }
94
+ };
95
+ var ValidationError = class extends MailZenoError {
96
+ constructor(message, cause) {
97
+ super(message, "VALIDATION_ERROR", { statusCode: 400, cause });
98
+ }
99
+ };
100
+ var RenderError = class extends MailZenoError {
101
+ constructor(message, cause) {
102
+ super(message, "RENDER_ERROR", { cause });
103
+ }
104
+ };
105
+ var SMTPConnectionError = class extends MailZenoError {
106
+ constructor(message, cause) {
107
+ super(message, "SMTP_CONNECTION_ERROR", { cause });
108
+ }
109
+ };
110
+ var SMTPAuthError = class extends MailZenoError {
111
+ constructor(message, cause) {
112
+ super(message, "SMTP_AUTH_ERROR", { statusCode: 401, cause });
113
+ }
114
+ };
115
+ var SMTPResponseError = class extends MailZenoError {
116
+ constructor(message, cause) {
117
+ super(message, "SMTP_RESPONSE_ERROR", { cause });
118
+ }
119
+ };
120
+ var SMTPTimeoutError = class extends MailZenoError {
121
+ constructor(message, cause) {
122
+ super(message, "SMTP_TIMEOUT_ERROR", { cause });
123
+ }
124
+ };
125
+
126
+ // src/render/render-react.ts
127
+ async function renderReact(component) {
128
+ try {
129
+ const html = await render(component);
130
+ return { html };
131
+ } catch (err) {
132
+ throw new RenderError("React email rendering failed", err);
133
+ }
134
+ }
135
+
136
+ // src/smtp/send.ts
137
+ async function sendEmail(smtp, options) {
138
+ try {
139
+ if (!options.from || !options.to || !options.subject) {
140
+ throw new ValidationError(
141
+ "Missing required fields: from, to, subject"
142
+ );
143
+ }
144
+ let html = options.html;
145
+ let text = options.text;
146
+ if (options.react) {
147
+ try {
148
+ const rendered2 = await renderReact(options.react);
149
+ html = rendered2.html;
150
+ } catch (err) {
151
+ throw new RenderError("React email rendering failed", err);
152
+ }
153
+ }
154
+ const rendered = renderHtml(html, text);
155
+ const transporter = getTransporter(smtp);
156
+ const info = await retry(
157
+ () => transporter.sendMail({
158
+ from: options.from,
159
+ to: options.to,
160
+ subject: options.subject,
161
+ html: rendered.html,
162
+ text: rendered.text
163
+ })
164
+ );
165
+ return {
166
+ success: true,
167
+ messageId: info.messageId,
168
+ accepted: info.accepted,
169
+ rejected: info.rejected,
170
+ response: info.response
171
+ };
172
+ } catch (err) {
173
+ if (err instanceof MailZenoError) {
174
+ return {
175
+ success: false,
176
+ error: err.message
177
+ };
178
+ }
179
+ if (err?.code === "EAUTH") {
180
+ return {
181
+ success: false,
182
+ error: new SMTPAuthError(
183
+ "SMTP authentication failed",
184
+ err
185
+ ).message
186
+ };
187
+ }
188
+ if (err?.code === "ETIMEDOUT") {
189
+ return {
190
+ success: false,
191
+ error: new SMTPTimeoutError(
192
+ "SMTP connection timed out",
193
+ err
194
+ ).message
195
+ };
196
+ }
197
+ if (err?.code === "ECONNECTION") {
198
+ return {
199
+ success: false,
200
+ error: new SMTPConnectionError(
201
+ "SMTP connection failed",
202
+ err
203
+ ).message
204
+ };
205
+ }
206
+ return {
207
+ success: false,
208
+ error: new SMTPResponseError(
209
+ err?.message || "SMTP sending failed",
210
+ err
211
+ ).message
212
+ };
213
+ }
214
+ }
215
+ export {
216
+ sendEmail
217
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mailzeno/core",
3
+ "version": "0.1.0",
4
+ "description": "SMTP engine for MailZeno",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean"
22
+ },
23
+ "keywords": [
24
+ "smtp",
25
+ "email",
26
+ "mailzeno",
27
+ "email-engine",
28
+ "transactional-email"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/mailzeno/mailzeno.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/mailzeno/mailzeno/issues"
36
+ },
37
+ "homepage": "https://github.com/mailzeno/mailzeno#readme",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "sideEffects": false,
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@react-email/render": "^2.0.4",
47
+ "nodemailer": "^6.9.0"
48
+ },
49
+ "devDependencies": {
50
+ "tsup": "^8.0.0",
51
+ "typescript": "^5.0.0"
52
+ }
53
+ }