@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 +68 -0
- package/dist/index.cjs +254 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +217 -0
- package/package.json +53 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|