@push.rocks/smartmta 14.1.0 → 14.3.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/changelog.md +35 -2
- package/dist_rust/mailer-bin_linux_amd64_musl +0 -0
- package/dist_rust/mailer-bin_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/mailer-bin_linux_arm64_musl +0 -0
- package/dist_rust/mailer-bin_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/core/classes.email.d.ts +41 -3
- package/dist_ts/mail/core/classes.email.js +87 -12
- package/dist_ts/mail/core/functions.mailbox.d.ts +8 -0
- package/dist_ts/mail/core/functions.mailbox.js +21 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.js +41 -5
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +5 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +7 -8
- package/dist_ts/mail/delivery/functions.raw-email.js +4 -5
- package/dist_ts/mail/routing/classes.email.action.executor.js +10 -5
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +10 -1
- package/dist_ts/mail/routing/classes.unified.email.server.js +47 -16
- package/dist_ts/security/classes.contentscanner.js +3 -3
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +3 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
- package/package.json +1 -1
- package/readme.md +15 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/core/classes.email.ts +99 -15
- package/ts/mail/core/functions.mailbox.ts +21 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +48 -3
- package/ts/mail/delivery/classes.delivery.system.ts +6 -7
- package/ts/mail/delivery/functions.raw-email.ts +3 -4
- package/ts/mail/routing/classes.email.action.executor.ts +9 -4
- package/ts/mail/routing/classes.unified.email.server.ts +47 -15
- package/ts/security/classes.contentscanner.ts +2 -2
- package/ts/security/classes.rustsecuritybridge.ts +3 -0
|
@@ -284,14 +284,13 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* The copy of an email for one SMTP transaction: its envelope is exactly
|
|
289
|
+
* `recipients`, and its To, Cc and Bcc fields stay as they are, so every
|
|
290
|
+
* copy shows the same To and Cc.
|
|
291
|
+
*/
|
|
287
292
|
private createEmailForRecipients(email: Email, recipients: string[]): Email {
|
|
288
|
-
|
|
289
|
-
return new Email({
|
|
290
|
-
...email.toEmailOptions(),
|
|
291
|
-
to: email.to.filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
292
|
-
cc: (email.cc || []).filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
293
|
-
bcc: (email.bcc || []).filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
294
|
-
})
|
|
293
|
+
return new Email({ ...email.toEmailOptions(), envelopeTo: recipients })
|
|
295
294
|
.setMessageId(email.getMessageId())
|
|
296
295
|
.setEnvelopeFrom(email.getEnvelopeFrom());
|
|
297
296
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, type Hash } from 'node:crypto';
|
|
2
|
-
import { domainToASCII } from 'node:url';
|
|
3
2
|
|
|
4
3
|
import { Email } from '../core/classes.email.js';
|
|
4
|
+
import { canonicalMailboxAddress } from '../core/functions.mailbox.js';
|
|
5
5
|
|
|
6
6
|
export interface IRawSmtpEnvelope {
|
|
7
7
|
from: string;
|
|
@@ -50,11 +50,10 @@ const normalizeRawMailbox = (valueArg: string, fieldArg: string): string => {
|
|
|
50
50
|
if (atIndex < 1 || atIndex !== value.lastIndexOf('@') || atIndex === value.length - 1) {
|
|
51
51
|
throw new Error(`Raw SMTP envelope ${fieldArg} must be a plain mailbox address`);
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
53
|
+
const normalized = canonicalMailboxAddress(value);
|
|
54
|
+
if (!normalized) {
|
|
55
55
|
throw new Error(`Raw SMTP envelope ${fieldArg} has an invalid domain`);
|
|
56
56
|
}
|
|
57
|
-
const normalized = `${value.slice(0, atIndex).toLowerCase()}@${asciiDomain.toLowerCase()}`;
|
|
58
57
|
try {
|
|
59
58
|
new Email({ from: normalized, to: normalized, subject: '', text: '' });
|
|
60
59
|
} catch {
|
|
@@ -136,9 +136,14 @@ export class EmailActionExecutor {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Add standard forwarding headers
|
|
139
|
+
// Add standard forwarding headers. X-Forwarded-To names the one envelope
|
|
140
|
+
// recipient of a single-recipient copy and is never a list: the next hop
|
|
141
|
+
// has RCPT TO, and a list would show each recipient to every other.
|
|
140
142
|
email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown';
|
|
141
|
-
|
|
143
|
+
const forwardedRecipients = email.getAllRecipients();
|
|
144
|
+
if (forwardedRecipients.length === 1) {
|
|
145
|
+
email.headers['X-Forwarded-To'] = forwardedRecipients[0];
|
|
146
|
+
}
|
|
142
147
|
email.headers['X-Forwarded-Date'] = new Date().toISOString();
|
|
143
148
|
|
|
144
149
|
if (options.durableQueue) {
|
|
@@ -174,7 +179,7 @@ export class EmailActionExecutor {
|
|
|
174
179
|
routeName: context.session.matchedRoute?.name,
|
|
175
180
|
targetHost: host,
|
|
176
181
|
targetPort: port,
|
|
177
|
-
recipients: email.
|
|
182
|
+
recipients: email.getAllRecipients()
|
|
178
183
|
},
|
|
179
184
|
success: true
|
|
180
185
|
}));
|
|
@@ -272,7 +277,7 @@ export class EmailActionExecutor {
|
|
|
272
277
|
rejectCode: code,
|
|
273
278
|
rejectMessage: message,
|
|
274
279
|
from: _email.from,
|
|
275
|
-
to: _email.
|
|
280
|
+
to: _email.getAllRecipients()
|
|
276
281
|
},
|
|
277
282
|
success: false
|
|
278
283
|
}));
|
|
@@ -1057,12 +1057,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1057
1057
|
? await this.resolveOutboundDkim(options.dkimDomain, options.dkimSelector || 'default')
|
|
1058
1058
|
: undefined;
|
|
1059
1059
|
|
|
1060
|
-
// Serialize the Email to the outbound format
|
|
1060
|
+
// Serialize the Email to the outbound format. RCPT TO is the one accessor's
|
|
1061
|
+
// list, which mailer-bin sends verbatim; to and cc are only written.
|
|
1061
1062
|
const outboundEmail: IOutboundEmail = {
|
|
1062
1063
|
from: email.from,
|
|
1063
1064
|
to: email.to,
|
|
1064
1065
|
cc: email.cc || [],
|
|
1065
1066
|
bcc: email.bcc || [],
|
|
1067
|
+
envelopeTo: email.getAllRecipients(),
|
|
1066
1068
|
subject: email.subject || '',
|
|
1067
1069
|
text: email.text || '',
|
|
1068
1070
|
html: email.html || undefined,
|
|
@@ -2595,13 +2597,17 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2595
2597
|
}
|
|
2596
2598
|
}
|
|
2597
2599
|
|
|
2598
|
-
/**
|
|
2600
|
+
/**
|
|
2601
|
+
* A copy of the email whose envelope is only the given SMTP envelope
|
|
2602
|
+
* recipient; its To, Cc and Bcc fields stay as they are.
|
|
2603
|
+
*/
|
|
2599
2604
|
private cloneEmailForRoutingRecipient(email: Email, recipient: string): Email {
|
|
2600
2605
|
return new Email({
|
|
2601
2606
|
from: email.from,
|
|
2602
|
-
to: [
|
|
2603
|
-
cc: [],
|
|
2604
|
-
bcc: [],
|
|
2607
|
+
to: [...email.to],
|
|
2608
|
+
cc: [...email.cc],
|
|
2609
|
+
bcc: [...email.bcc],
|
|
2610
|
+
envelopeTo: [recipient],
|
|
2605
2611
|
subject: email.subject,
|
|
2606
2612
|
text: email.text,
|
|
2607
2613
|
html: email.html,
|
|
@@ -3514,6 +3520,19 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3514
3520
|
}
|
|
3515
3521
|
}
|
|
3516
3522
|
|
|
3523
|
+
/**
|
|
3524
|
+
* The envelope recipients of a received message's session. A received
|
|
3525
|
+
* message is delivered to its envelope alone, so an empty one is refused;
|
|
3526
|
+
* its To and Cc fields never stand in for it.
|
|
3527
|
+
*/
|
|
3528
|
+
private envelopeRecipientsOf(session: IExtendedSmtpSession): string[] {
|
|
3529
|
+
const recipients = (session.envelope?.rcptTo || []).map((recipient) => recipient.address);
|
|
3530
|
+
if (recipients.length === 0) {
|
|
3531
|
+
throw new Error('A received message needs at least one envelope recipient');
|
|
3532
|
+
}
|
|
3533
|
+
return recipients;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3517
3536
|
/**
|
|
3518
3537
|
* Builds the Email of stored message bytes (the replay paths
|
|
3519
3538
|
* dispatchAcceptedEnvelope and processEmailByMode(Buffer)) with the parser
|
|
@@ -3534,7 +3553,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3534
3553
|
/** The Email of a message the Rust parser read (docs/cleanroom/mailer-mime.md section 2.2). */
|
|
3535
3554
|
private createEmailFromParsed(parsed: IBridgeParsedMessage, session: IExtendedSmtpSession): Email {
|
|
3536
3555
|
const headerFromMailboxes = this.fromFieldAddresses(parsed).flat();
|
|
3537
|
-
const envelopeRecipients =
|
|
3556
|
+
const envelopeRecipients = this.envelopeRecipientsOf(session);
|
|
3538
3557
|
const parsedHeaders = new Map<string, string>();
|
|
3539
3558
|
for (const header of parsed.headers) {
|
|
3540
3559
|
if (this.isStandardParsedHeader(header.key)) {
|
|
@@ -3550,11 +3569,12 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3550
3569
|
return new Email({
|
|
3551
3570
|
// The first mailbox of the From fields in message order, groups flattened
|
|
3552
3571
|
from: headerFromMailboxes[0] || session.envelope.mailFrom.address,
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3572
|
+
// The message's To and Cc fields; the envelope alone receives it, and a
|
|
3573
|
+
// Bcc field never travels on.
|
|
3574
|
+
to: parsed.to.map(recipient => recipient.email),
|
|
3556
3575
|
cc: parsed.cc.map(recipient => recipient.email),
|
|
3557
3576
|
bcc: [],
|
|
3577
|
+
envelopeTo: envelopeRecipients,
|
|
3558
3578
|
subject: parsed.subject || '',
|
|
3559
3579
|
text: bodies.text,
|
|
3560
3580
|
html: bodies.html,
|
|
@@ -3971,6 +3991,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3971
3991
|
} else {
|
|
3972
3992
|
email = emailData;
|
|
3973
3993
|
}
|
|
3994
|
+
// The session's envelope alone decides who receives the message.
|
|
3995
|
+
email.setEnvelopeTo(this.envelopeRecipientsOf(session));
|
|
3974
3996
|
|
|
3975
3997
|
// Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
|
|
3976
3998
|
if (session.remoteAddress && session.remoteAddress !== '127.0.0.1') {
|
|
@@ -4281,23 +4303,26 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4281
4303
|
}
|
|
4282
4304
|
): Promise<string> {
|
|
4283
4305
|
this.assertAcceptingOutboundWork();
|
|
4284
|
-
|
|
4306
|
+
const recipients = email.getAllRecipients();
|
|
4307
|
+
// What goes to the queue; the caller's email is never changed.
|
|
4308
|
+
let outgoing = email;
|
|
4309
|
+
logger.log('info', `Sending email: ${email.subject} to ${recipients.join(', ')}`);
|
|
4285
4310
|
|
|
4286
4311
|
try {
|
|
4287
4312
|
if (!email.from) {
|
|
4288
4313
|
throw new Error('Email must have a sender address');
|
|
4289
4314
|
}
|
|
4290
4315
|
|
|
4291
|
-
if (
|
|
4316
|
+
if (recipients.length === 0) {
|
|
4292
4317
|
throw new Error('Email must have at least one recipient');
|
|
4293
4318
|
}
|
|
4294
4319
|
|
|
4295
4320
|
// Check if any recipients are on the suppression list
|
|
4296
4321
|
if (!options?.skipSuppressionCheck) {
|
|
4297
|
-
const suppressedRecipients =
|
|
4322
|
+
const suppressedRecipients = recipients.filter(recipient => this.isEmailSuppressed(recipient));
|
|
4298
4323
|
|
|
4299
4324
|
if (suppressedRecipients.length > 0) {
|
|
4300
|
-
const originalCount =
|
|
4325
|
+
const originalCount = recipients.length;
|
|
4301
4326
|
const suppressed = suppressedRecipients.map(recipient => {
|
|
4302
4327
|
const info = this.getSuppressionInfo(recipient);
|
|
4303
4328
|
return {
|
|
@@ -4313,11 +4338,18 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4313
4338
|
throw new Error('All recipients are on the suppression list');
|
|
4314
4339
|
}
|
|
4315
4340
|
|
|
4316
|
-
|
|
4341
|
+
// The message's fields stay as written; the suppressed recipients
|
|
4342
|
+
// only leave the envelope of the copy that is sent.
|
|
4343
|
+
outgoing = new Email({
|
|
4344
|
+
...email.toEmailOptions(),
|
|
4345
|
+
envelopeTo: recipients.filter(recipient => !this.isEmailSuppressed(recipient)),
|
|
4346
|
+
})
|
|
4347
|
+
.setMessageId(email.getMessageId())
|
|
4348
|
+
.setEnvelopeFrom(email.getEnvelopeFrom());
|
|
4317
4349
|
}
|
|
4318
4350
|
}
|
|
4319
4351
|
|
|
4320
|
-
const id = await this.deliveryQueue.enqueue(
|
|
4352
|
+
const id = await this.deliveryQueue.enqueue(outgoing, mode, route);
|
|
4321
4353
|
|
|
4322
4354
|
logger.log('info', `Email queued with ID: ${id}`);
|
|
4323
4355
|
return id;
|
|
@@ -346,7 +346,7 @@ export class ContentScanner {
|
|
|
346
346
|
void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
|
|
347
347
|
level: SecurityLogLevel.ERROR,
|
|
348
348
|
type: SecurityEventType.MALWARE,
|
|
349
|
-
message: `High threat content detected in email from ${email.from} to ${email.
|
|
349
|
+
message: `High threat content detected in email from ${email.from} to ${email.getAllRecipients().join(', ')}`,
|
|
350
350
|
details: {
|
|
351
351
|
messageId: email.getMessageId(),
|
|
352
352
|
threatType: result.threatType,
|
|
@@ -369,7 +369,7 @@ export class ContentScanner {
|
|
|
369
369
|
void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
|
|
370
370
|
level: SecurityLogLevel.WARN,
|
|
371
371
|
type: SecurityEventType.SPAM,
|
|
372
|
-
message: `Suspicious content detected in email from ${email.from} to ${email.
|
|
372
|
+
message: `Suspicious content detected in email from ${email.from} to ${email.getAllRecipients().join(', ')}`,
|
|
373
373
|
details: {
|
|
374
374
|
messageId: email.getMessageId(),
|
|
375
375
|
threatType: result.threatType,
|
|
@@ -358,9 +358,12 @@ const isSmtpDeliveryPhase = (valueArg: unknown): valueArg is TSmtpDeliveryPhase
|
|
|
358
358
|
|
|
359
359
|
interface IOutboundEmail {
|
|
360
360
|
from: string;
|
|
361
|
+
/** The message's To field; written as To, never used for RCPT TO when `envelopeTo` is set. */
|
|
361
362
|
to: string[];
|
|
362
363
|
cc?: string[];
|
|
363
364
|
bcc?: string[];
|
|
365
|
+
/** The SMTP envelope recipients; when present, RCPT TO goes to them alone. */
|
|
366
|
+
envelopeTo?: string[];
|
|
364
367
|
subject?: string;
|
|
365
368
|
text?: string;
|
|
366
369
|
html?: string;
|