mikromail 0.0.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/README.md +99 -0
- package/lib/Configuration.d.mts +41 -0
- package/lib/Configuration.d.ts +41 -0
- package/lib/Configuration.js +153 -0
- package/lib/Configuration.mjs +7 -0
- package/lib/MikroMail.d.mts +30 -0
- package/lib/MikroMail.d.ts +30 -0
- package/lib/MikroMail.js +806 -0
- package/lib/MikroMail.mjs +10 -0
- package/lib/SMTPClient.d.mts +106 -0
- package/lib/SMTPClient.d.ts +106 -0
- package/lib/SMTPClient.js +632 -0
- package/lib/SMTPClient.mjs +7 -0
- package/lib/chunk-47VXJTWV.mjs +13 -0
- package/lib/chunk-TCYL3UFZ.mjs +541 -0
- package/lib/chunk-UDLJWUFN.mjs +83 -0
- package/lib/chunk-V2NYOKWA.mjs +121 -0
- package/lib/chunk-XNGASLEZ.mjs +42 -0
- package/lib/errors/index.d.mts +8 -0
- package/lib/errors/index.d.ts +8 -0
- package/lib/errors/index.js +37 -0
- package/lib/errors/index.mjs +6 -0
- package/lib/index.d.mts +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +806 -0
- package/lib/index.mjs +10 -0
- package/lib/interfaces/index.d.mts +60 -0
- package/lib/interfaces/index.d.ts +60 -0
- package/lib/interfaces/index.js +18 -0
- package/lib/interfaces/index.mjs +0 -0
- package/lib/utils/index.d.mts +19 -0
- package/lib/utils/index.d.ts +19 -0
- package/lib/utils/index.js +110 -0
- package/lib/utils/index.mjs +12 -0
- package/package.json +56 -0
package/lib/MikroMail.js
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
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/MikroMail.ts
|
|
31
|
+
var MikroMail_exports = {};
|
|
32
|
+
__export(MikroMail_exports, {
|
|
33
|
+
MikroMail: () => MikroMail
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(MikroMail_exports);
|
|
36
|
+
|
|
37
|
+
// src/Configuration.ts
|
|
38
|
+
var import_node_fs = require("fs");
|
|
39
|
+
|
|
40
|
+
// src/errors/index.ts
|
|
41
|
+
var ValidationError = class extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super();
|
|
44
|
+
this.name = "ValidationError";
|
|
45
|
+
this.message = message;
|
|
46
|
+
this.cause = { statusCode: 400 };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/Configuration.ts
|
|
51
|
+
var Configuration = class {
|
|
52
|
+
config;
|
|
53
|
+
defaults = {
|
|
54
|
+
configFilePath: "mikromail.config.json",
|
|
55
|
+
args: []
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* @description Creates a new Configuration instance
|
|
59
|
+
* @param configFilePath Path to the configuration file (e.g., 'mikromail.config.json')
|
|
60
|
+
* @param args Command line arguments array from process.argv
|
|
61
|
+
*/
|
|
62
|
+
constructor(options) {
|
|
63
|
+
const configuration = options?.config || {};
|
|
64
|
+
const configFilePath = options?.configFilePath || this.defaults.configFilePath;
|
|
65
|
+
const args = options?.args || this.defaults.args;
|
|
66
|
+
this.config = this.create(configFilePath, args, configuration);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @description Creates a configuration object by merging defaults, config file settings,
|
|
70
|
+
* and CLI arguments (in order of increasing precedence)
|
|
71
|
+
* @param configFilePath Path to the configuration file
|
|
72
|
+
* @param args Command line arguments array
|
|
73
|
+
* @returns The merged configuration object
|
|
74
|
+
*/
|
|
75
|
+
create(configFilePath, args, configuration) {
|
|
76
|
+
const defaults = {
|
|
77
|
+
host: "",
|
|
78
|
+
user: "",
|
|
79
|
+
password: "",
|
|
80
|
+
port: 465,
|
|
81
|
+
secure: true,
|
|
82
|
+
debug: false,
|
|
83
|
+
maxRetries: 2
|
|
84
|
+
};
|
|
85
|
+
let fileConfig = {};
|
|
86
|
+
if ((0, import_node_fs.existsSync)(configFilePath)) {
|
|
87
|
+
try {
|
|
88
|
+
const fileContent = (0, import_node_fs.readFileSync)(configFilePath, "utf8");
|
|
89
|
+
fileConfig = JSON.parse(fileContent);
|
|
90
|
+
console.log(`Loaded configuration from ${configFilePath}`);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(
|
|
93
|
+
`Error reading config file: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const cliConfig = this.parseCliArgs(args);
|
|
98
|
+
return {
|
|
99
|
+
...defaults,
|
|
100
|
+
...configuration,
|
|
101
|
+
...fileConfig,
|
|
102
|
+
...cliConfig
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @description Parses command line arguments into a configuration object
|
|
107
|
+
* @param args Command line arguments array
|
|
108
|
+
* @returns Parsed CLI configuration
|
|
109
|
+
*/
|
|
110
|
+
parseCliArgs(args) {
|
|
111
|
+
const cliConfig = {};
|
|
112
|
+
for (let i = 2; i < args.length; i++) {
|
|
113
|
+
const arg = args[i];
|
|
114
|
+
switch (arg) {
|
|
115
|
+
case "--host":
|
|
116
|
+
if (i + 1 < args.length) cliConfig.host = args[++i];
|
|
117
|
+
break;
|
|
118
|
+
case "--user":
|
|
119
|
+
if (i + 1 < args.length) cliConfig.user = args[++i];
|
|
120
|
+
break;
|
|
121
|
+
case "--password":
|
|
122
|
+
if (i + 1 < args.length) cliConfig.password = args[++i];
|
|
123
|
+
break;
|
|
124
|
+
case "--port":
|
|
125
|
+
if (i + 1 < args.length) {
|
|
126
|
+
const value = Number.parseInt(args[++i], 10);
|
|
127
|
+
if (!Number.isNaN(value)) cliConfig.port = value;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case "--secure":
|
|
131
|
+
cliConfig.secure = true;
|
|
132
|
+
break;
|
|
133
|
+
case "--debug":
|
|
134
|
+
cliConfig.debug = true;
|
|
135
|
+
break;
|
|
136
|
+
case "--retries":
|
|
137
|
+
if (i + 1 < args.length) {
|
|
138
|
+
const value = Number.parseInt(args[++i], 10);
|
|
139
|
+
if (!Number.isNaN(value)) cliConfig.maxRetries = value;
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return cliConfig;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* @description Validates the configuration
|
|
148
|
+
* @throws Error if the configuration is invalid
|
|
149
|
+
*/
|
|
150
|
+
validate() {
|
|
151
|
+
if (!this.config.host) throw new ValidationError("Host value not found");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* @description Returns the complete configuration
|
|
155
|
+
* @returns The configuration object
|
|
156
|
+
*/
|
|
157
|
+
get() {
|
|
158
|
+
this.validate();
|
|
159
|
+
return this.config;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/SMTPClient.ts
|
|
164
|
+
var import_node_buffer = require("buffer");
|
|
165
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
166
|
+
var import_node_net = __toESM(require("net"));
|
|
167
|
+
var import_node_os = __toESM(require("os"));
|
|
168
|
+
var import_node_tls = __toESM(require("tls"));
|
|
169
|
+
|
|
170
|
+
// src/utils/index.ts
|
|
171
|
+
var import_node_dns = require("dns");
|
|
172
|
+
function encodeQuotedPrintable(text) {
|
|
173
|
+
let result = text.replace(/\r?\n/g, "\r\n");
|
|
174
|
+
result = result.replace(/=/g, "=3D");
|
|
175
|
+
const utf8Bytes = new TextEncoder().encode(result);
|
|
176
|
+
let encoded = "";
|
|
177
|
+
let lineLength = 0;
|
|
178
|
+
for (let i = 0; i < utf8Bytes.length; i++) {
|
|
179
|
+
const byte = utf8Bytes[i];
|
|
180
|
+
let chunk = "";
|
|
181
|
+
if (byte >= 33 && byte <= 126 && byte !== 61 || byte === 32) {
|
|
182
|
+
chunk = String.fromCharCode(byte);
|
|
183
|
+
} else if (byte === 13 || byte === 10) {
|
|
184
|
+
chunk = String.fromCharCode(byte);
|
|
185
|
+
if (byte === 10) {
|
|
186
|
+
lineLength = 0;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const hex = byte.toString(16).toUpperCase();
|
|
190
|
+
chunk = `=${hex.length < 2 ? `0${hex}` : hex}`;
|
|
191
|
+
}
|
|
192
|
+
if (lineLength + chunk.length > 75 && !(byte === 13 || byte === 10)) {
|
|
193
|
+
encoded += "=\r\n";
|
|
194
|
+
lineLength = 0;
|
|
195
|
+
}
|
|
196
|
+
encoded += chunk;
|
|
197
|
+
lineLength += chunk.length;
|
|
198
|
+
}
|
|
199
|
+
return encoded;
|
|
200
|
+
}
|
|
201
|
+
function validateEmail(email) {
|
|
202
|
+
try {
|
|
203
|
+
const [localPart, domain] = email.split("@");
|
|
204
|
+
if (!localPart || localPart.length > 64) return false;
|
|
205
|
+
if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes(".."))
|
|
206
|
+
return false;
|
|
207
|
+
if (!/^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+$/.test(localPart)) return false;
|
|
208
|
+
if (!domain || domain.length > 255) return false;
|
|
209
|
+
if (domain.startsWith("[") && domain.endsWith("]")) {
|
|
210
|
+
const ipContent = domain.slice(1, -1);
|
|
211
|
+
if (ipContent.startsWith("IPv6:")) return true;
|
|
212
|
+
const ipv4Regex = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/;
|
|
213
|
+
return ipv4Regex.test(ipContent);
|
|
214
|
+
}
|
|
215
|
+
if (domain.startsWith(".") || domain.endsWith(".") || domain.includes(".."))
|
|
216
|
+
return false;
|
|
217
|
+
const domainParts = domain.split(".");
|
|
218
|
+
if (domainParts.length < 2 || domainParts[domainParts.length - 1].length < 2)
|
|
219
|
+
return false;
|
|
220
|
+
for (const part of domainParts) {
|
|
221
|
+
if (!part || part.length > 63) return false;
|
|
222
|
+
if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
} catch (_error) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function verifyMXRecords(domain) {
|
|
230
|
+
try {
|
|
231
|
+
const records = await import_node_dns.promises.resolveMx(domain);
|
|
232
|
+
return !!records && records.length > 0;
|
|
233
|
+
} catch (_error) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function verifyEmailDomain(email) {
|
|
238
|
+
try {
|
|
239
|
+
const domain = email.split("@")[1];
|
|
240
|
+
if (!domain) return false;
|
|
241
|
+
return await verifyMXRecords(domain);
|
|
242
|
+
} catch (_error) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/SMTPClient.ts
|
|
248
|
+
var SMTPClient = class {
|
|
249
|
+
config;
|
|
250
|
+
socket;
|
|
251
|
+
connected;
|
|
252
|
+
lastCommand;
|
|
253
|
+
serverCapabilities;
|
|
254
|
+
secureMode;
|
|
255
|
+
retryCount;
|
|
256
|
+
constructor(config) {
|
|
257
|
+
this.config = {
|
|
258
|
+
host: config.host,
|
|
259
|
+
user: config.user,
|
|
260
|
+
password: config.password,
|
|
261
|
+
port: config.port ?? (config.secure ? 465 : 587),
|
|
262
|
+
secure: config.secure ?? true,
|
|
263
|
+
debug: config.debug ?? false,
|
|
264
|
+
timeout: config.timeout ?? 1e4,
|
|
265
|
+
clientName: config.clientName ?? import_node_os.default.hostname(),
|
|
266
|
+
maxRetries: config.maxRetries ?? 3,
|
|
267
|
+
retryDelay: config.retryDelay ?? 1e3,
|
|
268
|
+
skipAuthentication: config.skipAuthentication || false
|
|
269
|
+
};
|
|
270
|
+
this.socket = null;
|
|
271
|
+
this.connected = false;
|
|
272
|
+
this.lastCommand = "";
|
|
273
|
+
this.serverCapabilities = [];
|
|
274
|
+
this.secureMode = this.config.secure;
|
|
275
|
+
this.retryCount = 0;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Log debug messages if debug mode is enabled
|
|
279
|
+
*/
|
|
280
|
+
log(message, isError = false) {
|
|
281
|
+
if (this.config.debug) {
|
|
282
|
+
const prefix = isError ? "SMTP ERROR: " : "SMTP: ";
|
|
283
|
+
console.log(`${prefix}${message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Connect to the SMTP server
|
|
288
|
+
*/
|
|
289
|
+
async connect() {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
const connectionTimeout = setTimeout(() => {
|
|
292
|
+
reject(new Error(`Connection timeout after ${this.config.timeout}ms`));
|
|
293
|
+
this.socket?.destroy();
|
|
294
|
+
}, this.config.timeout);
|
|
295
|
+
try {
|
|
296
|
+
if (this.config.secure) {
|
|
297
|
+
this.createTLSConnection(connectionTimeout, resolve, reject);
|
|
298
|
+
} else {
|
|
299
|
+
this.createPlainConnection(connectionTimeout, resolve, reject);
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
clearTimeout(connectionTimeout);
|
|
303
|
+
this.log(`Failed to create socket: ${error.message}`, true);
|
|
304
|
+
reject(error);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Create a secure TLS connection
|
|
310
|
+
*/
|
|
311
|
+
createTLSConnection(connectionTimeout, resolve, reject) {
|
|
312
|
+
this.socket = import_node_tls.default.connect({
|
|
313
|
+
host: this.config.host,
|
|
314
|
+
port: this.config.port,
|
|
315
|
+
rejectUnauthorized: true,
|
|
316
|
+
// Always validate TLS certificates
|
|
317
|
+
minVersion: "TLSv1.2",
|
|
318
|
+
// Enforce TLS 1.2 or higher
|
|
319
|
+
ciphers: "HIGH:!aNULL:!MD5:!RC4"
|
|
320
|
+
});
|
|
321
|
+
this.setupSocketEventHandlers(connectionTimeout, resolve, reject);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Create a plain socket connection (for later STARTTLS upgrade)
|
|
325
|
+
*/
|
|
326
|
+
createPlainConnection(connectionTimeout, resolve, reject) {
|
|
327
|
+
this.socket = import_node_net.default.createConnection({
|
|
328
|
+
host: this.config.host,
|
|
329
|
+
port: this.config.port
|
|
330
|
+
});
|
|
331
|
+
this.setupSocketEventHandlers(connectionTimeout, resolve, reject);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Set up common socket event handlers
|
|
335
|
+
*/
|
|
336
|
+
setupSocketEventHandlers(connectionTimeout, resolve, reject) {
|
|
337
|
+
if (!this.socket) return;
|
|
338
|
+
this.socket.once("error", (err) => {
|
|
339
|
+
clearTimeout(connectionTimeout);
|
|
340
|
+
this.log(`Connection error: ${err.message}`, true);
|
|
341
|
+
reject(new Error(`SMTP connection error: ${err.message}`));
|
|
342
|
+
});
|
|
343
|
+
this.socket.once("connect", () => {
|
|
344
|
+
this.log("Connected to SMTP server");
|
|
345
|
+
clearTimeout(connectionTimeout);
|
|
346
|
+
this.socket.once("data", (data) => {
|
|
347
|
+
const greeting = data.toString().trim();
|
|
348
|
+
this.log(`Server greeting: ${greeting}`);
|
|
349
|
+
if (greeting.startsWith("220")) {
|
|
350
|
+
this.connected = true;
|
|
351
|
+
this.secureMode = this.config.secure;
|
|
352
|
+
resolve();
|
|
353
|
+
} else {
|
|
354
|
+
reject(new Error(`Unexpected server greeting: ${greeting}`));
|
|
355
|
+
this.socket.destroy();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
this.socket.once("close", (hadError) => {
|
|
360
|
+
if (this.connected) {
|
|
361
|
+
this.log(`Connection closed${hadError ? " with error" : ""}`);
|
|
362
|
+
} else {
|
|
363
|
+
clearTimeout(connectionTimeout);
|
|
364
|
+
reject(new Error("Connection closed before initialization completed"));
|
|
365
|
+
}
|
|
366
|
+
this.connected = false;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Upgrade connection to TLS using STARTTLS
|
|
371
|
+
*/
|
|
372
|
+
async upgradeToTLS() {
|
|
373
|
+
if (!this.socket || this.secureMode) return;
|
|
374
|
+
return new Promise((resolve, reject) => {
|
|
375
|
+
const plainSocket = this.socket;
|
|
376
|
+
const tlsOptions = {
|
|
377
|
+
socket: plainSocket,
|
|
378
|
+
host: this.config.host,
|
|
379
|
+
rejectUnauthorized: true,
|
|
380
|
+
minVersion: "TLSv1.2",
|
|
381
|
+
ciphers: "HIGH:!aNULL:!MD5:!RC4"
|
|
382
|
+
};
|
|
383
|
+
const tlsSocket = import_node_tls.default.connect(tlsOptions);
|
|
384
|
+
tlsSocket.once("error", (err) => {
|
|
385
|
+
this.log(`TLS upgrade error: ${err.message}`, true);
|
|
386
|
+
reject(new Error(`STARTTLS error: ${err.message}`));
|
|
387
|
+
});
|
|
388
|
+
tlsSocket.once("secureConnect", () => {
|
|
389
|
+
this.log("Connection upgraded to TLS");
|
|
390
|
+
if (tlsSocket.authorized) {
|
|
391
|
+
this.socket = tlsSocket;
|
|
392
|
+
this.secureMode = true;
|
|
393
|
+
resolve();
|
|
394
|
+
} else {
|
|
395
|
+
reject(
|
|
396
|
+
new Error(
|
|
397
|
+
`TLS certificate verification failed: ${tlsSocket.authorizationError}`
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Send an SMTP command and await response
|
|
406
|
+
*/
|
|
407
|
+
async sendCommand(command, expectedCode, timeout = this.config.timeout) {
|
|
408
|
+
if (!this.socket || !this.connected) {
|
|
409
|
+
throw new Error("Not connected to SMTP server");
|
|
410
|
+
}
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const commandTimeout = setTimeout(() => {
|
|
413
|
+
this.socket?.removeListener("data", onData);
|
|
414
|
+
reject(new Error(`Command timeout after ${timeout}ms: ${command}`));
|
|
415
|
+
}, timeout);
|
|
416
|
+
let responseData = "";
|
|
417
|
+
const onData = (chunk) => {
|
|
418
|
+
responseData += chunk.toString();
|
|
419
|
+
const lines = responseData.split("\r\n");
|
|
420
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
421
|
+
const lastLine = lines[lines.length - 2] || "";
|
|
422
|
+
const matches = /^(\d{3})(.?)/.exec(lastLine);
|
|
423
|
+
if (matches?.[1] && matches[2] !== "-") {
|
|
424
|
+
this.socket?.removeListener("data", onData);
|
|
425
|
+
clearTimeout(commandTimeout);
|
|
426
|
+
this.log(`SMTP Response: ${responseData.trim()}`);
|
|
427
|
+
if (matches[1] === expectedCode.toString()) {
|
|
428
|
+
resolve(responseData.trim());
|
|
429
|
+
} else {
|
|
430
|
+
reject(new Error(`SMTP Error: ${responseData.trim()}`));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
this.socket.on("data", onData);
|
|
436
|
+
if (command.startsWith("AUTH PLAIN") || command.startsWith("AUTH LOGIN") || this.lastCommand === "AUTH LOGIN" && !command.startsWith("AUTH")) {
|
|
437
|
+
this.log("SMTP Command: [Credentials hidden]");
|
|
438
|
+
} else {
|
|
439
|
+
this.log(`SMTP Command: ${command}`);
|
|
440
|
+
}
|
|
441
|
+
this.lastCommand = command;
|
|
442
|
+
this.socket.write(`${command}\r
|
|
443
|
+
`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Parse EHLO response to determine server capabilities
|
|
448
|
+
*/
|
|
449
|
+
parseCapabilities(ehloResponse) {
|
|
450
|
+
const lines = ehloResponse.split("\r\n");
|
|
451
|
+
this.serverCapabilities = [];
|
|
452
|
+
for (let i = 1; i < lines.length; i++) {
|
|
453
|
+
const line = lines[i];
|
|
454
|
+
if (line.match(/^\d{3}/) && line.charAt(3) === " ") {
|
|
455
|
+
const capability = line.substr(4).toUpperCase();
|
|
456
|
+
this.serverCapabilities.push(capability);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this.log(`Server capabilities: ${this.serverCapabilities.join(", ")}`);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Determine the best authentication method supported by the server
|
|
463
|
+
*/
|
|
464
|
+
getBestAuthMethod() {
|
|
465
|
+
const capabilities = this.serverCapabilities.map(
|
|
466
|
+
(cap) => cap.split(" ")[0]
|
|
467
|
+
);
|
|
468
|
+
if (capabilities.includes("AUTH")) {
|
|
469
|
+
const authLine = this.serverCapabilities.find(
|
|
470
|
+
(cap) => cap.startsWith("AUTH ")
|
|
471
|
+
);
|
|
472
|
+
if (authLine) {
|
|
473
|
+
const methods = authLine.split(" ").slice(1);
|
|
474
|
+
if (methods.includes("CRAM-MD5")) return "CRAM-MD5";
|
|
475
|
+
if (methods.includes("LOGIN")) return "LOGIN";
|
|
476
|
+
if (methods.includes("PLAIN")) return "PLAIN";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return "PLAIN";
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Authenticate with the SMTP server using the best available method
|
|
483
|
+
*/
|
|
484
|
+
async authenticate() {
|
|
485
|
+
const authMethod = this.getBestAuthMethod();
|
|
486
|
+
switch (authMethod) {
|
|
487
|
+
case "CRAM-MD5":
|
|
488
|
+
await this.authenticateCramMD5();
|
|
489
|
+
break;
|
|
490
|
+
case "LOGIN":
|
|
491
|
+
await this.authenticateLogin();
|
|
492
|
+
break;
|
|
493
|
+
default:
|
|
494
|
+
await this.authenticatePlain();
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Authenticate using PLAIN method
|
|
500
|
+
*/
|
|
501
|
+
async authenticatePlain() {
|
|
502
|
+
const authPlain = import_node_buffer.Buffer.from(
|
|
503
|
+
`\0${this.config.user}\0${this.config.password}`
|
|
504
|
+
).toString("base64");
|
|
505
|
+
await this.sendCommand(`AUTH PLAIN ${authPlain}`, 235);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Authenticate using LOGIN method
|
|
509
|
+
*/
|
|
510
|
+
async authenticateLogin() {
|
|
511
|
+
await this.sendCommand("AUTH LOGIN", 334);
|
|
512
|
+
await this.sendCommand(
|
|
513
|
+
import_node_buffer.Buffer.from(this.config.user).toString("base64"),
|
|
514
|
+
334
|
|
515
|
+
);
|
|
516
|
+
await this.sendCommand(
|
|
517
|
+
import_node_buffer.Buffer.from(this.config.password).toString("base64"),
|
|
518
|
+
235
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Authenticate using CRAM-MD5 method
|
|
523
|
+
*/
|
|
524
|
+
async authenticateCramMD5() {
|
|
525
|
+
const response = await this.sendCommand("AUTH CRAM-MD5", 334);
|
|
526
|
+
const challenge = import_node_buffer.Buffer.from(response.substr(4), "base64").toString(
|
|
527
|
+
"utf8"
|
|
528
|
+
);
|
|
529
|
+
const hmac = import_node_crypto.default.createHmac("md5", this.config.password);
|
|
530
|
+
hmac.update(challenge);
|
|
531
|
+
const digest = hmac.digest("hex");
|
|
532
|
+
const cramResponse = `${this.config.user} ${digest}`;
|
|
533
|
+
const encodedResponse = import_node_buffer.Buffer.from(cramResponse).toString("base64");
|
|
534
|
+
await this.sendCommand(encodedResponse, 235);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Generate a unique message ID
|
|
538
|
+
*/
|
|
539
|
+
generateMessageId() {
|
|
540
|
+
const random = import_node_crypto.default.randomBytes(16).toString("hex");
|
|
541
|
+
const domain = this.config.user.split("@")[1] || "localhost";
|
|
542
|
+
return `<${random}@${domain}>`;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Generate MIME boundary for multipart messages
|
|
546
|
+
*/
|
|
547
|
+
generateBoundary() {
|
|
548
|
+
return `----=_NextPart_${import_node_crypto.default.randomBytes(12).toString("hex")}`;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Encode string according to RFC 2047 for headers with non-ASCII characters
|
|
552
|
+
*/
|
|
553
|
+
encodeHeaderValue(value) {
|
|
554
|
+
if (/^[\x00-\x7F]*$/.test(value)) {
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
return `=?UTF-8?Q?${// biome-ignore lint/suspicious/noControlCharactersInRegex: <explanation>
|
|
558
|
+
value.replace(/[^\x00-\x7F]/g, (c) => {
|
|
559
|
+
const hex = c.charCodeAt(0).toString(16).toUpperCase();
|
|
560
|
+
return `=${hex.length < 2 ? `0${hex}` : hex}`;
|
|
561
|
+
})}?=`;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Sanitize and encode header value to prevent injection and handle internationalization
|
|
565
|
+
*/
|
|
566
|
+
sanitizeHeader(value) {
|
|
567
|
+
const sanitized = value.replace(/[\r\n]+/g, " ");
|
|
568
|
+
return this.encodeHeaderValue(sanitized);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Create email headers with proper sanitization
|
|
572
|
+
*/
|
|
573
|
+
createEmailHeaders(options) {
|
|
574
|
+
const messageId = this.generateMessageId();
|
|
575
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
576
|
+
const from = options.from || this.config.user;
|
|
577
|
+
const { to } = options;
|
|
578
|
+
const headers = [
|
|
579
|
+
`From: ${this.sanitizeHeader(from)}`,
|
|
580
|
+
`To: ${this.sanitizeHeader(to)}`,
|
|
581
|
+
`Subject: ${this.sanitizeHeader(options.subject)}`,
|
|
582
|
+
`Message-ID: ${messageId}`,
|
|
583
|
+
`Date: ${date}`,
|
|
584
|
+
"MIME-Version: 1.0"
|
|
585
|
+
];
|
|
586
|
+
if (options.cc) {
|
|
587
|
+
const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
|
|
588
|
+
headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
|
|
589
|
+
}
|
|
590
|
+
if (options.replyTo) {
|
|
591
|
+
headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
|
|
592
|
+
}
|
|
593
|
+
if (options.headers) {
|
|
594
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
595
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
|
|
596
|
+
if (/^(from|to|cc|bcc|subject|date|message-id)$/i.test(name)) continue;
|
|
597
|
+
headers.push(`${name}: ${this.sanitizeHeader(value)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return headers;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Create a multipart email with text and HTML parts
|
|
604
|
+
*/
|
|
605
|
+
createMultipartEmail(options) {
|
|
606
|
+
const { text, html } = options;
|
|
607
|
+
const headers = this.createEmailHeaders(options);
|
|
608
|
+
const boundary = this.generateBoundary();
|
|
609
|
+
if (html && text) {
|
|
610
|
+
headers.push(
|
|
611
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`
|
|
612
|
+
);
|
|
613
|
+
return `${headers.join("\r\n")}\r
|
|
614
|
+
\r
|
|
615
|
+
--${boundary}\r
|
|
616
|
+
Content-Type: text/plain; charset=utf-8\r
|
|
617
|
+
Content-Transfer-Encoding: quoted-printable\r
|
|
618
|
+
\r
|
|
619
|
+
${encodeQuotedPrintable(text || "")}\r
|
|
620
|
+
\r
|
|
621
|
+
--${boundary}\r
|
|
622
|
+
Content-Type: text/html; charset=utf-8\r
|
|
623
|
+
Content-Transfer-Encoding: quoted-printable\r
|
|
624
|
+
\r
|
|
625
|
+
${encodeQuotedPrintable(html || "")}\r
|
|
626
|
+
\r
|
|
627
|
+
--${boundary}--\r
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
if (html) {
|
|
631
|
+
headers.push("Content-Type: text/html; charset=utf-8");
|
|
632
|
+
headers.push("Content-Transfer-Encoding: quoted-printable");
|
|
633
|
+
return `${headers.join("\r\n")}\r
|
|
634
|
+
\r
|
|
635
|
+
${encodeQuotedPrintable(html)}`;
|
|
636
|
+
}
|
|
637
|
+
headers.push("Content-Type: text/plain; charset=utf-8");
|
|
638
|
+
headers.push("Content-Transfer-Encoding: quoted-printable");
|
|
639
|
+
return `${headers.join("\r\n")}\r
|
|
640
|
+
\r
|
|
641
|
+
${encodeQuotedPrintable(text || "")}`;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Perform full SMTP handshake, including STARTTLS if needed
|
|
645
|
+
*/
|
|
646
|
+
async smtpHandshake() {
|
|
647
|
+
const ehloResponse = await this.sendCommand(
|
|
648
|
+
`EHLO ${this.config.clientName}`,
|
|
649
|
+
250
|
|
650
|
+
);
|
|
651
|
+
this.parseCapabilities(ehloResponse);
|
|
652
|
+
if (!this.secureMode && this.serverCapabilities.includes("STARTTLS")) {
|
|
653
|
+
await this.sendCommand("STARTTLS", 220);
|
|
654
|
+
await this.upgradeToTLS();
|
|
655
|
+
const secureEhloResponse = await this.sendCommand(
|
|
656
|
+
`EHLO ${this.config.clientName}`,
|
|
657
|
+
250
|
|
658
|
+
);
|
|
659
|
+
this.parseCapabilities(secureEhloResponse);
|
|
660
|
+
}
|
|
661
|
+
if (!this.config.skipAuthentication) {
|
|
662
|
+
await this.authenticate();
|
|
663
|
+
} else {
|
|
664
|
+
this.log("Authentication skipped (testing mode)");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Send an email with retry capability
|
|
669
|
+
*/
|
|
670
|
+
async sendEmail(options) {
|
|
671
|
+
const from = options.from || this.config.user;
|
|
672
|
+
const { to, subject } = options;
|
|
673
|
+
const text = options.text || "";
|
|
674
|
+
const html = options.html || "";
|
|
675
|
+
if (!from || !to || !subject || !text && !html) {
|
|
676
|
+
return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: "Missing required email parameters (from, to, subject, and either text or html)"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (!validateEmail(from) || !validateEmail(to)) {
|
|
682
|
+
return {
|
|
683
|
+
success: false,
|
|
684
|
+
error: "Invalid email address format"
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
|
|
688
|
+
try {
|
|
689
|
+
if (this.retryCount > 0) {
|
|
690
|
+
this.log(
|
|
691
|
+
`Retrying email send (attempt ${this.retryCount} of ${this.config.maxRetries})...`
|
|
692
|
+
);
|
|
693
|
+
await new Promise(
|
|
694
|
+
(resolve) => setTimeout(resolve, this.config.retryDelay)
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (!this.connected) {
|
|
698
|
+
await this.connect();
|
|
699
|
+
await this.smtpHandshake();
|
|
700
|
+
}
|
|
701
|
+
await this.sendCommand(`MAIL FROM:<${from}>`, 250);
|
|
702
|
+
await this.sendCommand(`RCPT TO:<${to}>`, 250);
|
|
703
|
+
if (options.cc) {
|
|
704
|
+
const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
|
|
705
|
+
for (const cc of ccList) {
|
|
706
|
+
if (validateEmail(cc)) {
|
|
707
|
+
await this.sendCommand(`RCPT TO:<${cc}>`, 250);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (options.bcc) {
|
|
712
|
+
const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
|
|
713
|
+
for (const bcc of bccList) {
|
|
714
|
+
if (validateEmail(bcc)) {
|
|
715
|
+
await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
await this.sendCommand("DATA", 354);
|
|
720
|
+
const emailContent = this.createMultipartEmail(options);
|
|
721
|
+
await this.sendCommand(`${emailContent}\r
|
|
722
|
+
.`, 250);
|
|
723
|
+
const messageIdMatch = /Message-ID: (.*)/i.exec(emailContent);
|
|
724
|
+
const messageId = messageIdMatch ? messageIdMatch[1].trim() : void 0;
|
|
725
|
+
return {
|
|
726
|
+
success: true,
|
|
727
|
+
messageId,
|
|
728
|
+
message: "Email sent successfully"
|
|
729
|
+
};
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const errorMessage = error.message;
|
|
732
|
+
this.log(`Error sending email: ${errorMessage}`, true);
|
|
733
|
+
const isPermanentError = errorMessage.includes("5.") || // 5xx SMTP errors are permanent
|
|
734
|
+
errorMessage.includes("Authentication failed") || errorMessage.includes("certificate");
|
|
735
|
+
if (isPermanentError || this.retryCount >= this.config.maxRetries) {
|
|
736
|
+
return {
|
|
737
|
+
success: false,
|
|
738
|
+
error: errorMessage
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
if (this.connected) {
|
|
743
|
+
await this.sendCommand("RSET", 250);
|
|
744
|
+
}
|
|
745
|
+
} catch (_error) {
|
|
746
|
+
}
|
|
747
|
+
this.socket?.end();
|
|
748
|
+
this.connected = false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
success: false,
|
|
753
|
+
error: "Maximum retry count exceeded"
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Close the connection gracefully
|
|
758
|
+
*/
|
|
759
|
+
async close() {
|
|
760
|
+
try {
|
|
761
|
+
if (this.connected) {
|
|
762
|
+
await this.sendCommand("QUIT", 221);
|
|
763
|
+
}
|
|
764
|
+
} catch (e) {
|
|
765
|
+
this.log(`Error during QUIT: ${e.message}`, true);
|
|
766
|
+
} finally {
|
|
767
|
+
if (this.socket) {
|
|
768
|
+
this.socket.end();
|
|
769
|
+
this.connected = false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// src/MikroMail.ts
|
|
776
|
+
var MikroMail = class {
|
|
777
|
+
smtpClient;
|
|
778
|
+
constructor(options) {
|
|
779
|
+
const config = new Configuration(options).get();
|
|
780
|
+
const smtpClient = new SMTPClient(config);
|
|
781
|
+
this.smtpClient = smtpClient;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Sends an email to valid domains.
|
|
785
|
+
*/
|
|
786
|
+
async send(emailOptions) {
|
|
787
|
+
try {
|
|
788
|
+
const hasMXRecords = await verifyEmailDomain(emailOptions.to);
|
|
789
|
+
if (!hasMXRecords)
|
|
790
|
+
console.error("Warning: No MX records found for recipient domain");
|
|
791
|
+
const result = await this.smtpClient.sendEmail(emailOptions);
|
|
792
|
+
if (result.success) console.log(`Message ID: ${result.messageId}`);
|
|
793
|
+
else console.error(`Failed to send email: ${result.error}`);
|
|
794
|
+
await this.smtpClient.close();
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.error(
|
|
797
|
+
"Error in email sending process:",
|
|
798
|
+
error.message
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
804
|
+
0 && (module.exports = {
|
|
805
|
+
MikroMail
|
|
806
|
+
});
|