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.
@@ -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
+ });