@push.rocks/smartmta 14.2.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 +17 -0
- package/dist_rust/mailer-bin_linux_amd64_musl.tsrust-build.json +3 -3
- package/dist_rust/mailer-bin_linux_arm64_musl.tsrust-build.json +3 -3
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/core/classes.email.d.ts +17 -0
- package/dist_ts/mail/core/classes.email.js +33 -7
- package/dist_ts/mail/delivery/classes.delivery.queue.js +11 -12
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +5 -1
- package/dist_ts/mail/delivery/classes.delivery.system.js +8 -11
- package/dist_ts/mail/routing/classes.email.action.executor.js +8 -3
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +4 -1
- package/dist_ts/mail/routing/classes.unified.email.server.js +25 -12
- package/package.json +1 -1
- package/readme.md +3 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/core/classes.email.ts +42 -9
- package/ts/mail/delivery/classes.delivery.queue.ts +10 -11
- package/ts/mail/delivery/classes.delivery.system.ts +7 -10
- package/ts/mail/routing/classes.email.action.executor.ts +7 -2
- package/ts/mail/routing/classes.unified.email.server.ts +24 -11
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -518,7 +518,9 @@ email.getEnvelopeTo(); // ['alice@example.com', 'archive@example.org']
|
|
|
518
518
|
email.getAllRecipients(); // the same list: the RCPT TO of every delivery
|
|
519
519
|
```
|
|
520
520
|
|
|
521
|
-
|
|
521
|
+
`new Email({ ..., envelopeTo })` sets the envelope at construction. With an envelope, `to`, `cc` and `bcc` are the message's header fields: they take no part in delivery, and every copy of the message (one per MX domain or per recipient) shows the same To and Cc. Bcc is never written. No field entry is refused: CR and LF are removed, the entry is trimmed, and an entry that is then empty is dropped.
|
|
522
|
+
|
|
523
|
+
The `Email` of a received message (hook contexts, `processEmailByMode()`, `dispatchAcceptedEnvelope()`) always carries the SMTP envelope it was accepted with, and its `to` and `cc` are the addresses of the message's To and Cc fields, in order; a relayed copy carries them, never addresses taken from the envelope, and a message without a To field is relayed without one. Display names, groups and comments are not carried: a relayed copy's To and Cc list the addresses only (a group's member addresses among them). A To or Cc entry that is not a valid mailbox address never refuses the message: relayed copies carry it, and `deliver` and `store` routes hand on the received bytes unchanged. `X-Forwarded-To` is added only to a forwarded copy with exactly one envelope recipient and names that recipient. `processEmailByMode(emailOrBuffer, session)` sets the envelope from `session.envelope.rcptTo` on every call and throws when that list is empty. `sendEmail()` and `enqueue()` never change the caller's `Email`: the queue item holds its own copy, whose envelope is the recipients at that call minus any address on the suppression list.
|
|
522
524
|
|
|
523
525
|
### 📮 Delivery Modes
|
|
524
526
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -24,6 +24,13 @@ export interface IEmailOptions {
|
|
|
24
24
|
priority?: 'high' | 'normal' | 'low'; // Optional email priority
|
|
25
25
|
skipAdvancedValidation?: boolean; // Skip advanced validation for special cases
|
|
26
26
|
variables?: Record<string, any>; // Template variables for placeholder replacement
|
|
27
|
+
/**
|
|
28
|
+
* The SMTP envelope recipients (RCPT TO), as setEnvelopeTo sets them. With
|
|
29
|
+
* an envelope, `to`, `cc` and `bcc` are the message's header fields: they
|
|
30
|
+
* take no part in delivery, every copy of the email shows the same entries,
|
|
31
|
+
* and no entry is refused (see headerFieldRecipients).
|
|
32
|
+
*/
|
|
33
|
+
envelopeTo?: string[];
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/**
|
|
@@ -70,13 +77,21 @@ export class Email {
|
|
|
70
77
|
}
|
|
71
78
|
this.from = options.from;
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
if (options.envelopeTo !== undefined) {
|
|
81
|
+
// The envelope alone receives the email; to, cc and bcc are header fields.
|
|
82
|
+
this.setEnvelopeTo(options.envelopeTo);
|
|
83
|
+
this.to = this.headerFieldRecipients(options.to);
|
|
84
|
+
this.cc = this.headerFieldRecipients(options.cc);
|
|
85
|
+
this.bcc = this.headerFieldRecipients(options.bcc);
|
|
86
|
+
} else {
|
|
87
|
+
// Handle to addresses (single or multiple)
|
|
88
|
+
this.to = options.to ? this.parseRecipients(options.to) : [];
|
|
89
|
+
|
|
90
|
+
// Handle optional cc and bcc
|
|
91
|
+
this.cc = options.cc ? this.parseRecipients(options.cc) : [];
|
|
92
|
+
this.bcc = options.bcc ? this.parseRecipients(options.bcc) : [];
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
// Note: Templates may be created without recipients
|
|
81
96
|
// Recipients will be added when the email is actually sent
|
|
82
97
|
|
|
@@ -216,6 +231,24 @@ export class Email {
|
|
|
216
231
|
return result;
|
|
217
232
|
}
|
|
218
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The entries of a To, Cc or Bcc field of an email that has an envelope, for
|
|
236
|
+
* received and constructed emails alike. The envelope alone receives such an
|
|
237
|
+
* email, so an entry is kept whether or not it is a valid mailbox address:
|
|
238
|
+
* CR and LF are removed (an entry never adds a header line), the entry is
|
|
239
|
+
* trimmed, and an entry that is then empty is dropped. It never throws.
|
|
240
|
+
* @param recipients A string or array of field entries
|
|
241
|
+
* @returns The field entries, in order
|
|
242
|
+
*/
|
|
243
|
+
private headerFieldRecipients(recipients: string | string[] | undefined): string[] {
|
|
244
|
+
if (recipients === undefined) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return (Array.isArray(recipients) ? recipients : [recipients])
|
|
248
|
+
.map((entry) => entry.replace(/[\r\n]/g, '').trim())
|
|
249
|
+
.filter((entry) => entry.length > 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
/**
|
|
220
253
|
* Basic sanitization for strings to prevent header injection
|
|
221
254
|
* @param input The string to sanitize
|
|
@@ -563,8 +596,8 @@ export class Email {
|
|
|
563
596
|
}
|
|
564
597
|
|
|
565
598
|
// Validate recipients
|
|
566
|
-
const recipientsToCheck = options.checkFirstRecipientOnly ?
|
|
567
|
-
|
|
599
|
+
const recipientsToCheck = options.checkFirstRecipientOnly ?
|
|
600
|
+
this.getAllRecipients().slice(0, 1) : this.getAllRecipients();
|
|
568
601
|
|
|
569
602
|
for (const recipient of recipientsToCheck) {
|
|
570
603
|
const recipientResult = await Email.emailValidator.validate(recipient, {
|
|
@@ -461,11 +461,9 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
461
461
|
|
|
462
462
|
private cloneEmail(emailArg: Email): Email {
|
|
463
463
|
const options = deserialize(serialize(emailArg.toEmailOptions())) as IEmailOptions;
|
|
464
|
-
|
|
464
|
+
return new Email({ ...options, envelopeTo: emailArg.getEnvelopeTo() })
|
|
465
465
|
.setMessageId(emailArg.getMessageId())
|
|
466
466
|
.setEnvelopeFrom(emailArg.getEnvelopeFrom());
|
|
467
|
-
const envelopeTo = emailArg.getEnvelopeTo();
|
|
468
|
-
return envelopeTo ? email.setEnvelopeTo(envelopeTo) : email;
|
|
469
467
|
}
|
|
470
468
|
|
|
471
469
|
public [kQueueCreateItemSnapshot](itemArg: IQueueItem): IQueueItem {
|
|
@@ -1116,10 +1114,13 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
1116
1114
|
},
|
|
1117
1115
|
): Promise<string> {
|
|
1118
1116
|
if (!raw) {
|
|
1119
|
-
// The item's
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1117
|
+
// The item owns a copy of the caller's email, so a caller that edits and
|
|
1118
|
+
// sends its email again never changes or reuses this item. Its
|
|
1119
|
+
// recipients become its envelope, the list serializeItem stores and
|
|
1120
|
+
// deserializeItem restores. setEnvelopeTo refuses an empty list or a
|
|
1121
|
+
// recipient that is not a mailbox, so the store never holds an item its
|
|
1122
|
+
// reader refuses.
|
|
1123
|
+
processingResult = this.cloneEmail(processingResult);
|
|
1123
1124
|
try {
|
|
1124
1125
|
processingResult.setEnvelopeTo(processingResult.getAllRecipients());
|
|
1125
1126
|
} catch (error) {
|
|
@@ -2310,12 +2311,10 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
2310
2311
|
|
|
2311
2312
|
let processingResult: Email;
|
|
2312
2313
|
try {
|
|
2313
|
-
|
|
2314
|
+
// With its stored envelope, an item's to, cc and bcc are header fields.
|
|
2315
|
+
processingResult = new Email({ ...storedOptions, envelopeTo: parsed.processingResult.envelopeTo })
|
|
2314
2316
|
.setMessageId(parsed.processingResult.messageId)
|
|
2315
2317
|
.setEnvelopeFrom(parsed.processingResult.envelopeFrom);
|
|
2316
|
-
if (parsed.processingResult.envelopeTo !== undefined) {
|
|
2317
|
-
processingResult.setEnvelopeTo(parsed.processingResult.envelopeTo);
|
|
2318
|
-
}
|
|
2319
2318
|
} catch (error) {
|
|
2320
2319
|
throw new Error(`Invalid managed queue item at ${key}: ${getErrorMessage(error)}`);
|
|
2321
2320
|
}
|
|
@@ -284,18 +284,15 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
/**
|
|
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
|
+
*/
|
|
288
292
|
private createEmailForRecipients(email: Email, recipients: string[]): Email {
|
|
289
|
-
|
|
290
|
-
return new Email({
|
|
291
|
-
...email.toEmailOptions(),
|
|
292
|
-
to: email.to.filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
293
|
-
cc: (email.cc || []).filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
294
|
-
bcc: (email.bcc || []).filter((recipient) => recipientSet.has(recipient.toLowerCase())),
|
|
295
|
-
})
|
|
293
|
+
return new Email({ ...email.toEmailOptions(), envelopeTo: recipients })
|
|
296
294
|
.setMessageId(email.getMessageId())
|
|
297
|
-
.setEnvelopeFrom(email.getEnvelopeFrom())
|
|
298
|
-
.setEnvelopeTo(recipients);
|
|
295
|
+
.setEnvelopeFrom(email.getEnvelopeFrom());
|
|
299
296
|
}
|
|
300
297
|
|
|
301
298
|
private contextualizeSmtpError(
|
|
@@ -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) {
|
|
@@ -2597,13 +2597,17 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2597
2597
|
}
|
|
2598
2598
|
}
|
|
2599
2599
|
|
|
2600
|
-
/**
|
|
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
|
+
*/
|
|
2601
2604
|
private cloneEmailForRoutingRecipient(email: Email, recipient: string): Email {
|
|
2602
2605
|
return new Email({
|
|
2603
2606
|
from: email.from,
|
|
2604
|
-
to: [
|
|
2605
|
-
cc: [],
|
|
2606
|
-
bcc: [],
|
|
2607
|
+
to: [...email.to],
|
|
2608
|
+
cc: [...email.cc],
|
|
2609
|
+
bcc: [...email.bcc],
|
|
2610
|
+
envelopeTo: [recipient],
|
|
2607
2611
|
subject: email.subject,
|
|
2608
2612
|
text: email.text,
|
|
2609
2613
|
html: email.html,
|
|
@@ -2617,8 +2621,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2617
2621
|
variables: structuredClone(email.variables),
|
|
2618
2622
|
})
|
|
2619
2623
|
.setMessageId(email.getMessageId())
|
|
2620
|
-
.setEnvelopeFrom(email.getEnvelopeFrom())
|
|
2621
|
-
.setEnvelopeTo([recipient]);
|
|
2624
|
+
.setEnvelopeFrom(email.getEnvelopeFrom());
|
|
2622
2625
|
}
|
|
2623
2626
|
|
|
2624
2627
|
private scopeSessionForRoutingRecipient(
|
|
@@ -3566,9 +3569,12 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3566
3569
|
return new Email({
|
|
3567
3570
|
// The first mailbox of the From fields in message order, groups flattened
|
|
3568
3571
|
from: headerFromMailboxes[0] || session.envelope.mailFrom.address,
|
|
3569
|
-
|
|
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),
|
|
3570
3575
|
cc: parsed.cc.map(recipient => recipient.email),
|
|
3571
3576
|
bcc: [],
|
|
3577
|
+
envelopeTo: envelopeRecipients,
|
|
3572
3578
|
subject: parsed.subject || '',
|
|
3573
3579
|
text: bodies.text,
|
|
3574
3580
|
html: bodies.html,
|
|
@@ -3579,7 +3585,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3579
3585
|
contentType: attachment.contentType,
|
|
3580
3586
|
...(attachment.contentId ? { contentId: attachment.contentId } : {}),
|
|
3581
3587
|
})),
|
|
3582
|
-
})
|
|
3588
|
+
});
|
|
3583
3589
|
}
|
|
3584
3590
|
|
|
3585
3591
|
private isStandardParsedHeader(headerName: string): boolean {
|
|
@@ -4298,6 +4304,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4298
4304
|
): Promise<string> {
|
|
4299
4305
|
this.assertAcceptingOutboundWork();
|
|
4300
4306
|
const recipients = email.getAllRecipients();
|
|
4307
|
+
// What goes to the queue; the caller's email is never changed.
|
|
4308
|
+
let outgoing = email;
|
|
4301
4309
|
logger.log('info', `Sending email: ${email.subject} to ${recipients.join(', ')}`);
|
|
4302
4310
|
|
|
4303
4311
|
try {
|
|
@@ -4331,12 +4339,17 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4331
4339
|
}
|
|
4332
4340
|
|
|
4333
4341
|
// The message's fields stay as written; the suppressed recipients
|
|
4334
|
-
// only leave the envelope.
|
|
4335
|
-
|
|
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());
|
|
4336
4349
|
}
|
|
4337
4350
|
}
|
|
4338
4351
|
|
|
4339
|
-
const id = await this.deliveryQueue.enqueue(
|
|
4352
|
+
const id = await this.deliveryQueue.enqueue(outgoing, mode, route);
|
|
4340
4353
|
|
|
4341
4354
|
logger.log('info', `Email queued with ID: ${id}`);
|
|
4342
4355
|
return id;
|