@push.rocks/smartmta 13.1.0 → 14.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/changelog.md +23 -0
- package/dist_rust/mailer-bin_linux_amd64_musl +0 -0
- package/dist_rust/mailer-bin_linux_amd64_musl.third-party-notices.json +0 -51
- package/dist_rust/mailer-bin_linux_amd64_musl.third-party-notices.txt +1 -291
- 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.third-party-notices.json +0 -51
- package/dist_rust/mailer-bin_linux_arm64_musl.third-party-notices.txt +1 -291
- 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/routing/classes.unified.email.server.d.ts +17 -14
- package/dist_ts/mail/routing/classes.unified.email.server.js +80 -71
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +108 -1
- package/dist_ts/security/classes.rustsecuritybridge.js +187 -50
- package/native-notices/manifest.json +1 -64
- package/native-notices/readme.md +3 -3
- package/package.json +9 -10
- package/readme.md +20 -12
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/routing/classes.unified.email.server.ts +82 -75
- package/ts/security/classes.rustsecuritybridge.ts +264 -49
- package/native-notices/crates/charset-0.1.5/copyright +0 -12
- package/native-notices/crates/charset-0.1.5/license-apache +0 -202
- package/native-notices/crates/charset-0.1.5/license-mit +0 -25
- package/native-notices/crates/mailparse-0.16.1/license +0 -13
- package/native-notices/crates/quoted_printable-0.5.2/license +0 -13
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"bzip2@0.6.1",
|
|
28
28
|
"cc@1.3.0",
|
|
29
29
|
"cfg-if@1.0.4",
|
|
30
|
-
"charset@0.1.5",
|
|
31
30
|
"cipher@0.4.4",
|
|
32
31
|
"clap@4.6.3",
|
|
33
32
|
"clap_builder@4.6.2",
|
|
@@ -102,7 +101,6 @@
|
|
|
102
101
|
"mail-auth@0.7.5",
|
|
103
102
|
"mail-builder@0.4.4",
|
|
104
103
|
"mail-parser@0.11.5",
|
|
105
|
-
"mailparse@0.16.1",
|
|
106
104
|
"memchr@2.8.3",
|
|
107
105
|
"miniz_oxide@0.8.9",
|
|
108
106
|
"mio@1.2.2",
|
|
@@ -126,7 +124,6 @@
|
|
|
126
124
|
"quick-xml@0.38.4",
|
|
127
125
|
"quick_cache@0.6.24",
|
|
128
126
|
"quote@1.0.47",
|
|
129
|
-
"quoted_printable@0.5.2",
|
|
130
127
|
"rand@0.9.5",
|
|
131
128
|
"rand_chacha@0.9.0",
|
|
132
129
|
"rand_core@0.9.5",
|
|
@@ -232,7 +229,6 @@
|
|
|
232
229
|
"bzip2@0.6.1",
|
|
233
230
|
"cc@1.3.0",
|
|
234
231
|
"cfg-if@1.0.4",
|
|
235
|
-
"charset@0.1.5",
|
|
236
232
|
"cipher@0.4.4",
|
|
237
233
|
"clap@4.6.3",
|
|
238
234
|
"clap_builder@4.6.2",
|
|
@@ -307,7 +303,6 @@
|
|
|
307
303
|
"mail-auth@0.7.5",
|
|
308
304
|
"mail-builder@0.4.4",
|
|
309
305
|
"mail-parser@0.11.5",
|
|
310
|
-
"mailparse@0.16.1",
|
|
311
306
|
"memchr@2.8.3",
|
|
312
307
|
"miniz_oxide@0.8.9",
|
|
313
308
|
"mio@1.2.2",
|
|
@@ -331,7 +326,6 @@
|
|
|
331
326
|
"quick-xml@0.38.4",
|
|
332
327
|
"quick_cache@0.6.24",
|
|
333
328
|
"quote@1.0.47",
|
|
334
|
-
"quoted_printable@0.5.2",
|
|
335
329
|
"rand@0.9.5",
|
|
336
330
|
"rand_chacha@0.9.0",
|
|
337
331
|
"rand_core@0.9.5",
|
|
@@ -761,30 +755,6 @@
|
|
|
761
755
|
}
|
|
762
756
|
]
|
|
763
757
|
},
|
|
764
|
-
{
|
|
765
|
-
"name": "charset",
|
|
766
|
-
"version": "0.1.5",
|
|
767
|
-
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
768
|
-
"license": "Apache-2.0 OR MIT",
|
|
769
|
-
"files": [
|
|
770
|
-
{
|
|
771
|
-
"path": "crates/charset-0.1.5/copyright",
|
|
772
|
-
"licenses": []
|
|
773
|
-
},
|
|
774
|
-
{
|
|
775
|
-
"path": "crates/charset-0.1.5/license-apache",
|
|
776
|
-
"licenses": [
|
|
777
|
-
"Apache-2.0"
|
|
778
|
-
]
|
|
779
|
-
},
|
|
780
|
-
{
|
|
781
|
-
"path": "crates/charset-0.1.5/license-mit",
|
|
782
|
-
"licenses": [
|
|
783
|
-
"MIT"
|
|
784
|
-
]
|
|
785
|
-
}
|
|
786
|
-
]
|
|
787
|
-
},
|
|
788
758
|
{
|
|
789
759
|
"name": "cipher",
|
|
790
760
|
"version": "0.4.4",
|
|
@@ -2223,20 +2193,6 @@
|
|
|
2223
2193
|
}
|
|
2224
2194
|
]
|
|
2225
2195
|
},
|
|
2226
|
-
{
|
|
2227
|
-
"name": "mailparse",
|
|
2228
|
-
"version": "0.16.1",
|
|
2229
|
-
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
2230
|
-
"license": "0BSD",
|
|
2231
|
-
"files": [
|
|
2232
|
-
{
|
|
2233
|
-
"path": "crates/mailparse-0.16.1/license",
|
|
2234
|
-
"licenses": [
|
|
2235
|
-
"0BSD"
|
|
2236
|
-
]
|
|
2237
|
-
}
|
|
2238
|
-
]
|
|
2239
|
-
},
|
|
2240
2196
|
{
|
|
2241
2197
|
"name": "memchr",
|
|
2242
2198
|
"version": "2.8.3",
|
|
@@ -2688,20 +2644,6 @@
|
|
|
2688
2644
|
}
|
|
2689
2645
|
]
|
|
2690
2646
|
},
|
|
2691
|
-
{
|
|
2692
|
-
"name": "quoted_printable",
|
|
2693
|
-
"version": "0.5.2",
|
|
2694
|
-
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
2695
|
-
"license": "0BSD",
|
|
2696
|
-
"files": [
|
|
2697
|
-
{
|
|
2698
|
-
"path": "crates/quoted_printable-0.5.2/license",
|
|
2699
|
-
"licenses": [
|
|
2700
|
-
"0BSD"
|
|
2701
|
-
]
|
|
2702
|
-
}
|
|
2703
|
-
]
|
|
2704
|
-
},
|
|
2705
2647
|
{
|
|
2706
2648
|
"name": "rand",
|
|
2707
2649
|
"version": "0.9.5",
|
|
@@ -4451,9 +4393,6 @@
|
|
|
4451
4393
|
"crates/cc-1.3.0/license-mit": "378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397",
|
|
4452
4394
|
"crates/cfg-if-1.0.4/license-apache": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
|
4453
4395
|
"crates/cfg-if-1.0.4/license-mit": "378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397",
|
|
4454
|
-
"crates/charset-0.1.5/copyright": "5918196cdc0b35bca82b9b21b775b4e344dafe95ca9b6b716906c36b4a1161da",
|
|
4455
|
-
"crates/charset-0.1.5/license-apache": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
|
4456
|
-
"crates/charset-0.1.5/license-mit": "3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c",
|
|
4457
4396
|
"crates/cipher-0.4.4/license-apache": "a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5",
|
|
4458
4397
|
"crates/cipher-0.4.4/license-mit": "5c7bd92d1f096f12203dc1b601e3cb17484fa1b023e30b6b8edcc80e416237ec",
|
|
4459
4398
|
"crates/clap-4.6.3/license-apache": "c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08",
|
|
@@ -4594,7 +4533,6 @@
|
|
|
4594
4533
|
"crates/mail-builder-0.4.4/mit.txt": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
|
4595
4534
|
"crates/mail-parser-0.11.5/apache-2.0.txt": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
|
4596
4535
|
"crates/mail-parser-0.11.5/mit.txt": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
|
4597
|
-
"crates/mailparse-0.16.1/license": "b3d6ddb122348e7ce92973bdafd8fb0b643beb3ecdfc32c15d73fb44c7f5ba62",
|
|
4598
4536
|
"crates/memchr-2.8.3/copying": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f",
|
|
4599
4537
|
"crates/memchr-2.8.3/license-mit": "0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f",
|
|
4600
4538
|
"crates/memchr-2.8.3/unlicense": "7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c",
|
|
@@ -4640,7 +4578,6 @@
|
|
|
4640
4578
|
"crates/quick_cache-0.6.24/licence.md": "c845c85aa74d7eb1fb6848d2d96360a5edfe51dfbacbf96d7bb5193951e79b1a",
|
|
4641
4579
|
"crates/quote-1.0.47/license-apache": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
|
4642
4580
|
"crates/quote-1.0.47/license-mit": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
|
4643
|
-
"crates/quoted_printable-0.5.2/license": "b3d6ddb122348e7ce92973bdafd8fb0b643beb3ecdfc32c15d73fb44c7f5ba62",
|
|
4644
4581
|
"crates/rand-0.9.5/copyright": "90eb64f0279b0d9432accfa6023ff803bc4965212383697eee27a0f426d5f8d5",
|
|
4645
4582
|
"crates/rand-0.9.5/license-apache": "35242e7a83f69875e6edeff02291e688c97caafe2f8902e4e19b49d3e78b4cab",
|
|
4646
4583
|
"crates/rand-0.9.5/license-mit": "209fbbe0ad52d9235e37badf9cadfe4dbdc87203179c0899e738b39ade42177b",
|
|
@@ -4792,7 +4729,7 @@
|
|
|
4792
4729
|
"native/tikv-jemalloc-sys-0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8/jemalloc-5.3.1/jemalloc-copying": "94aa2caa98c25d942f58b956c71dba6a99ff98fc3a31cbc669fe2a4cd0268b53",
|
|
4793
4730
|
"native/zstd-sys-2.0.16+zstd.1.5.7/zstd-1.5.7/zstd-copying": "f9c375a1be4a41f7b70301dd83c91cb89e41567478859b77eef375a52d782505",
|
|
4794
4731
|
"native/zstd-sys-2.0.16+zstd.1.5.7/zstd-1.5.7/zstd-license": "7055266497633c9025b777c78eb7235af13922117480ed5c674677adc381c9d8",
|
|
4795
|
-
"readme.md": "
|
|
4732
|
+
"readme.md": "c13dc71596cdeebe53a22c7c9c8446347ee56d0abf6bd3ec9bf2a2acb0444da3",
|
|
4796
4733
|
"runtime/gcc/gpl-3.0.txt": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
|
|
4797
4734
|
"runtime/gcc/runtime-library-exception-3.1.txt": "9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74",
|
|
4798
4735
|
"runtime/libunwind-22.1.2/license.txt": "b5efebcaca80879234098e52d1725e6d9eb8fb96a19fce625d39184b705f7b6d",
|
package/native-notices/readme.md
CHANGED
|
@@ -12,12 +12,12 @@ package's own code is covered by its own license.
|
|
|
12
12
|
|
|
13
13
|
| Target | Rust target | Linkage | Cargo packages |
|
|
14
14
|
| --- | --- | --- | --- |
|
|
15
|
-
| linux_amd64_musl | x86_64-unknown-linux-musl | static musl |
|
|
16
|
-
| linux_arm64_musl | aarch64-unknown-linux-musl | static musl |
|
|
15
|
+
| linux_amd64_musl | x86_64-unknown-linux-musl | static musl | 190 |
|
|
16
|
+
| linux_arm64_musl | aarch64-unknown-linux-musl | static musl | 190 |
|
|
17
17
|
|
|
18
18
|
## Contents
|
|
19
19
|
|
|
20
|
-
- `crates/`: the license and notice files of the
|
|
20
|
+
- `crates/`: the license and notice files of the 190 third-party Cargo packages compiled
|
|
21
21
|
for these targets (normal and build dependencies, including procedural macros;
|
|
22
22
|
development dependencies are excluded), copied verbatim. `manifest.json` records each
|
|
23
23
|
package's license expression, the texts that cover it, and the targets it is built for.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartmta",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"description": "A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mta",
|
|
@@ -31,16 +31,15 @@
|
|
|
31
31
|
".": "./dist_ts/index.js"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@git.zone/cli": "^
|
|
35
|
-
"@git.zone/
|
|
36
|
-
"@git.zone/
|
|
37
|
-
"@git.zone/tstest": "^8.0.0",
|
|
34
|
+
"@git.zone/cli": "^11.0.1",
|
|
35
|
+
"@git.zone/rsrepo": "^1.1.1",
|
|
36
|
+
"@git.zone/tsrepo": "^1.1.1",
|
|
38
37
|
"@types/node": "^26.6.5",
|
|
39
38
|
"tsx": "^4.23.15"
|
|
40
39
|
},
|
|
41
40
|
"dependencies": {
|
|
42
41
|
"@push.rocks/smartlog": "^5.0.1",
|
|
43
|
-
"@push.rocks/smartmail": "^3.
|
|
42
|
+
"@push.rocks/smartmail": "^3.2.0",
|
|
44
43
|
"@push.rocks/smartpath": "^6.0.0",
|
|
45
44
|
"@push.rocks/smartrust": "^2.6.0",
|
|
46
45
|
"@tsclass/tsclass": "^9.5.0",
|
|
@@ -69,10 +68,10 @@
|
|
|
69
68
|
"access": "public"
|
|
70
69
|
},
|
|
71
70
|
"scripts": {
|
|
72
|
-
"test": "
|
|
71
|
+
"test": "tsrepo test test/ --logfile --verbose --timeout 60",
|
|
73
72
|
"test:rust": "cd rust && cargo test",
|
|
74
|
-
"build": "
|
|
75
|
-
"build:release": "(
|
|
76
|
-
"check": "
|
|
73
|
+
"build": "tsrepo build tsfolders && rsrepo",
|
|
74
|
+
"build:release": "(tsrepo build tsfolders) && (rsrepo --configured-targets) && (rsrepo verify)",
|
|
75
|
+
"check": "tsrepo check"
|
|
77
76
|
}
|
|
78
77
|
}
|
package/readme.md
CHANGED
|
@@ -355,7 +355,7 @@ Without this, an authenticated submission addressed to a domain this server host
|
|
|
355
355
|
|
|
356
356
|
`hooks.onMessageData` runs after `DATA` and before the final SMTP response — but only for envelopes with **no** resolved `store` or `deliver` action; terminal local deliveries go through `hooks.onAcceptEnvelope` instead and never invoke `onMessageData`. Its `rawMessage` holds the same exact received bytes as in `hooks.onAcceptEnvelope`, final CRLF included. Return `{ accepted: true }` only after your application has accepted responsibility for the message, such as after durable persistence. Set `continueProcessing: false` when the consumer owns all downstream processing.
|
|
357
357
|
|
|
358
|
-
`hooks.onAcceptEnvelope` is the terminal durable owner for any envelope containing a resolved `store` or `deliver` action. It receives the exact raw RFC822 bytes (`rawMessage`): the message as the client sent it, with only the DATA dot-stuffing removed and the CRLF that ends the last line kept (RFC 5321 §4.1.1.4: the first CRLF of `<CRLF>.<CRLF>` belongs to the message), so the bytes can be relayed unchanged through `enqueueRawEmail()`, `sendRawOutboundEmail()` or an unsigned raw relay. The listener refuses a message that contains a bare CR or bare LF (RFC 5321 §2.3.8) with `550 5.6.0` after reading it to its end-of-data marker, without calling a hook, so every handed-off message uses canonical CRLF line endings. It also receives the complete per-recipient route resolution (`resolvedRecipientRoutes`), and the Rust security verdicts (`securityResults`). It must durably persist the message before resolving — the SMTP `250` is only sent after the hook resolves. Throwing an `AcceptEnvelopeRejectionError` with a `5xx` code rejects the envelope permanently with that status; any other error or a `smtp.messageAcceptanceTimeoutMs` timeout refuses the envelope with a temporary `451`. Use `createAcceptedEnvelopeDispatchPlan()` inside the hook to snapshot the resolved plan for later replay-safe `dispatchAcceptedEnvelope()` calls. Both `AcceptEnvelopeRejectionError` and `IAcceptEnvelopeContext` are exported.
|
|
358
|
+
`hooks.onAcceptEnvelope` is the terminal durable owner for any envelope containing a resolved `store` or `deliver` action. It receives the exact raw RFC822 bytes (`rawMessage`): the message as the client sent it, with only the DATA dot-stuffing removed and the CRLF that ends the last line kept (RFC 5321 §4.1.1.4: the first CRLF of `<CRLF>.<CRLF>` belongs to the message), so the bytes can be relayed unchanged through `enqueueRawEmail()`, `sendRawOutboundEmail()` or an unsigned raw relay. The listener refuses a message that contains a bare CR or bare LF (RFC 5321 §2.3.8) with `550 5.6.0` after reading it to its end-of-data marker, without calling a hook, so every handed-off message uses canonical CRLF line endings. It also receives the complete per-recipient route resolution (`resolvedRecipientRoutes`), and the Rust security verdicts (`securityResults`). It must durably persist the message before resolving — the SMTP `250` is only sent after the hook resolves. Throwing an `AcceptEnvelopeRejectionError` with a `5xx` code rejects the envelope permanently with that status; any other error or a `smtp.messageAcceptanceTimeoutMs` timeout refuses the envelope with a temporary `451`. Use `createAcceptedEnvelopeDispatchPlan()` inside the hook to snapshot the resolved plan for later replay-safe `dispatchAcceptedEnvelope()` calls on a started server: a replay that builds an `Email` parses the stored bytes with the Rust parser of the DATA path (`parseMessage`). Both `AcceptEnvelopeRejectionError` and `IAcceptEnvelopeContext` are exported.
|
|
359
359
|
|
|
360
360
|
Use `smtp.securePort` when the implicit TLS listener uses a remapped backend port instead of public `465`; the configured secure port must also be present in `ports`, otherwise `start()` throws.
|
|
361
361
|
|
|
@@ -646,6 +646,8 @@ Embedders can check `smartMtaCapabilities.selectorCorrectDkimSigning`, `callerMa
|
|
|
646
646
|
|
|
647
647
|
All verification is powered by the Rust binary. For inbound mail, `UnifiedEmailServer` runs the full security pipeline **automatically** — DKIM, SPF, DMARC, content scanning, and IP reputation in a single Rust pass. Results are attached as headers (`Received-SPF`, `X-DKIM-Result`, `X-DMARC-Result`).
|
|
648
648
|
|
|
649
|
+
A From that names more than one domain (one field with several domains, or several From fields) cannot be evaluated for DMARC (RFC 9989 §5.3.1); such a message gets `X-DMARC-Result: quarantine` and is marked as potential spam (§11.5). When no DMARC policy was evaluated (no From domain, no DMARC record, or a DNS error), the action is `none`, never `pass`.
|
|
650
|
+
|
|
649
651
|
For direct TypeScript usage, `SpfVerifier` is exported as a lightweight SPF record parser. DKIM signing/key management is handled through `DKIMCreator` and the Rust bridge; SPF, DKIM, and DMARC verification results are produced by the automatic inbound security pipeline.
|
|
650
652
|
|
|
651
653
|
```typescript
|
|
@@ -901,18 +903,23 @@ The Rust `mailer-bin` process is supervised by `RustSecurityBridge`:
|
|
|
901
903
|
|
|
902
904
|
Tune with `RustSecurityBridge.configure({ maxRestartAttempts, healthCheckIntervalMs, healthCheckTimeoutMs, restartBackoffBaseMs, restartBackoffMaxMs })`.
|
|
903
905
|
|
|
906
|
+
### 📏 IPC Line Cap
|
|
907
|
+
|
|
908
|
+
Every JSON line between TypeScript and `mailer-bin` is limited by one cap, `4 × maxMessageSize` bytes (41,943,040 at the 10 MiB default). `UnifiedEmailServer` sets it through `RustSecurityBridge.setMaxMessageSize(maxMessageSize)` before the bridge starts; the bridge passes the same value to smartrust (`maxPayloadSize`) and to `mailer-bin --max-line-bytes`, so both ends of the transport agree, and a changed `maxMessageSize` (also through `updateOptions()`, which restarts the listener) restarts the process with the new cap. Rust never writes a line over the cap: a received message whose `emailReceived` event would exceed it is refused at DATA with `552 5.3.4 Parsed message size exceeds maximum of <cap> bytes`. The outbound requests `sendEmail`, `sendRawEmail`, `signDkim` and `verifyDkim` are limited by the same cap: a request line above it is refused by smartrust before anything is written (`Outbound message exceeds maxPayloadSize`), which is about 3 × `maxMessageSize` of message.
|
|
909
|
+
|
|
904
910
|
## 🦀 Rust Acceleration Layer
|
|
905
911
|
|
|
906
|
-
Performance-critical operations are implemented in Rust and communicate with the TypeScript runtime via `@push.rocks/smartrust` (JSON-over-stdin/stdout IPC). The Rust workspace lives at `rust/` with
|
|
912
|
+
Performance-critical operations are implemented in Rust and communicate with the TypeScript runtime via `@push.rocks/smartrust` (JSON-over-stdin/stdout IPC). The Rust workspace lives at `rust/` with five crates:
|
|
907
913
|
|
|
908
914
|
| Crate | Status | Purpose |
|
|
909
915
|
|---|---|---|
|
|
916
|
+
| `mailer-mime` | Complete | The inbound RFC 5322 / MIME parser: one parse per message at DATA, for the content scan, DMARC alignment and the `emailReceived` event's `parsed` result, from which TypeScript builds the `Email` (stored bytes through `parseMessage`); a clean-room implementation of `docs/cleanroom/mailer-mime.md` |
|
|
910
917
|
| `mailer-core` | Complete | Email types, validation, MIME building, bounce detection |
|
|
911
918
|
| `mailer-security` | Complete | DKIM sign/verify, SPF, DMARC, IP reputation/DNSBL, content scanning |
|
|
912
919
|
| `mailer-smtp` | Complete | Full SMTP protocol engine — TCP/TLS server + client, STARTTLS, AUTH, pipelining, connection pooling, in-process security pipeline |
|
|
913
920
|
| `mailer-bin` | Complete | CLI + smartrust IPC bridge — wires everything together |
|
|
914
921
|
|
|
915
|
-
`RustSecurityBridge` finds `mailer-bin` in the package's `dist_rust/` (or a local `rust/target` build). Inside a Node.js single executable application (built with `@git.zone/
|
|
922
|
+
`RustSecurityBridge` finds `mailer-bin` in the package's `dist_rust/` (or a local `rust/target` build). Inside a Node.js single executable application (built with `tsrepo sea` from `@git.zone/tsrepo`), smartrust runs the sidecar the executable embeds under the name `mailer-bin` instead and searches nothing else; a sidecar the executable cannot provide makes `start()` resolve `false`, and background restarts look it up again.
|
|
916
923
|
|
|
917
924
|
The shipped Linux binaries are static musl executables. The SMTP client connects with `TcpStream::connect("host:port")` (`rust/crates/mailer-smtp/src/client/connection.rs`), so a host name it connects to (an MX or smarthost name, or a `connect_host`) is looked up through the C library, which is musl: only `/etc/hosts` and `/etc/resolv.conf` are consulted, and no NSS modules (`nsswitch.conf`, systemd-resolved's NSS module, LDAP and the like) take part. The SPF, DKIM, DMARC and DNSBL lookups use the hickory resolver instead.
|
|
918
925
|
|
|
@@ -929,6 +936,7 @@ The shipped Linux binaries are static musl executables. The SMTP client connects
|
|
|
929
936
|
| Content scanning (text patterns) | 🦀 Rust | Regex engine performance |
|
|
930
937
|
| Bounce detection (pattern matching) | 🦀 Rust | Regex engine performance |
|
|
931
938
|
| Email validation & MIME building | 🦀 Rust | Parsing performance |
|
|
939
|
+
| Inbound MIME parsing (`mailer-mime`, at DATA and through `parseMessage`) | 🦀 Rust | One linear-time parse with explicit limits for hostile input |
|
|
932
940
|
| Email routing & orchestration | 🟦 TypeScript | Business logic, flexibility |
|
|
933
941
|
| Delivery queue & retry | 🟦 TypeScript | State management, persistence |
|
|
934
942
|
| Template rendering | 🟦 TypeScript | String interpolation |
|
|
@@ -968,10 +976,10 @@ pnpm run build
|
|
|
968
976
|
pnpm test
|
|
969
977
|
|
|
970
978
|
# Run specific test files
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
979
|
+
pnpm exec tsrepo test test/test.e2e.server-lifecycle.node.ts --verbose --timeout 60
|
|
980
|
+
pnpm exec tsrepo test test/test.e2e.inbound-smtp.node.ts --verbose --timeout 60
|
|
981
|
+
pnpm exec tsrepo test test/test.e2e.routing-actions.node.ts --verbose --timeout 60
|
|
982
|
+
pnpm exec tsrepo test test/test.e2e.outbound-delivery.node.ts --verbose --timeout 60
|
|
975
983
|
```
|
|
976
984
|
|
|
977
985
|
**E2E tests** exercise the full pipeline — starting `UnifiedEmailServer`, connecting via raw TCP sockets, sending SMTP transactions, verifying routing actions, and testing outbound delivery through a mock SMTP receiver.
|
|
@@ -1009,15 +1017,15 @@ third-party works too.
|
|
|
1009
1017
|
|
|
1010
1018
|
Every binary carries its notices beside it in `dist_rust/`:
|
|
1011
1019
|
`mailer-bin_<target>.third-party-notices.json` and `.txt`, in the format that
|
|
1012
|
-
[`@git.zone/
|
|
1020
|
+
[`@git.zone/tsrepo`](https://code.foss.global/git.zone/tsrepo)'s `sea` build merges into a
|
|
1013
1021
|
single executable application. Keep that pair with any separately distributed
|
|
1014
|
-
copy of a `mailer-bin` executable. `@git.zone/
|
|
1022
|
+
copy of a `mailer-bin` executable. [`@git.zone/rsrepo`](https://code.foss.global/git.zone/rsrepo) projects them from the
|
|
1015
1023
|
committed `native-notices/` directory, which ships in the npm package as well.
|
|
1016
|
-
`
|
|
1024
|
+
`rsrepo notices` generates that directory from `rust/Cargo.lock`, the toolchain
|
|
1017
1025
|
pinned in `rust-toolchain.toml` and the `notices` overrides in `.smartconfig.json`
|
|
1018
1026
|
(reviewed project texts live in `legal/`); every build checks it and
|
|
1019
|
-
`
|
|
1020
|
-
`pnpm exec
|
|
1027
|
+
`rsrepo verify` checks the release binaries' notices. Regenerate it with
|
|
1028
|
+
`pnpm exec rsrepo notices` after any change to the Cargo graph, the toolchain or
|
|
1021
1029
|
the targets, and review the diff.
|
|
1022
1030
|
|
|
1023
1031
|
## License and Legal Information
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { Email } from '../core/classes.email.js';
|
|
|
37
37
|
import { DomainRegistry } from './classes.domain.registry.js';
|
|
38
38
|
import { DnsManager } from './classes.dns.manager.js';
|
|
39
39
|
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
|
40
|
-
import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js';
|
|
40
|
+
import type { ISmtpSendResult, IOutboundEmail, IBridgeParsedMessage } from '../../security/classes.rustsecuritybridge.js';
|
|
41
41
|
import { MultiModeDeliverySystem, type IDeliveryStats, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
|
42
42
|
import {
|
|
43
43
|
ACCEPTED_ENVELOPE_IDEMPOTENCY_RETENTION_MS,
|
|
@@ -1383,6 +1383,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1383
1383
|
try {
|
|
1384
1384
|
await this.startDeliveryPipeline();
|
|
1385
1385
|
this.startAcceptedEnvelopeDispatchCleanup();
|
|
1386
|
+
// The IPC line cap derives from the SMTP size limit and is fixed per mailer-bin process,
|
|
1387
|
+
// so it is set before the bridge starts (and before any parseMessage call).
|
|
1388
|
+
await this.rustBridge.setMaxMessageSize(this.getMaxMessageSize());
|
|
1386
1389
|
await this.startRustBridge();
|
|
1387
1390
|
await this.initializeDkimAndDns();
|
|
1388
1391
|
await this.startDeliveryProcessing();
|
|
@@ -1419,6 +1422,11 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1419
1422
|
logger.log('info', 'Email delivery system started');
|
|
1420
1423
|
}
|
|
1421
1424
|
|
|
1425
|
+
/** The SMTP size limit, which the bridge's IPC line cap derives from. */
|
|
1426
|
+
private getMaxMessageSize(): number {
|
|
1427
|
+
return this.options.maxMessageSize || 10 * 1024 * 1024;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1422
1430
|
private async startRustBridge(): Promise<void> {
|
|
1423
1431
|
const bridgeOk = await this.rustBridge.start();
|
|
1424
1432
|
if (!bridgeOk) {
|
|
@@ -1618,7 +1626,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1618
1626
|
tlsCertPem,
|
|
1619
1627
|
tlsKeyPem,
|
|
1620
1628
|
...(tlsTerminatedPorts.length > 0 ? { tlsTerminatedPorts } : {}),
|
|
1621
|
-
maxMessageSize: this.
|
|
1629
|
+
maxMessageSize: this.getMaxMessageSize(),
|
|
1622
1630
|
maxConnections: this.options.maxConnections || this.options.maxClients || 100,
|
|
1623
1631
|
maxRecipients: 100,
|
|
1624
1632
|
connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30,
|
|
@@ -1727,6 +1735,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1727
1735
|
this.smtpListenerActive = false;
|
|
1728
1736
|
await this.rustBridge.stopSmtpServer();
|
|
1729
1737
|
if (this.smtpListenerShutdown) return;
|
|
1738
|
+
// With the listener stopped, a changed maximum message size restarts the bridge with
|
|
1739
|
+
// the new line cap before the listener comes back with the new size.
|
|
1740
|
+
await this.rustBridge.setMaxMessageSize(this.getMaxMessageSize());
|
|
1730
1741
|
await this.startSmtpServer();
|
|
1731
1742
|
})
|
|
1732
1743
|
.catch(async (error) => {
|
|
@@ -2479,36 +2490,15 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2479
2490
|
|
|
2480
2491
|
/**
|
|
2481
2492
|
* The mailbox addresses of every From header field, one list per field in
|
|
2482
|
-
* message order
|
|
2483
|
-
*
|
|
2484
|
-
*
|
|
2485
|
-
* recipient's client might display. A
|
|
2486
|
-
* as an empty group or an encoded word in
|
|
2487
|
-
* empty list.
|
|
2488
|
-
*/
|
|
2489
|
-
private parseFromHeaderFields(
|
|
2490
|
-
headers: ReadonlyArray<{ key: string; value: string }>,
|
|
2491
|
-
): string[][] {
|
|
2492
|
-
return headers
|
|
2493
|
-
.filter((header) => header.key === 'from')
|
|
2494
|
-
.map((header) =>
|
|
2495
|
-
plugins.smartmail.parseAddressList(header.value).map((mailbox) => mailbox.email),
|
|
2496
|
-
);
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
/**
|
|
2500
|
-
* The From header fields of the header section of a raw message, see
|
|
2501
|
-
* parseFromHeaderFields. Parsing stops at the first CRLFCRLF header
|
|
2502
|
-
* boundary when present (the Rust receiver normalizes line endings to
|
|
2503
|
-
* CRLF); a bodyless message without one is parsed whole.
|
|
2493
|
+
* message order, as the Rust parser read them (`parsed.fromFields`). Groups
|
|
2494
|
+
* are flattened into their member mailboxes, and a repeated From field is
|
|
2495
|
+
* kept rather than collapsed to one occurrence, because scope enforcement and
|
|
2496
|
+
* Email.from must see every mailbox a recipient's client might display. A
|
|
2497
|
+
* field that yields no mailbox, such as an empty group or an encoded word in
|
|
2498
|
+
* place of an address, gives an empty list.
|
|
2504
2499
|
*/
|
|
2505
|
-
private
|
|
2506
|
-
|
|
2507
|
-
const headerSection = headerBoundary === -1
|
|
2508
|
-
? rawMessage
|
|
2509
|
-
: rawMessage.subarray(0, headerBoundary + 4);
|
|
2510
|
-
const parsed = await plugins.smartmail.parseMailMessage(headerSection);
|
|
2511
|
-
return this.parseFromHeaderFields(parsed.headers);
|
|
2500
|
+
private fromFieldAddresses(parsed: IBridgeParsedMessage): string[][] {
|
|
2501
|
+
return parsed.fromFields.map((field) => field.map((mailbox) => mailbox.email));
|
|
2512
2502
|
}
|
|
2513
2503
|
|
|
2514
2504
|
/**
|
|
@@ -2520,7 +2510,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2520
2510
|
*/
|
|
2521
2511
|
private async checkAuthenticatedHeaderFromScope(
|
|
2522
2512
|
email: Email,
|
|
2523
|
-
|
|
2513
|
+
fromFields: string[][],
|
|
2524
2514
|
session: IExtendedSmtpSession,
|
|
2525
2515
|
): Promise<ISmtpDecision | null> {
|
|
2526
2516
|
const username = session.user?.username;
|
|
@@ -2539,13 +2529,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
2539
2529
|
smtpCode: 553,
|
|
2540
2530
|
smtpMessage: `5.7.1 Header From address not permitted for authenticated user ${username}`,
|
|
2541
2531
|
};
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
fromFields = await this.parseHeaderFromFields(rawMessage);
|
|
2545
|
-
} catch {
|
|
2546
|
-
// A header section that cannot be read cannot be shown to be in scope
|
|
2547
|
-
return notPermitted;
|
|
2548
|
-
}
|
|
2532
|
+
// A message the parser rejects never reaches this check: Rust refuses it at
|
|
2533
|
+
// DATA, as it refuses a header section that ends before its empty line.
|
|
2549
2534
|
let fromMailboxes: string[];
|
|
2550
2535
|
if (fromFields.length === 0) {
|
|
2551
2536
|
// A message without a From field is accepted deliberately: Email.from
|
|
@@ -3348,14 +3333,18 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3348
3333
|
}
|
|
3349
3334
|
|
|
3350
3335
|
|
|
3351
|
-
|
|
3336
|
+
// The one parse Rust made at DATA; this path never parses again.
|
|
3337
|
+
if (!data.parsed) {
|
|
3338
|
+
throw new Error('emailReceived event without its parsed message');
|
|
3339
|
+
}
|
|
3340
|
+
const email = this.createEmailFromParsed(data.parsed, session);
|
|
3352
3341
|
|
|
3353
3342
|
// Header-From alignment for scoped authenticated accounts must be
|
|
3354
3343
|
// enforced before any durable acceptance or routing: the RCPT phase
|
|
3355
3344
|
// only ever saw the envelope MAIL FROM.
|
|
3356
3345
|
const headerFromViolation = await this.checkAuthenticatedHeaderFromScope(
|
|
3357
3346
|
email,
|
|
3358
|
-
|
|
3347
|
+
this.fromFieldAddresses(data.parsed),
|
|
3359
3348
|
session,
|
|
3360
3349
|
);
|
|
3361
3350
|
if (headerFromViolation) {
|
|
@@ -3523,45 +3512,58 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3523
3512
|
}
|
|
3524
3513
|
}
|
|
3525
3514
|
|
|
3515
|
+
/**
|
|
3516
|
+
* Builds the Email of stored message bytes (the replay paths
|
|
3517
|
+
* dispatchAcceptedEnvelope and processEmailByMode(Buffer)) with the parser
|
|
3518
|
+
* and setting of the DATA path, through mailer-bin's parseMessage, so a
|
|
3519
|
+
* replay rebuilds the Email the live path built for the same bytes.
|
|
3520
|
+
*/
|
|
3526
3521
|
private async createEmailFromBuffer(emailData: Buffer, session: IExtendedSmtpSession): Promise<Email> {
|
|
3522
|
+
let parsed: IBridgeParsedMessage;
|
|
3527
3523
|
try {
|
|
3528
|
-
|
|
3529
|
-
const parsed = await plugins.smartmail.parseMailMessage(emailData, { rfc822Attachments: true });
|
|
3530
|
-
const headerFromMailboxes = this.parseFromHeaderFields(parsed.headers).flat();
|
|
3531
|
-
const envelopeRecipients = session.envelope.rcptTo.map(recipient => recipient.address);
|
|
3532
|
-
const parsedHeaders = new Map<string, string>();
|
|
3533
|
-
for (const header of parsed.headers) {
|
|
3534
|
-
if (this.isStandardParsedHeader(header.key)) {
|
|
3535
|
-
continue;
|
|
3536
|
-
}
|
|
3537
|
-
// Raw values as written; a repeated field joins its values with ', '
|
|
3538
|
-
const previous = parsedHeaders.get(header.key);
|
|
3539
|
-
parsedHeaders.set(header.key, previous === undefined ? header.value : `${previous}, ${header.value}`);
|
|
3540
|
-
}
|
|
3541
|
-
return new Email({
|
|
3542
|
-
// The first mailbox of the From fields in message order, groups flattened
|
|
3543
|
-
from: headerFromMailboxes[0] || session.envelope.mailFrom.address,
|
|
3544
|
-
to: envelopeRecipients.length > 0
|
|
3545
|
-
? envelopeRecipients
|
|
3546
|
-
: parsed.to.map(recipient => recipient.email),
|
|
3547
|
-
cc: parsed.cc.map(recipient => recipient.email),
|
|
3548
|
-
bcc: [],
|
|
3549
|
-
subject: parsed.subject || '',
|
|
3550
|
-
// A message that carries only HTML is read and scanned through its text
|
|
3551
|
-
text: parsed.text ?? (parsed.html ? plugins.smartmail.htmlToText(parsed.html) : ''),
|
|
3552
|
-
html: parsed.html || undefined,
|
|
3553
|
-
headers: Object.fromEntries(parsedHeaders),
|
|
3554
|
-
attachments: parsed.attachments.map(attachment => ({
|
|
3555
|
-
filename: attachment.filename || '',
|
|
3556
|
-
content: Buffer.from(attachment.content),
|
|
3557
|
-
contentType: attachment.contentType,
|
|
3558
|
-
...(attachment.contentId ? { contentId: attachment.contentId } : {}),
|
|
3559
|
-
})),
|
|
3560
|
-
});
|
|
3524
|
+
parsed = await this.rustBridge.parseMessage(emailData);
|
|
3561
3525
|
} catch (error) {
|
|
3562
3526
|
logger.log('error', `Error parsing email data: ${(error as Error).message}`);
|
|
3563
3527
|
throw new Error(`Error parsing email data: ${(error as Error).message}`);
|
|
3564
3528
|
}
|
|
3529
|
+
return this.createEmailFromParsed(parsed, session);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
/** The Email of a message the Rust parser read (docs/cleanroom/mailer-mime.md section 2.2). */
|
|
3533
|
+
private createEmailFromParsed(parsed: IBridgeParsedMessage, session: IExtendedSmtpSession): Email {
|
|
3534
|
+
const headerFromMailboxes = this.fromFieldAddresses(parsed).flat();
|
|
3535
|
+
const envelopeRecipients = session.envelope.rcptTo.map(recipient => recipient.address);
|
|
3536
|
+
const parsedHeaders = new Map<string, string>();
|
|
3537
|
+
for (const header of parsed.headers) {
|
|
3538
|
+
if (this.isStandardParsedHeader(header.key)) {
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
// Raw values as written; a repeated field joins its values with ', '
|
|
3542
|
+
const previous = parsedHeaders.get(header.key);
|
|
3543
|
+
parsedHeaders.set(header.key, previous === undefined ? header.value : `${previous}, ${header.value}`);
|
|
3544
|
+
}
|
|
3545
|
+
// Each body travels once, as body groups; an HTML-only group is read through
|
|
3546
|
+
// smartmail's HTML-to-text, the only HTML converter.
|
|
3547
|
+
const bodies = plugins.smartmail.joinMailBodyGroups(parsed.bodyGroups);
|
|
3548
|
+
return new Email({
|
|
3549
|
+
// The first mailbox of the From fields in message order, groups flattened
|
|
3550
|
+
from: headerFromMailboxes[0] || session.envelope.mailFrom.address,
|
|
3551
|
+
to: envelopeRecipients.length > 0
|
|
3552
|
+
? envelopeRecipients
|
|
3553
|
+
: parsed.to.map(recipient => recipient.email),
|
|
3554
|
+
cc: parsed.cc.map(recipient => recipient.email),
|
|
3555
|
+
bcc: [],
|
|
3556
|
+
subject: parsed.subject || '',
|
|
3557
|
+
text: bodies.text,
|
|
3558
|
+
html: bodies.html,
|
|
3559
|
+
headers: Object.fromEntries(parsedHeaders),
|
|
3560
|
+
attachments: parsed.attachments.map(attachment => ({
|
|
3561
|
+
filename: attachment.filename || '',
|
|
3562
|
+
content: Buffer.from(attachment.content, 'base64'),
|
|
3563
|
+
contentType: attachment.contentType,
|
|
3564
|
+
...(attachment.contentId ? { contentId: attachment.contentId } : {}),
|
|
3565
|
+
})),
|
|
3566
|
+
});
|
|
3565
3567
|
}
|
|
3566
3568
|
|
|
3567
3569
|
private isStandardParsedHeader(headerName: string): boolean {
|
|
@@ -4141,6 +4143,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4141
4143
|
const tlsTerminatedPortsChanged = options.smtp !== undefined
|
|
4142
4144
|
&& JSON.stringify(options.smtp.tlsTerminatedPorts ?? [])
|
|
4143
4145
|
!== JSON.stringify(this.options.smtp?.tlsTerminatedPorts ?? []);
|
|
4146
|
+
// The size limit is listener configuration and fixes the bridge's IPC line cap.
|
|
4147
|
+
const maxMessageSizeChanged = options.maxMessageSize !== undefined
|
|
4148
|
+
&& options.maxMessageSize !== this.options.maxMessageSize;
|
|
4144
4149
|
|
|
4145
4150
|
if (portsChanged) {
|
|
4146
4151
|
this.stop().then(() => {
|
|
@@ -4188,7 +4193,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
4188
4193
|
? 'SMTP TLS material replaced by configuration update'
|
|
4189
4194
|
: tlsTerminatedPortsChanged
|
|
4190
4195
|
? 'SMTP upstream-terminated port list changed by configuration update'
|
|
4191
|
-
:
|
|
4196
|
+
: maxMessageSizeChanged
|
|
4197
|
+
? 'SMTP maximum message size changed by configuration update'
|
|
4198
|
+
: undefined;
|
|
4192
4199
|
if (listenerRestartReason && this.smtpListenerActive && !this.smtpListenerShutdown) {
|
|
4193
4200
|
this.scheduleSmtpListenerRestart(listenerRestartReason);
|
|
4194
4201
|
}
|