mikromail 0.0.6 → 1.0.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 CHANGED
@@ -3,6 +3,7 @@
3
3
  **Lightweight replacement for Nodemailer, supporting HTML, international symbols, and more**.
4
4
 
5
5
  ![Build Status](https://github.com/mikaelvesavuori/mikromail/workflows/main/badge.svg)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
8
  ---
8
9
 
@@ -14,7 +15,7 @@
14
15
 
15
16
  ## Usage
16
17
 
17
- ### Basic importing and usage
18
+ ### Quick Start
18
19
 
19
20
  ```typescript
20
21
  import { MikroMail } from 'MikroMail';
@@ -29,7 +30,7 @@ const emailOptions = {
29
30
  from: 'me@mydomain.com',
30
31
  subject: 'Test Email',
31
32
  text: 'Hello!',
32
- to: 'you@yourdomain.com'
33
+ to: 'you@yourdomain.com' // You can also send to multiple recipients: ['sam@acmecorp.cloud', 'sammy@acmecorp.cloud']
33
34
  };
34
35
 
35
36
  await new MikroMail({ config }).send(emailOptions);
@@ -88,6 +89,49 @@ const emailOptions = {
88
89
  await new MikroMail({ config }).send(emailOptions);
89
90
  ```
90
91
 
92
+ ## Provider-specific configurations
93
+
94
+ ### Proton Mail
95
+
96
+ Proton Mail works reliably with port 465 and implicit TLS:
97
+
98
+ ```typescript
99
+ const config = {
100
+ user: 'your-email@proton.me',
101
+ password: 'YOUR_APP_PASSWORD',
102
+ host: 'smtp.protonmail.ch',
103
+ port: 465,
104
+ secure: true
105
+ };
106
+ ```
107
+
108
+ Note: Port 587 with STARTTLS (`secure: false`) may have connectivity issues with some Proton Mail configurations. Use port 465 for the most reliable connection.
109
+
110
+ ### Test providers (Mailtrap, etc.)
111
+
112
+ Some test SMTP providers use long random IDs instead of email addresses. To support these providers, use the `skipEmailValidation` and `skipMXRecordCheck` options:
113
+
114
+ ```typescript
115
+ const config = {
116
+ user: 'your-mailtrap-username',
117
+ password: 'your-mailtrap-password',
118
+ host: 'smtp.mailtrap.io',
119
+ port: 587,
120
+ secure: false,
121
+ skipEmailValidation: true,
122
+ skipMXRecordCheck: true
123
+ };
124
+
125
+ const emailOptions = {
126
+ from: 'sender@example.com',
127
+ subject: 'Test Email',
128
+ text: 'Hello!',
129
+ to: 'a1b2c3d4e5f6g7' // Long random ID used by test providers
130
+ };
131
+
132
+ await new MikroMail({ config }).send(emailOptions);
133
+ ```
134
+
91
135
  ## Testing
92
136
 
93
137
  Some of the tests require faking an SMTP server. Here we use [Mailpit](https://github.com/axllent/mailpit), which will run a server on `http://localhost:8025`.
@@ -98,4 +142,4 @@ Some of the tests require faking an SMTP server. Here we use [Mailpit](https://g
98
142
 
99
143
  ## License
100
144
 
101
- MIT
145
+ MIT. See the `LICENSE` file.
@@ -67,7 +67,9 @@ var Configuration = class {
67
67
  port: 465,
68
68
  secure: true,
69
69
  debug: false,
70
- maxRetries: 2
70
+ maxRetries: 2,
71
+ skipEmailValidation: false,
72
+ skipMXRecordCheck: false
71
73
  };
72
74
  let fileConfig = {};
73
75
  if ((0, import_node_fs.existsSync)(configFilePath)) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Configuration
3
- } from "./chunk-YVXB6HCK.mjs";
3
+ } from "./chunk-ULVDQIAT.mjs";
4
4
  import "./chunk-47VXJTWV.mjs";
5
5
  export {
6
6
  Configuration
@@ -20,6 +20,7 @@ import { ConfigurationOptions, EmailOptions } from './interfaces/index.mjs';
20
20
  */
21
21
  declare class MikroMail {
22
22
  private readonly smtpClient;
23
+ private readonly config;
23
24
  constructor(options?: ConfigurationOptions);
24
25
  /**
25
26
  * Sends an email to valid domains.
@@ -20,6 +20,7 @@ import { ConfigurationOptions, EmailOptions } from './interfaces/index.js';
20
20
  */
21
21
  declare class MikroMail {
22
22
  private readonly smtpClient;
23
+ private readonly config;
23
24
  constructor(options?: ConfigurationOptions);
24
25
  /**
25
26
  * Sends an email to valid domains.
package/lib/MikroMail.js CHANGED
@@ -79,7 +79,9 @@ var Configuration = class {
79
79
  port: 465,
80
80
  secure: true,
81
81
  debug: false,
82
- maxRetries: 2
82
+ maxRetries: 2,
83
+ skipEmailValidation: false,
84
+ skipMXRecordCheck: false
83
85
  };
84
86
  let fileConfig = {};
85
87
  if ((0, import_node_fs.existsSync)(configFilePath)) {
@@ -189,7 +191,7 @@ function validateEmail(email) {
189
191
  return false;
190
192
  for (const part of domainParts) {
191
193
  if (!part || part.length > 63) return false;
192
- if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
194
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(part)) return false;
193
195
  }
194
196
  return true;
195
197
  } catch (_error) {
@@ -237,7 +239,9 @@ var SMTPClient = class {
237
239
  clientName: config.clientName ?? import_node_os.default.hostname(),
238
240
  maxRetries: config.maxRetries ?? 3,
239
241
  retryDelay: config.retryDelay ?? 1e3,
240
- skipAuthentication: config.skipAuthentication || false
242
+ skipAuthentication: config.skipAuthentication || false,
243
+ skipEmailValidation: config.skipEmailValidation || false,
244
+ skipMXRecordCheck: config.skipMXRecordCheck || false
241
245
  };
242
246
  this.socket = null;
243
247
  this.connected = false;
@@ -546,10 +550,10 @@ var SMTPClient = class {
546
550
  const messageId = this.generateMessageId();
547
551
  const date = (/* @__PURE__ */ new Date()).toUTCString();
548
552
  const from = options.from || this.config.user;
549
- const { to } = options;
553
+ const recipients = Array.isArray(options.to) ? options.to.join(", ") : options.to;
550
554
  const headers = [
551
555
  `From: ${this.sanitizeHeader(from)}`,
552
- `To: ${this.sanitizeHeader(to)}`,
556
+ `To: ${this.sanitizeHeader(recipients)}`,
553
557
  `Subject: ${this.sanitizeHeader(options.subject)}`,
554
558
  `Message-ID: ${messageId}`,
555
559
  `Date: ${date}`,
@@ -559,9 +563,8 @@ var SMTPClient = class {
559
563
  const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
560
564
  headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
561
565
  }
562
- if (options.replyTo) {
566
+ if (options.replyTo)
563
567
  headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
564
- }
565
568
  if (options.headers) {
566
569
  for (const [name, value] of Object.entries(options.headers)) {
567
570
  if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
@@ -643,11 +646,24 @@ ${text || ""}`;
643
646
  error: "Missing required email parameters (from, to, subject, and either text or html)"
644
647
  };
645
648
  }
646
- if (!validateEmail(from) || !validateEmail(to)) {
647
- return {
648
- success: false,
649
- error: "Invalid email address format"
650
- };
649
+ const recipients = Array.isArray(options.to) ? options.to : [options.to];
650
+ if (!this.config.skipEmailValidation) {
651
+ if (!validateEmail(from)) {
652
+ return {
653
+ success: false,
654
+ error: "Invalid email address format"
655
+ };
656
+ }
657
+ for (const recipient of recipients) {
658
+ if (!validateEmail(recipient)) {
659
+ return {
660
+ success: false,
661
+ error: `Invalid recipient email address format: ${recipient}`
662
+ };
663
+ }
664
+ }
665
+ } else {
666
+ this.log("Email validation skipped (testing mode)");
651
667
  }
652
668
  for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
653
669
  try {
@@ -664,21 +680,20 @@ ${text || ""}`;
664
680
  await this.smtpHandshake();
665
681
  }
666
682
  await this.sendCommand(`MAIL FROM:<${from}>`, 250);
667
- await this.sendCommand(`RCPT TO:<${to}>`, 250);
683
+ for (const recipient of recipients)
684
+ await this.sendCommand(`RCPT TO:<${recipient}>`, 250);
668
685
  if (options.cc) {
669
686
  const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
670
687
  for (const cc of ccList) {
671
- if (validateEmail(cc)) {
688
+ if (validateEmail(cc))
672
689
  await this.sendCommand(`RCPT TO:<${cc}>`, 250);
673
- }
674
690
  }
675
691
  }
676
692
  if (options.bcc) {
677
693
  const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
678
694
  for (const bcc of bccList) {
679
- if (validateEmail(bcc)) {
695
+ if (validateEmail(bcc))
680
696
  await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
681
- }
682
697
  }
683
698
  }
684
699
  await this.sendCommand("DATA", 354);
@@ -747,19 +762,28 @@ ${text || ""}`;
747
762
  // src/MikroMail.ts
748
763
  var MikroMail = class {
749
764
  smtpClient;
765
+ config;
750
766
  constructor(options) {
751
767
  const config = new Configuration(options).get();
752
768
  const smtpClient = new SMTPClient(config);
753
769
  this.smtpClient = smtpClient;
770
+ this.config = config;
754
771
  }
755
772
  /**
756
773
  * Sends an email to valid domains.
757
774
  */
758
775
  async send(emailOptions) {
759
776
  try {
760
- const hasMXRecords = await verifyEmailDomain(emailOptions.to);
761
- if (!hasMXRecords)
762
- console.error("Warning: No MX records found for recipient domain");
777
+ const recipients = Array.isArray(emailOptions.to) ? emailOptions.to : [emailOptions.to];
778
+ if (!this.config.skipMXRecordCheck) {
779
+ for (const recipient of recipients) {
780
+ const hasMXRecords = await verifyEmailDomain(recipient);
781
+ if (!hasMXRecords)
782
+ console.error(
783
+ `Warning: No MX records found for recipient domain: ${recipient}`
784
+ );
785
+ }
786
+ }
763
787
  const result = await this.smtpClient.sendEmail(emailOptions);
764
788
  if (result.success) console.log(`Message ID: ${result.messageId}`);
765
789
  else console.error(`Failed to send email: ${result.error}`);
package/lib/MikroMail.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  MikroMail
3
- } from "./chunk-FP3YCQBU.mjs";
4
- import "./chunk-YVXB6HCK.mjs";
5
- import "./chunk-HZ3BDGTE.mjs";
3
+ } from "./chunk-IYCQK5YZ.mjs";
4
+ import "./chunk-ULVDQIAT.mjs";
5
+ import "./chunk-QCFOUQRH.mjs";
6
6
  import "./chunk-47VXJTWV.mjs";
7
- import "./chunk-UDLJWUFN.mjs";
7
+ import "./chunk-5BK3VY6I.mjs";
8
8
  export {
9
9
  MikroMail
10
10
  };
package/lib/SMTPClient.js CHANGED
@@ -62,7 +62,7 @@ function validateEmail(email) {
62
62
  return false;
63
63
  for (const part of domainParts) {
64
64
  if (!part || part.length > 63) return false;
65
- if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
65
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(part)) return false;
66
66
  }
67
67
  return true;
68
68
  } catch (_error) {
@@ -93,7 +93,9 @@ var SMTPClient = class {
93
93
  clientName: config.clientName ?? import_node_os.default.hostname(),
94
94
  maxRetries: config.maxRetries ?? 3,
95
95
  retryDelay: config.retryDelay ?? 1e3,
96
- skipAuthentication: config.skipAuthentication || false
96
+ skipAuthentication: config.skipAuthentication || false,
97
+ skipEmailValidation: config.skipEmailValidation || false,
98
+ skipMXRecordCheck: config.skipMXRecordCheck || false
97
99
  };
98
100
  this.socket = null;
99
101
  this.connected = false;
@@ -402,10 +404,10 @@ var SMTPClient = class {
402
404
  const messageId = this.generateMessageId();
403
405
  const date = (/* @__PURE__ */ new Date()).toUTCString();
404
406
  const from = options.from || this.config.user;
405
- const { to } = options;
407
+ const recipients = Array.isArray(options.to) ? options.to.join(", ") : options.to;
406
408
  const headers = [
407
409
  `From: ${this.sanitizeHeader(from)}`,
408
- `To: ${this.sanitizeHeader(to)}`,
410
+ `To: ${this.sanitizeHeader(recipients)}`,
409
411
  `Subject: ${this.sanitizeHeader(options.subject)}`,
410
412
  `Message-ID: ${messageId}`,
411
413
  `Date: ${date}`,
@@ -415,9 +417,8 @@ var SMTPClient = class {
415
417
  const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
416
418
  headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
417
419
  }
418
- if (options.replyTo) {
420
+ if (options.replyTo)
419
421
  headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
420
- }
421
422
  if (options.headers) {
422
423
  for (const [name, value] of Object.entries(options.headers)) {
423
424
  if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
@@ -499,11 +500,24 @@ ${text || ""}`;
499
500
  error: "Missing required email parameters (from, to, subject, and either text or html)"
500
501
  };
501
502
  }
502
- if (!validateEmail(from) || !validateEmail(to)) {
503
- return {
504
- success: false,
505
- error: "Invalid email address format"
506
- };
503
+ const recipients = Array.isArray(options.to) ? options.to : [options.to];
504
+ if (!this.config.skipEmailValidation) {
505
+ if (!validateEmail(from)) {
506
+ return {
507
+ success: false,
508
+ error: "Invalid email address format"
509
+ };
510
+ }
511
+ for (const recipient of recipients) {
512
+ if (!validateEmail(recipient)) {
513
+ return {
514
+ success: false,
515
+ error: `Invalid recipient email address format: ${recipient}`
516
+ };
517
+ }
518
+ }
519
+ } else {
520
+ this.log("Email validation skipped (testing mode)");
507
521
  }
508
522
  for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
509
523
  try {
@@ -520,21 +534,20 @@ ${text || ""}`;
520
534
  await this.smtpHandshake();
521
535
  }
522
536
  await this.sendCommand(`MAIL FROM:<${from}>`, 250);
523
- await this.sendCommand(`RCPT TO:<${to}>`, 250);
537
+ for (const recipient of recipients)
538
+ await this.sendCommand(`RCPT TO:<${recipient}>`, 250);
524
539
  if (options.cc) {
525
540
  const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
526
541
  for (const cc of ccList) {
527
- if (validateEmail(cc)) {
542
+ if (validateEmail(cc))
528
543
  await this.sendCommand(`RCPT TO:<${cc}>`, 250);
529
- }
530
544
  }
531
545
  }
532
546
  if (options.bcc) {
533
547
  const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
534
548
  for (const bcc of bccList) {
535
- if (validateEmail(bcc)) {
549
+ if (validateEmail(bcc))
536
550
  await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
537
- }
538
551
  }
539
552
  }
540
553
  await this.sendCommand("DATA", 354);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SMTPClient
3
- } from "./chunk-HZ3BDGTE.mjs";
4
- import "./chunk-UDLJWUFN.mjs";
3
+ } from "./chunk-QCFOUQRH.mjs";
4
+ import "./chunk-5BK3VY6I.mjs";
5
5
  export {
6
6
  SMTPClient
7
7
  };
@@ -1,5 +1,5 @@
1
1
  // src/utils/index.ts
2
- import { promises as dnsPromises } from "node:dns";
2
+ import { promises as dnsPromises } from "dns";
3
3
  function encodeQuotedPrintable(text) {
4
4
  let result = text.replace(/\r?\n/g, "\r\n");
5
5
  result = result.replace(/=/g, "=3D");
@@ -50,7 +50,7 @@ function validateEmail(email) {
50
50
  return false;
51
51
  for (const part of domainParts) {
52
52
  if (!part || part.length > 63) return false;
53
- if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
53
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(part)) return false;
54
54
  }
55
55
  return true;
56
56
  } catch (_error) {
@@ -1,29 +1,38 @@
1
1
  import {
2
2
  Configuration
3
- } from "./chunk-YVXB6HCK.mjs";
3
+ } from "./chunk-ULVDQIAT.mjs";
4
4
  import {
5
5
  SMTPClient
6
- } from "./chunk-HZ3BDGTE.mjs";
6
+ } from "./chunk-QCFOUQRH.mjs";
7
7
  import {
8
8
  verifyEmailDomain
9
- } from "./chunk-UDLJWUFN.mjs";
9
+ } from "./chunk-5BK3VY6I.mjs";
10
10
 
11
11
  // src/MikroMail.ts
12
12
  var MikroMail = class {
13
13
  smtpClient;
14
+ config;
14
15
  constructor(options) {
15
16
  const config = new Configuration(options).get();
16
17
  const smtpClient = new SMTPClient(config);
17
18
  this.smtpClient = smtpClient;
19
+ this.config = config;
18
20
  }
19
21
  /**
20
22
  * Sends an email to valid domains.
21
23
  */
22
24
  async send(emailOptions) {
23
25
  try {
24
- const hasMXRecords = await verifyEmailDomain(emailOptions.to);
25
- if (!hasMXRecords)
26
- console.error("Warning: No MX records found for recipient domain");
26
+ const recipients = Array.isArray(emailOptions.to) ? emailOptions.to : [emailOptions.to];
27
+ if (!this.config.skipMXRecordCheck) {
28
+ for (const recipient of recipients) {
29
+ const hasMXRecords = await verifyEmailDomain(recipient);
30
+ if (!hasMXRecords)
31
+ console.error(
32
+ `Warning: No MX records found for recipient domain: ${recipient}`
33
+ );
34
+ }
35
+ }
27
36
  const result = await this.smtpClient.sendEmail(emailOptions);
28
37
  if (result.success) console.log(`Message ID: ${result.messageId}`);
29
38
  else console.error(`Failed to send email: ${result.error}`);
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  validateEmail
3
- } from "./chunk-UDLJWUFN.mjs";
3
+ } from "./chunk-5BK3VY6I.mjs";
4
4
 
5
5
  // src/SMTPClient.ts
6
- import { Buffer } from "node:buffer";
7
- import crypto from "node:crypto";
8
- import net from "node:net";
9
- import os from "node:os";
10
- import tls from "node:tls";
6
+ import { Buffer } from "buffer";
7
+ import crypto from "crypto";
8
+ import net from "net";
9
+ import os from "os";
10
+ import tls from "tls";
11
11
  var SMTPClient = class {
12
12
  config;
13
13
  socket;
@@ -30,7 +30,9 @@ var SMTPClient = class {
30
30
  clientName: config.clientName ?? os.hostname(),
31
31
  maxRetries: config.maxRetries ?? 3,
32
32
  retryDelay: config.retryDelay ?? 1e3,
33
- skipAuthentication: config.skipAuthentication || false
33
+ skipAuthentication: config.skipAuthentication || false,
34
+ skipEmailValidation: config.skipEmailValidation || false,
35
+ skipMXRecordCheck: config.skipMXRecordCheck || false
34
36
  };
35
37
  this.socket = null;
36
38
  this.connected = false;
@@ -339,10 +341,10 @@ var SMTPClient = class {
339
341
  const messageId = this.generateMessageId();
340
342
  const date = (/* @__PURE__ */ new Date()).toUTCString();
341
343
  const from = options.from || this.config.user;
342
- const { to } = options;
344
+ const recipients = Array.isArray(options.to) ? options.to.join(", ") : options.to;
343
345
  const headers = [
344
346
  `From: ${this.sanitizeHeader(from)}`,
345
- `To: ${this.sanitizeHeader(to)}`,
347
+ `To: ${this.sanitizeHeader(recipients)}`,
346
348
  `Subject: ${this.sanitizeHeader(options.subject)}`,
347
349
  `Message-ID: ${messageId}`,
348
350
  `Date: ${date}`,
@@ -352,9 +354,8 @@ var SMTPClient = class {
352
354
  const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
353
355
  headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
354
356
  }
355
- if (options.replyTo) {
357
+ if (options.replyTo)
356
358
  headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
357
- }
358
359
  if (options.headers) {
359
360
  for (const [name, value] of Object.entries(options.headers)) {
360
361
  if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
@@ -436,11 +437,24 @@ ${text || ""}`;
436
437
  error: "Missing required email parameters (from, to, subject, and either text or html)"
437
438
  };
438
439
  }
439
- if (!validateEmail(from) || !validateEmail(to)) {
440
- return {
441
- success: false,
442
- error: "Invalid email address format"
443
- };
440
+ const recipients = Array.isArray(options.to) ? options.to : [options.to];
441
+ if (!this.config.skipEmailValidation) {
442
+ if (!validateEmail(from)) {
443
+ return {
444
+ success: false,
445
+ error: "Invalid email address format"
446
+ };
447
+ }
448
+ for (const recipient of recipients) {
449
+ if (!validateEmail(recipient)) {
450
+ return {
451
+ success: false,
452
+ error: `Invalid recipient email address format: ${recipient}`
453
+ };
454
+ }
455
+ }
456
+ } else {
457
+ this.log("Email validation skipped (testing mode)");
444
458
  }
445
459
  for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
446
460
  try {
@@ -457,21 +471,20 @@ ${text || ""}`;
457
471
  await this.smtpHandshake();
458
472
  }
459
473
  await this.sendCommand(`MAIL FROM:<${from}>`, 250);
460
- await this.sendCommand(`RCPT TO:<${to}>`, 250);
474
+ for (const recipient of recipients)
475
+ await this.sendCommand(`RCPT TO:<${recipient}>`, 250);
461
476
  if (options.cc) {
462
477
  const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
463
478
  for (const cc of ccList) {
464
- if (validateEmail(cc)) {
479
+ if (validateEmail(cc))
465
480
  await this.sendCommand(`RCPT TO:<${cc}>`, 250);
466
- }
467
481
  }
468
482
  }
469
483
  if (options.bcc) {
470
484
  const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
471
485
  for (const bcc of bccList) {
472
- if (validateEmail(bcc)) {
486
+ if (validateEmail(bcc))
473
487
  await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
474
- }
475
488
  }
476
489
  }
477
490
  await this.sendCommand("DATA", 354);
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-47VXJTWV.mjs";
4
4
 
5
5
  // src/Configuration.ts
6
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync } from "fs";
7
7
  var Configuration = class {
8
8
  config;
9
9
  defaults = {
@@ -35,7 +35,9 @@ var Configuration = class {
35
35
  port: 465,
36
36
  secure: true,
37
37
  debug: false,
38
- maxRetries: 2
38
+ maxRetries: 2,
39
+ skipEmailValidation: false,
40
+ skipMXRecordCheck: false
39
41
  };
40
42
  let fileConfig = {};
41
43
  if (existsSync(configFilePath)) {
package/lib/index.js CHANGED
@@ -79,7 +79,9 @@ var Configuration = class {
79
79
  port: 465,
80
80
  secure: true,
81
81
  debug: false,
82
- maxRetries: 2
82
+ maxRetries: 2,
83
+ skipEmailValidation: false,
84
+ skipMXRecordCheck: false
83
85
  };
84
86
  let fileConfig = {};
85
87
  if ((0, import_node_fs.existsSync)(configFilePath)) {
@@ -189,7 +191,7 @@ function validateEmail(email) {
189
191
  return false;
190
192
  for (const part of domainParts) {
191
193
  if (!part || part.length > 63) return false;
192
- if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
194
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(part)) return false;
193
195
  }
194
196
  return true;
195
197
  } catch (_error) {
@@ -237,7 +239,9 @@ var SMTPClient = class {
237
239
  clientName: config.clientName ?? import_node_os.default.hostname(),
238
240
  maxRetries: config.maxRetries ?? 3,
239
241
  retryDelay: config.retryDelay ?? 1e3,
240
- skipAuthentication: config.skipAuthentication || false
242
+ skipAuthentication: config.skipAuthentication || false,
243
+ skipEmailValidation: config.skipEmailValidation || false,
244
+ skipMXRecordCheck: config.skipMXRecordCheck || false
241
245
  };
242
246
  this.socket = null;
243
247
  this.connected = false;
@@ -546,10 +550,10 @@ var SMTPClient = class {
546
550
  const messageId = this.generateMessageId();
547
551
  const date = (/* @__PURE__ */ new Date()).toUTCString();
548
552
  const from = options.from || this.config.user;
549
- const { to } = options;
553
+ const recipients = Array.isArray(options.to) ? options.to.join(", ") : options.to;
550
554
  const headers = [
551
555
  `From: ${this.sanitizeHeader(from)}`,
552
- `To: ${this.sanitizeHeader(to)}`,
556
+ `To: ${this.sanitizeHeader(recipients)}`,
553
557
  `Subject: ${this.sanitizeHeader(options.subject)}`,
554
558
  `Message-ID: ${messageId}`,
555
559
  `Date: ${date}`,
@@ -559,9 +563,8 @@ var SMTPClient = class {
559
563
  const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
560
564
  headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
561
565
  }
562
- if (options.replyTo) {
566
+ if (options.replyTo)
563
567
  headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
564
- }
565
568
  if (options.headers) {
566
569
  for (const [name, value] of Object.entries(options.headers)) {
567
570
  if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
@@ -643,11 +646,24 @@ ${text || ""}`;
643
646
  error: "Missing required email parameters (from, to, subject, and either text or html)"
644
647
  };
645
648
  }
646
- if (!validateEmail(from) || !validateEmail(to)) {
647
- return {
648
- success: false,
649
- error: "Invalid email address format"
650
- };
649
+ const recipients = Array.isArray(options.to) ? options.to : [options.to];
650
+ if (!this.config.skipEmailValidation) {
651
+ if (!validateEmail(from)) {
652
+ return {
653
+ success: false,
654
+ error: "Invalid email address format"
655
+ };
656
+ }
657
+ for (const recipient of recipients) {
658
+ if (!validateEmail(recipient)) {
659
+ return {
660
+ success: false,
661
+ error: `Invalid recipient email address format: ${recipient}`
662
+ };
663
+ }
664
+ }
665
+ } else {
666
+ this.log("Email validation skipped (testing mode)");
651
667
  }
652
668
  for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
653
669
  try {
@@ -664,21 +680,20 @@ ${text || ""}`;
664
680
  await this.smtpHandshake();
665
681
  }
666
682
  await this.sendCommand(`MAIL FROM:<${from}>`, 250);
667
- await this.sendCommand(`RCPT TO:<${to}>`, 250);
683
+ for (const recipient of recipients)
684
+ await this.sendCommand(`RCPT TO:<${recipient}>`, 250);
668
685
  if (options.cc) {
669
686
  const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
670
687
  for (const cc of ccList) {
671
- if (validateEmail(cc)) {
688
+ if (validateEmail(cc))
672
689
  await this.sendCommand(`RCPT TO:<${cc}>`, 250);
673
- }
674
690
  }
675
691
  }
676
692
  if (options.bcc) {
677
693
  const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
678
694
  for (const bcc of bccList) {
679
- if (validateEmail(bcc)) {
695
+ if (validateEmail(bcc))
680
696
  await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
681
- }
682
697
  }
683
698
  }
684
699
  await this.sendCommand("DATA", 354);
@@ -747,19 +762,28 @@ ${text || ""}`;
747
762
  // src/MikroMail.ts
748
763
  var MikroMail = class {
749
764
  smtpClient;
765
+ config;
750
766
  constructor(options) {
751
767
  const config = new Configuration(options).get();
752
768
  const smtpClient = new SMTPClient(config);
753
769
  this.smtpClient = smtpClient;
770
+ this.config = config;
754
771
  }
755
772
  /**
756
773
  * Sends an email to valid domains.
757
774
  */
758
775
  async send(emailOptions) {
759
776
  try {
760
- const hasMXRecords = await verifyEmailDomain(emailOptions.to);
761
- if (!hasMXRecords)
762
- console.error("Warning: No MX records found for recipient domain");
777
+ const recipients = Array.isArray(emailOptions.to) ? emailOptions.to : [emailOptions.to];
778
+ if (!this.config.skipMXRecordCheck) {
779
+ for (const recipient of recipients) {
780
+ const hasMXRecords = await verifyEmailDomain(recipient);
781
+ if (!hasMXRecords)
782
+ console.error(
783
+ `Warning: No MX records found for recipient domain: ${recipient}`
784
+ );
785
+ }
786
+ }
763
787
  const result = await this.smtpClient.sendEmail(emailOptions);
764
788
  if (result.success) console.log(`Message ID: ${result.messageId}`);
765
789
  else console.error(`Failed to send email: ${result.error}`);
package/lib/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  MikroMail
3
- } from "./chunk-FP3YCQBU.mjs";
4
- import "./chunk-YVXB6HCK.mjs";
5
- import "./chunk-HZ3BDGTE.mjs";
3
+ } from "./chunk-IYCQK5YZ.mjs";
4
+ import "./chunk-ULVDQIAT.mjs";
5
+ import "./chunk-QCFOUQRH.mjs";
6
6
  import "./chunk-47VXJTWV.mjs";
7
- import "./chunk-UDLJWUFN.mjs";
7
+ import "./chunk-5BK3VY6I.mjs";
8
8
  export {
9
9
  MikroMail
10
10
  };
@@ -13,13 +13,15 @@ interface SMTPConfiguration {
13
13
  maxRetries?: number;
14
14
  retryDelay?: number;
15
15
  skipAuthentication?: boolean;
16
+ skipEmailValidation?: boolean;
17
+ skipMXRecordCheck?: boolean;
16
18
  }
17
19
  /**
18
20
  * Email sending options.
19
21
  */
20
22
  interface EmailOptions {
21
23
  from?: string;
22
- to: string;
24
+ to: string | string[];
23
25
  cc?: string | string[];
24
26
  bcc?: string | string[];
25
27
  replyTo?: string;
@@ -13,13 +13,15 @@ interface SMTPConfiguration {
13
13
  maxRetries?: number;
14
14
  retryDelay?: number;
15
15
  skipAuthentication?: boolean;
16
+ skipEmailValidation?: boolean;
17
+ skipMXRecordCheck?: boolean;
16
18
  }
17
19
  /**
18
20
  * Email sending options.
19
21
  */
20
22
  interface EmailOptions {
21
23
  from?: string;
22
- to: string;
24
+ to: string | string[];
23
25
  cc?: string | string[];
24
26
  bcc?: string | string[];
25
27
  replyTo?: string;
@@ -77,7 +77,7 @@ function validateEmail(email) {
77
77
  return false;
78
78
  for (const part of domainParts) {
79
79
  if (!part || part.length > 63) return false;
80
- if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
80
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(part)) return false;
81
81
  }
82
82
  return true;
83
83
  } catch (_error) {
@@ -3,7 +3,7 @@ import {
3
3
  validateEmail,
4
4
  verifyEmailDomain,
5
5
  verifyMXRecords
6
- } from "../chunk-UDLJWUFN.mjs";
6
+ } from "../chunk-5BK3VY6I.mjs";
7
7
  export {
8
8
  encodeQuotedPrintable,
9
9
  validateEmail,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mikromail",
3
3
  "description": "Lightweight replacement for Nodemailer, supporting HTML, international symbols, and more.",
4
- "version": "0.0.6",
4
+ "version": "1.0.0",
5
5
  "author": "Mikael Vesavuori",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -31,7 +31,6 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "test": "npm run test:licenses && npm run test:types && npm run lint && npm run test:unit",
34
- "test:data": "rm -rf test-db && npx tsx random-data.ts",
35
34
  "test:types": "npx type-coverage --at-least 95 --strict --ignore-files \"tests/**/*.ts\" --ignore-files \"*.ts\" --ignore-files \"src/errors/*.ts\" --ignore-files \"testdata/*.ts\"",
36
35
  "test:licenses": "npx license-compliance --direct --allow 'MIT;ISC;0BSD;BSD-2-Clause;BSD-3-Clause;Apache-2.0;Unlicense;CC0-1.0'",
37
36
  "test:unit": "npx vitest run --coverage",
@@ -41,12 +40,13 @@
41
40
  "clean": "rm -rf lib && rm -rf dist",
42
41
  "lint": "npx @biomejs/biome check --write ./src ./tests",
43
42
  "package": "npm pack",
44
- "prepublishOnly": "npm run build"
43
+ "prepublishOnly": "npm run build",
44
+ "prepare": "husky"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "1",
47
+ "@biomejs/biome": "2",
48
48
  "@types/node": "latest",
49
- "@vitest/coverage-v8": "2",
49
+ "@vitest/coverage-v8": "4",
50
50
  "husky": "9",
51
51
  "license-compliance": "latest",
52
52
  "tslib": "latest",
@@ -54,6 +54,6 @@
54
54
  "tsx": "latest",
55
55
  "type-coverage": "2",
56
56
  "typescript": "5",
57
- "vitest": "2"
57
+ "vitest": "4"
58
58
  }
59
59
  }