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 +47 -3
- package/lib/Configuration.js +3 -1
- package/lib/Configuration.mjs +1 -1
- package/lib/MikroMail.d.mts +1 -0
- package/lib/MikroMail.d.ts +1 -0
- package/lib/MikroMail.js +44 -20
- package/lib/MikroMail.mjs +4 -4
- package/lib/SMTPClient.js +29 -16
- package/lib/SMTPClient.mjs +2 -2
- package/lib/{chunk-UDLJWUFN.mjs → chunk-5BK3VY6I.mjs} +2 -2
- package/lib/{chunk-FP3YCQBU.mjs → chunk-IYCQK5YZ.mjs} +15 -6
- package/lib/{chunk-HZ3BDGTE.mjs → chunk-QCFOUQRH.mjs} +34 -21
- package/lib/{chunk-YVXB6HCK.mjs → chunk-ULVDQIAT.mjs} +4 -2
- package/lib/index.js +44 -20
- package/lib/index.mjs +4 -4
- package/lib/interfaces/index.d.mts +3 -1
- package/lib/interfaces/index.d.ts +3 -1
- package/lib/utils/index.js +1 -1
- package/lib/utils/index.mjs +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Lightweight replacement for Nodemailer, supporting HTML, international symbols, and more**.
|
|
4
4
|
|
|
5
5
|

|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
|
|
15
16
|
## Usage
|
|
16
17
|
|
|
17
|
-
###
|
|
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.
|
package/lib/Configuration.js
CHANGED
|
@@ -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)) {
|
package/lib/Configuration.mjs
CHANGED
package/lib/MikroMail.d.mts
CHANGED
|
@@ -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.
|
package/lib/MikroMail.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
|
761
|
-
if (!
|
|
762
|
-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-IYCQK5YZ.mjs";
|
|
4
|
+
import "./chunk-ULVDQIAT.mjs";
|
|
5
|
+
import "./chunk-QCFOUQRH.mjs";
|
|
6
6
|
import "./chunk-47VXJTWV.mjs";
|
|
7
|
-
import "./chunk-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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);
|
package/lib/SMTPClient.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/utils/index.ts
|
|
2
|
-
import { promises as dnsPromises } from "
|
|
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
|
|
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-
|
|
3
|
+
} from "./chunk-ULVDQIAT.mjs";
|
|
4
4
|
import {
|
|
5
5
|
SMTPClient
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QCFOUQRH.mjs";
|
|
7
7
|
import {
|
|
8
8
|
verifyEmailDomain
|
|
9
|
-
} from "./chunk-
|
|
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
|
|
25
|
-
if (!
|
|
26
|
-
|
|
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-
|
|
3
|
+
} from "./chunk-5BK3VY6I.mjs";
|
|
4
4
|
|
|
5
5
|
// src/SMTPClient.ts
|
|
6
|
-
import { Buffer } from "
|
|
7
|
-
import crypto from "
|
|
8
|
-
import net from "
|
|
9
|
-
import os from "
|
|
10
|
-
import tls from "
|
|
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
|
|
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(
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
|
761
|
-
if (!
|
|
762
|
-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-IYCQK5YZ.mjs";
|
|
4
|
+
import "./chunk-ULVDQIAT.mjs";
|
|
5
|
+
import "./chunk-QCFOUQRH.mjs";
|
|
6
6
|
import "./chunk-47VXJTWV.mjs";
|
|
7
|
-
import "./chunk-
|
|
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;
|
package/lib/utils/index.js
CHANGED
|
@@ -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
|
|
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) {
|
package/lib/utils/index.mjs
CHANGED
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
|
|
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": "
|
|
47
|
+
"@biomejs/biome": "2",
|
|
48
48
|
"@types/node": "latest",
|
|
49
|
-
"@vitest/coverage-v8": "
|
|
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": "
|
|
57
|
+
"vitest": "4"
|
|
58
58
|
}
|
|
59
59
|
}
|