@push.rocks/smartmta 14.0.1 → 14.2.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 +30 -2
- package/dist_rust/mailer-bin_linux_amd64_musl +0 -0
- package/dist_rust/mailer-bin_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/mailer-bin_linux_arm64_musl +0 -0
- package/dist_rust/mailer-bin_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/core/classes.email.d.ts +24 -3
- package/dist_ts/mail/core/classes.email.js +55 -6
- 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 -4
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +1 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +4 -2
- package/dist_ts/mail/delivery/functions.raw-email.js +4 -5
- package/dist_ts/mail/routing/classes.email.action.executor.js +3 -3
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +9 -2
- package/dist_ts/mail/routing/classes.unified.email.server.js +47 -27
- 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 +5 -5
- package/readme.md +14 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/core/classes.email.ts +57 -6
- package/ts/mail/core/functions.mailbox.ts +21 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +48 -2
- package/ts/mail/delivery/classes.delivery.system.ts +3 -1
- package/ts/mail/delivery/functions.raw-email.ts +3 -4
- package/ts/mail/routing/classes.email.action.executor.ts +2 -2
- package/ts/mail/routing/classes.unified.email.server.ts +46 -25
- package/ts/security/classes.contentscanner.ts +2 -2
- package/ts/security/classes.rustsecuritybridge.ts +3 -0
|
@@ -174,7 +174,7 @@ export class EmailActionExecutor {
|
|
|
174
174
|
routeName: context.session.matchedRoute?.name,
|
|
175
175
|
targetHost: host,
|
|
176
176
|
targetPort: port,
|
|
177
|
-
recipients: email.
|
|
177
|
+
recipients: email.getAllRecipients()
|
|
178
178
|
},
|
|
179
179
|
success: true
|
|
180
180
|
}));
|
|
@@ -272,7 +272,7 @@ export class EmailActionExecutor {
|
|
|
272
272
|
rejectCode: code,
|
|
273
273
|
rejectMessage: message,
|
|
274
274
|
from: _email.from,
|
|
275
|
-
to: _email.
|
|
275
|
+
to: _email.getAllRecipients()
|
|
276
276
|
},
|
|
277
277
|
success: false
|
|
278
278
|
}));
|
|
@@ -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,
|
|
@@ -2506,10 +2508,10 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2506
2508
|
* envelope MAIL FROM is enforced per recipient via resolveRecipientPolicy
|
|
2507
2509
|
* from session.envelope truth; every parsed header From mailbox must
|
|
2508
2510
|
* independently stay in scope because route matching and downstream
|
|
2509
|
-
* consumers see the header identity.
|
|
2511
|
+
* consumers see the header identity. A scoped submission without a From
|
|
2512
|
+
* field is refused with 550 5.6.0.
|
|
2510
2513
|
*/
|
|
2511
2514
|
private async checkAuthenticatedHeaderFromScope(
|
|
2512
|
-
email: Email,
|
|
2513
2515
|
fromFields: string[][],
|
|
2514
2516
|
session: IExtendedSmtpSession,
|
|
2515
2517
|
): Promise<ISmtpDecision | null> {
|
|
@@ -2531,21 +2533,24 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2531
2533
|
};
|
|
2532
2534
|
// A message the parser rejects never reaches this check: Rust refuses it at
|
|
2533
2535
|
// DATA, as it refuses a header section that ends before its empty line.
|
|
2534
|
-
let fromMailboxes: string[];
|
|
2535
2536
|
if (fromFields.length === 0) {
|
|
2536
|
-
// A
|
|
2537
|
-
//
|
|
2538
|
-
//
|
|
2539
|
-
|
|
2540
|
-
|
|
2537
|
+
// A scoped submission must carry the From field this check verifies
|
|
2538
|
+
// (RFC 5322 3.6). Passing it on the envelope sender would rest on the
|
|
2539
|
+
// assumption that no recipient's reader finds a From where this parser
|
|
2540
|
+
// found none; unscoped and unauthenticated mail are not checked here.
|
|
2541
|
+
return {
|
|
2542
|
+
accepted: false,
|
|
2543
|
+
smtpCode: 550,
|
|
2544
|
+
smtpMessage: '5.6.0 Message has no From header field (RFC 5322 3.6)',
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
if (fromFields.some((field) => field.length === 0)) {
|
|
2541
2548
|
// A From field without a mailbox, such as an empty group or an encoded
|
|
2542
2549
|
// word in place of the address, still displays an identity this check
|
|
2543
2550
|
// cannot verify.
|
|
2544
2551
|
return notPermitted;
|
|
2545
|
-
} else {
|
|
2546
|
-
fromMailboxes = fromFields.flat();
|
|
2547
2552
|
}
|
|
2548
|
-
const outOfScope =
|
|
2553
|
+
const outOfScope = fromFields.flat().find(
|
|
2549
2554
|
(mailbox) => !this.matchesScopePatterns(mailbox, scope.senders),
|
|
2550
2555
|
);
|
|
2551
2556
|
return outOfScope === undefined ? null : notPermitted;
|
|
@@ -2592,7 +2597,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2592
2597
|
}
|
|
2593
2598
|
}
|
|
2594
2599
|
|
|
2595
|
-
/**
|
|
2600
|
+
/** A copy of the email whose envelope is only the given SMTP envelope recipient. */
|
|
2596
2601
|
private cloneEmailForRoutingRecipient(email: Email, recipient: string): Email {
|
|
2597
2602
|
return new Email({
|
|
2598
2603
|
from: email.from,
|
|
@@ -2612,7 +2617,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2612
2617
|
variables: structuredClone(email.variables),
|
|
2613
2618
|
})
|
|
2614
2619
|
.setMessageId(email.getMessageId())
|
|
2615
|
-
.setEnvelopeFrom(email.getEnvelopeFrom())
|
|
2620
|
+
.setEnvelopeFrom(email.getEnvelopeFrom())
|
|
2621
|
+
.setEnvelopeTo([recipient]);
|
|
2616
2622
|
}
|
|
2617
2623
|
|
|
2618
2624
|
private scopeSessionForRoutingRecipient(
|
|
@@ -3343,7 +3349,6 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3343
3349
|
// enforced before any durable acceptance or routing: the RCPT phase
|
|
3344
3350
|
// only ever saw the envelope MAIL FROM.
|
|
3345
3351
|
const headerFromViolation = await this.checkAuthenticatedHeaderFromScope(
|
|
3346
|
-
email,
|
|
3347
3352
|
this.fromFieldAddresses(data.parsed),
|
|
3348
3353
|
session,
|
|
3349
3354
|
);
|
|
@@ -3512,6 +3517,19 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3512
3517
|
}
|
|
3513
3518
|
}
|
|
3514
3519
|
|
|
3520
|
+
/**
|
|
3521
|
+
* The envelope recipients of a received message's session. A received
|
|
3522
|
+
* message is delivered to its envelope alone, so an empty one is refused;
|
|
3523
|
+
* its To and Cc fields never stand in for it.
|
|
3524
|
+
*/
|
|
3525
|
+
private envelopeRecipientsOf(session: IExtendedSmtpSession): string[] {
|
|
3526
|
+
const recipients = (session.envelope?.rcptTo || []).map((recipient) => recipient.address);
|
|
3527
|
+
if (recipients.length === 0) {
|
|
3528
|
+
throw new Error('A received message needs at least one envelope recipient');
|
|
3529
|
+
}
|
|
3530
|
+
return recipients;
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3515
3533
|
/**
|
|
3516
3534
|
* Builds the Email of stored message bytes (the replay paths
|
|
3517
3535
|
* dispatchAcceptedEnvelope and processEmailByMode(Buffer)) with the parser
|
|
@@ -3532,7 +3550,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3532
3550
|
/** The Email of a message the Rust parser read (docs/cleanroom/mailer-mime.md section 2.2). */
|
|
3533
3551
|
private createEmailFromParsed(parsed: IBridgeParsedMessage, session: IExtendedSmtpSession): Email {
|
|
3534
3552
|
const headerFromMailboxes = this.fromFieldAddresses(parsed).flat();
|
|
3535
|
-
const envelopeRecipients =
|
|
3553
|
+
const envelopeRecipients = this.envelopeRecipientsOf(session);
|
|
3536
3554
|
const parsedHeaders = new Map<string, string>();
|
|
3537
3555
|
for (const header of parsed.headers) {
|
|
3538
3556
|
if (this.isStandardParsedHeader(header.key)) {
|
|
@@ -3548,9 +3566,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3548
3566
|
return new Email({
|
|
3549
3567
|
// The first mailbox of the From fields in message order, groups flattened
|
|
3550
3568
|
from: headerFromMailboxes[0] || session.envelope.mailFrom.address,
|
|
3551
|
-
to: envelopeRecipients
|
|
3552
|
-
? envelopeRecipients
|
|
3553
|
-
: parsed.to.map(recipient => recipient.email),
|
|
3569
|
+
to: envelopeRecipients,
|
|
3554
3570
|
cc: parsed.cc.map(recipient => recipient.email),
|
|
3555
3571
|
bcc: [],
|
|
3556
3572
|
subject: parsed.subject || '',
|
|
@@ -3563,7 +3579,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3563
3579
|
contentType: attachment.contentType,
|
|
3564
3580
|
...(attachment.contentId ? { contentId: attachment.contentId } : {}),
|
|
3565
3581
|
})),
|
|
3566
|
-
});
|
|
3582
|
+
}).setEnvelopeTo(envelopeRecipients);
|
|
3567
3583
|
}
|
|
3568
3584
|
|
|
3569
3585
|
private isStandardParsedHeader(headerName: string): boolean {
|
|
@@ -3969,6 +3985,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3969
3985
|
} else {
|
|
3970
3986
|
email = emailData;
|
|
3971
3987
|
}
|
|
3988
|
+
// The session's envelope alone decides who receives the message.
|
|
3989
|
+
email.setEnvelopeTo(this.envelopeRecipientsOf(session));
|
|
3972
3990
|
|
|
3973
3991
|
// Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
|
|
3974
3992
|
if (session.remoteAddress && session.remoteAddress !== '127.0.0.1') {
|
|
@@ -4279,23 +4297,24 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4279
4297
|
}
|
|
4280
4298
|
): Promise<string> {
|
|
4281
4299
|
this.assertAcceptingOutboundWork();
|
|
4282
|
-
|
|
4300
|
+
const recipients = email.getAllRecipients();
|
|
4301
|
+
logger.log('info', `Sending email: ${email.subject} to ${recipients.join(', ')}`);
|
|
4283
4302
|
|
|
4284
4303
|
try {
|
|
4285
4304
|
if (!email.from) {
|
|
4286
4305
|
throw new Error('Email must have a sender address');
|
|
4287
4306
|
}
|
|
4288
4307
|
|
|
4289
|
-
if (
|
|
4308
|
+
if (recipients.length === 0) {
|
|
4290
4309
|
throw new Error('Email must have at least one recipient');
|
|
4291
4310
|
}
|
|
4292
4311
|
|
|
4293
4312
|
// Check if any recipients are on the suppression list
|
|
4294
4313
|
if (!options?.skipSuppressionCheck) {
|
|
4295
|
-
const suppressedRecipients =
|
|
4314
|
+
const suppressedRecipients = recipients.filter(recipient => this.isEmailSuppressed(recipient));
|
|
4296
4315
|
|
|
4297
4316
|
if (suppressedRecipients.length > 0) {
|
|
4298
|
-
const originalCount =
|
|
4317
|
+
const originalCount = recipients.length;
|
|
4299
4318
|
const suppressed = suppressedRecipients.map(recipient => {
|
|
4300
4319
|
const info = this.getSuppressionInfo(recipient);
|
|
4301
4320
|
return {
|
|
@@ -4311,7 +4330,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4311
4330
|
throw new Error('All recipients are on the suppression list');
|
|
4312
4331
|
}
|
|
4313
4332
|
|
|
4314
|
-
|
|
4333
|
+
// The message's fields stay as written; the suppressed recipients
|
|
4334
|
+
// only leave the envelope.
|
|
4335
|
+
email.setEnvelopeTo(recipients.filter(recipient => !this.isEmailSuppressed(recipient)));
|
|
4315
4336
|
}
|
|
4316
4337
|
}
|
|
4317
4338
|
|
|
@@ -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;
|