diff --git a/changelog.md b/changelog.md index 58d5dbb..059dd57 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-11 - 4.0.0 - BREAKING CHANGE(smtp-client) +Replace the legacy TypeScript SMTP client with a new Rust-based SMTP client and IPC bridge for outbound delivery + +- Introduce a Rust SMTP client crate with connection handling, TLS, protocol engine, and connection pooling (new modules: connection, pool, protocol, error, config). +- Add IPC handlers and management commands in the Rust binary: sendEmail, sendRawEmail, verifySmtpConnection, closeSmtpPool, getSmtpPoolStatus and integrate a SmtpClientManager into the runtime. +- Update TypeScript bridge (RustSecurityBridge) with new types and methods (ISmtpSendOptions, ISmtpSendResult, verifySmtpConnection, sendOutboundEmail, sendRawEmail, getSmtpPoolStatus, closeSmtpPool) and rework UnifiedEmailServer to use the Rust bridge for outbound delivery and DKIM signing. +- Remove the previous TypeScript SMTP client implementation and associated tests/utilities (many ts/mail/delivery/smtpclient modules and tests deleted) in favor of the Rust implementation. +- Bump dependencies and cargo config: @push.rocks/smartrust to ^1.2.0 in package.json and add/require crates (uuid, base64, webpki-roots) in Rust Cargo files. + ## 2026-02-10 - 3.0.0 - BREAKING CHANGE(security) implement resilience and lifecycle management for RustSecurityBridge (auto-restart, health checks, state machine and eventing); remove legacy TS SMTP test helper and DNSManager; remove deliverability IP-warmup/sender-reputation integrations and related types; drop unused dependencies diff --git a/package.json b/package.json index e5bafc3..f2e55c8 100644 --- a/package.json +++ b/package.json @@ -44,32 +44,12 @@ "tsx": "^4.21.0" }, "dependencies": { - "@api.global/typedrequest": "^3.2.5", - "@api.global/typedserver": "^8.3.0", - "@api.global/typedsocket": "^4.1.0", - "@apiclient.xyz/cloudflare": "^7.1.0", - "@push.rocks/projectinfo": "^5.0.1", - "@push.rocks/qenv": "^6.1.0", - "@push.rocks/smartacme": "^8.0.0", - "@push.rocks/smartdata": "^7.0.15", - "@push.rocks/smartdns": "^7.5.0", "@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfs": "^1.3.1", - "@push.rocks/smartguard": "^3.1.0", - "@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartmail": "^2.2.0", - "@push.rocks/smartmetrics": "^2.0.10", - "@push.rocks/smartnetwork": "^4.0.2", "@push.rocks/smartpath": "^6.0.0", - "@push.rocks/smartpromise": "^4.0.3", - "@push.rocks/smartproxy": "^23.1.0", - "@push.rocks/smartrequest": "^5.0.1", - "@push.rocks/smartrule": "^2.0.1", - "@push.rocks/smartrust": "^1.1.1", - "@push.rocks/smartrx": "^3.0.10", - "@push.rocks/smartunique": "^3.0.9", - "@serve.zone/interfaces": "^5.0.4", + "@push.rocks/smartrust": "^1.2.0", "@tsclass/tsclass": "^9.2.0", "lru-cache": "^11.2.5", "mailparser": "^3.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d58af31..9e45ebc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,84 +8,24 @@ importers: .: dependencies: - '@api.global/typedrequest': - specifier: ^3.2.5 - version: 3.2.5 - '@api.global/typedserver': - specifier: ^8.3.0 - version: 8.3.0(@tiptap/pm@2.27.2) - '@api.global/typedsocket': - specifier: ^4.1.0 - version: 4.1.0(@push.rocks/smartserve@2.0.1) - '@apiclient.xyz/cloudflare': - specifier: ^7.1.0 - version: 7.1.0 - '@push.rocks/projectinfo': - specifier: ^5.0.1 - version: 5.0.2 - '@push.rocks/qenv': - specifier: ^6.1.0 - version: 6.1.3 - '@push.rocks/smartacme': - specifier: ^8.0.0 - version: 8.0.0(socks@2.8.7) - '@push.rocks/smartdata': - specifier: ^7.0.15 - version: 7.0.15(socks@2.8.7) - '@push.rocks/smartdns': - specifier: ^7.5.0 - version: 7.6.1 '@push.rocks/smartfile': specifier: ^13.1.2 version: 13.1.2 '@push.rocks/smartfs': specifier: ^1.3.1 version: 1.3.1 - '@push.rocks/smartguard': - specifier: ^3.1.0 - version: 3.1.0 - '@push.rocks/smartjwt': - specifier: ^2.2.1 - version: 2.2.1 '@push.rocks/smartlog': specifier: ^3.1.8 version: 3.1.10 '@push.rocks/smartmail': specifier: ^2.2.0 version: 2.2.0 - '@push.rocks/smartmetrics': - specifier: ^2.0.10 - version: 2.0.10 - '@push.rocks/smartnetwork': - specifier: ^4.0.2 - version: 4.4.0 '@push.rocks/smartpath': specifier: ^6.0.0 version: 6.0.0 - '@push.rocks/smartpromise': - specifier: ^4.0.3 - version: 4.2.3 - '@push.rocks/smartproxy': - specifier: ^23.1.0 - version: 23.1.0(socks@2.8.7) - '@push.rocks/smartrequest': - specifier: ^5.0.1 - version: 5.0.1 - '@push.rocks/smartrule': - specifier: ^2.0.1 - version: 2.0.1 '@push.rocks/smartrust': - specifier: ^1.1.1 - version: 1.1.1 - '@push.rocks/smartrx': - specifier: ^3.0.10 - version: 3.0.10 - '@push.rocks/smartunique': - specifier: ^3.0.9 - version: 3.0.9 - '@serve.zone/interfaces': - specifier: ^5.0.4 - version: 5.3.0 + specifier: ^1.2.0 + version: 1.2.0 '@tsclass/tsclass': specifier: ^9.2.0 version: 9.3.0 @@ -132,9 +72,6 @@ packages: '@api.global/typedserver@3.0.80': resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==} - '@api.global/typedserver@8.3.0': - resolution: {integrity: sha512-Uh2sQkoQXbsKFb/fhSm7P9oCCEnawGY7R5/9VgCLQUuFV30G0FL0oBTKZNqFli0CNNDDs0nQHE+dpdf4VHhlXQ==} - '@api.global/typedsocket@3.1.1': resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==} peerDependencies: @@ -143,17 +80,6 @@ packages: '@push.rocks/smartserve': optional: true - '@api.global/typedsocket@4.1.0': - resolution: {integrity: sha512-ttmoU5BNHmLAkAF/o+Ta8F5O4F7CUmkFo6LK7NKHQvuYJvodPMYWdhJ6yCINTF4pfCgljkMDUqoVKobm6ea4mQ==} - peerDependencies: - '@push.rocks/smartserve': '>=1.1.0' - - '@apiclient.xyz/cloudflare@6.4.3': - resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==} - - '@apiclient.xyz/cloudflare@7.1.0': - resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -345,9 +271,6 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@design.estate/dees-catalog@3.42.0': - resolution: {integrity: sha512-pArkafnrhRsHsSxKUMUM2YP5ei/AbcchPEKZY2PyHHAdXcNxyT3pE2Oh1FPcs1pqF2LpEgJRq8KFQbFhvhp8Nw==} - '@design.estate/dees-comms@1.0.30': resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} @@ -357,9 +280,6 @@ packages: '@design.estate/dees-element@2.1.6': resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==} - '@design.estate/dees-wcctools@3.8.0': - resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==} - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -525,26 +445,6 @@ packages: cpu: [x64] os: [win32] - '@fortawesome/fontawesome-common-types@7.1.0': - resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} - engines: {node: '>=6'} - - '@fortawesome/fontawesome-svg-core@7.1.0': - resolution: {integrity: sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==} - engines: {node: '>=6'} - - '@fortawesome/free-brands-svg-icons@7.1.0': - resolution: {integrity: sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==} - engines: {node: '>=6'} - - '@fortawesome/free-regular-svg-icons@7.1.0': - resolution: {integrity: sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==} - engines: {node: '>=6'} - - '@fortawesome/free-solid-svg-icons@7.1.0': - resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==} - engines: {node: '>=6'} - '@git.zone/tsbuild@4.1.2': resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==} hasBin: true @@ -637,10 +537,6 @@ packages: resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/cliui@9.0.0': resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} engines: {node: '>=18'} @@ -678,86 +574,12 @@ packages: '@mongodb-js/saslprep@1.4.5': resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==} - '@napi-rs/canvas-android-arm64@0.1.91': - resolution: {integrity: sha512-SLLzXXgSnfct4zy/BVAfweZQkYkPJsNsJ2e5DOE8DFEHC6PufyUrwb12yqeu2So2IOIDpWJJaDAxKY/xpy6MYQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@napi-rs/canvas-darwin-arm64@0.1.91': - resolution: {integrity: sha512-bzdbCjIjw3iRuVFL+uxdSoMra/l09ydGNX9gsBxO/zg+5nlppscIpj6gg+nL6VNG85zwUarDleIrUJ+FWHvmuA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@napi-rs/canvas-darwin-x64@0.1.91': - resolution: {integrity: sha512-q3qpkpw0IsG9fAS/dmcGIhCVoNxj8ojbexZKWwz3HwxlEWsLncEQRl4arnxrwbpLc2nTNTyj4WwDn7QR5NDAaA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': - resolution: {integrity: sha512-Io3g8wJZVhK8G+Fpg1363BE90pIPqg+ZbeehYNxPWDSzbgwU3xV0l8r/JBzODwC7XHi1RpFEk+xyUTMa2POj6w==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@napi-rs/canvas-linux-arm64-gnu@0.1.91': - resolution: {integrity: sha512-HBnto+0rxx1bQSl8bCWA9PyBKtlk2z/AI32r3cu4kcNO+M/5SD4b0v1MWBWZyqMQyxFjWgy3ECyDjDKMC6tY1A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@napi-rs/canvas-linux-arm64-musl@0.1.91': - resolution: {integrity: sha512-/eJtVe2Xw9A86I4kwXpxxoNagdGclu12/NSMsfoL8q05QmeRCbfjhg1PJS7ENAuAvaiUiALGrbVfeY1KU1gztQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@napi-rs/canvas-linux-riscv64-gnu@0.1.91': - resolution: {integrity: sha512-floNK9wQuRWevUhhXRcuis7h0zirdytVxPgkonWO+kQlbvxV7gEUHGUFQyq4n55UHYFwgck1SAfJ1HuXv/+ppQ==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - - '@napi-rs/canvas-linux-x64-gnu@0.1.91': - resolution: {integrity: sha512-c3YDqBdf7KETuZy2AxsHFMsBBX1dWT43yFfWUq+j1IELdgesWtxf/6N7csi3VPf6VA3PmnT9EhMyb+M1wfGtqw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@napi-rs/canvas-linux-x64-musl@0.1.91': - resolution: {integrity: sha512-RpZ3RPIwgEcNBHSHSX98adm+4VP8SMT5FN6250s5jQbWpX/XNUX5aLMfAVJS/YnDjS1QlsCgQxFOPU0aCCWgag==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@napi-rs/canvas-win32-arm64-msvc@0.1.91': - resolution: {integrity: sha512-gF8MBp4X134AgVurxqlCdDA2qO0WaDdi9o6Sd5rWRVXRhWhYQ6wkdEzXNLIrmmros0Tsp2J0hQzx4ej/9O8trQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@napi-rs/canvas-win32-x64-msvc@0.1.91': - resolution: {integrity: sha512-++gtW9EV/neKI8TshD8WFxzBYALSPag2kFRahIJV+LYsyt5kBn21b1dBhEUDHf7O+wiZmuFCeUa7QKGHnYRZBA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@napi-rs/canvas@0.1.91': - resolution: {integrity: sha512-eeIe1GoB74P1B0Nkw6pV8BCQ3hfCfvyYr4BntzlCsnFXzVJiPMDnLeIx3gVB0xQMblHYnjK/0nCLvirEhOjr5g==} - engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@oxc-project/types@0.99.0': resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} @@ -801,10 +623,6 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -846,15 +664,9 @@ packages: '@push.rocks/npmextra@5.3.3': resolution: {integrity: sha512-snLpSHwaQ5OXlZzF1KX/FY71W5LwajjBzor82Vue0smjEPnSeUPY5/JcVdMwtdprdJe13pc/EQQuIiL/zw4/yg==} - '@push.rocks/projectinfo@5.0.2': - resolution: {integrity: sha512-zzieCal6jwR++o+fDl8gMpWkNV2cGEsbT96vCNZu/H9kr0iqRmapOiA4DFadkhOnhlDqvRr6TPaXESu2YUbI8Q==} - '@push.rocks/qenv@6.1.3': resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} - '@push.rocks/smartacme@8.0.0': - resolution: {integrity: sha512-Oq+m+LX4IG0p4qCGZLEwa6UlMo5Hfq7paRjpREwQNsaGSKl23xsjsEJLxjxkePwaXnaIkHEwU/5MtrEkg2uKEQ==} - '@push.rocks/smartarchive@4.2.4': resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} @@ -889,15 +701,9 @@ packages: '@push.rocks/smartdata@5.16.7': resolution: {integrity: sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==} - '@push.rocks/smartdata@7.0.15': - resolution: {integrity: sha512-j09BUekmjiGZuvXmdGBiIpBTXFFnxrzG4rOBjZvPO/hG1BwNrvSkIVq20mIwdYomn8JGgya6oJ4Y7NL+FKTqEA==} - '@push.rocks/smartdelay@3.0.5': resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} - '@push.rocks/smartdns@6.2.2': - resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==} - '@push.rocks/smartdns@7.6.1': resolution: {integrity: sha512-nnP5+A2GOt0WsHrYhtKERmjdEHUchc+QbCCBEqlyeQTn+mNfx2WZvKVI1DFRJt8lamvzxP6Hr/BSe3WHdh4Snw==} @@ -922,9 +728,6 @@ packages: '@push.rocks/smartfile-interfaces@1.0.7': resolution: {integrity: sha512-MeOl/200UOvSO4Pgq/DVFiBVZpL9gjOBQM+4XYNjSxda8c6VBvchHAntaFLQUlO8U1ckNaP9i+nMO4O4/0ymyw==} - '@push.rocks/smartfile@10.0.41': - resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==} - '@push.rocks/smartfile@11.2.7': resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==} @@ -940,9 +743,6 @@ packages: '@push.rocks/smarthash@3.2.6': resolution: {integrity: sha512-Mq/WNX0Tjjes3X1gHd/ZBwOOKSrAG/Z3Xoc0OcCm3P20WKpniihkMpsnlE7wGjvpHLi/ZRe/XkB3KC3d5r9X4g==} - '@push.rocks/smarti18n@1.0.4': - resolution: {integrity: sha512-bHIi9Iuzp2cbux9q79ZK5jOQYPsYJ9zDDS4p/xEPQH31gr0mcFRosLSQb1kvDQDVmUhI0ADlQMqr2ui9zEXQHA==} - '@push.rocks/smartinteract@2.0.16': resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==} @@ -952,9 +752,6 @@ packages: '@push.rocks/smartjson@6.0.0': resolution: {integrity: sha512-FYfJnmukt66WePn6xrVZ3BLmRQl9W82LcsICK3VU9sGW7kasig090jKXPm+yX8ibQcZAO/KyR/Q8tMIYZNxGew==} - '@push.rocks/smartjwt@2.2.1': - resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==} - '@push.rocks/smartlog-destination-devtools@1.0.12': resolution: {integrity: sha512-zvsIkrqByc0JRaBgIyhh+PSz2SY/e/bmhZdUcr/OW6pudgAcqe2sso68EzrKux0w9OMl1P9ZnzF3FpCZPFWD/A==} @@ -979,12 +776,6 @@ packages: '@push.rocks/smartmatch@2.0.0': resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==} - '@push.rocks/smartmetrics@2.0.10': - resolution: {integrity: sha512-Fr4ZzJWFqTR67ThmPsj+uUfPoWZ+p87n7wItpXVjlQ2mf4pboWnsfmrrC1gqIXca1Dwa2i7L+GPoJ3hOZ+Qhfw==} - - '@push.rocks/smartmime@1.0.6': - resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==} - '@push.rocks/smartmime@2.0.4': resolution: {integrity: sha512-mG6lRBLr5nF+GLZmgCcdjhdDsmTtJWBFZDCa1eJ8Au9TvUzbPW0fY5aqJBb3UwfyZzH6St8Th9cJSXjagOQkYA==} @@ -1024,9 +815,6 @@ packages: '@push.rocks/smartpromise@4.2.3': resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} - '@push.rocks/smartproxy@23.1.0': - resolution: {integrity: sha512-2EhMFeQytDwnqooK9BNkLw9oz8M1LUFuMEg6271xRnwf8gUkDq5WT0brrmLdOmpqkU/3h/wDeZUrn65zq3VAcA==} - '@push.rocks/smartpuppeteer@2.0.5': resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} @@ -1042,11 +830,8 @@ packages: '@push.rocks/smartrouter@1.3.3': resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==} - '@push.rocks/smartrule@2.0.1': - resolution: {integrity: sha512-8oYEnS9z+NgCAcUtXPMguYyZpHqA/ROp0bxVQwUaHDwa3YzzA8jHIXvA94hk3sxvkk0xmIpp4UhBEelzIwwJow==} - - '@push.rocks/smartrust@1.1.1': - resolution: {integrity: sha512-NtfTOrVpw0K+z/jW24OmunvZBqkJHfe1tJhTMPFYUb4a5Yt5mtTc3oUvlX+bHarn94Jq0oh0HCLh8xcPQ2Sd7w==} + '@push.rocks/smartrust@1.2.0': + resolution: {integrity: sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==} '@push.rocks/smartrx@3.0.10': resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==} @@ -1072,9 +857,6 @@ packages: '@push.rocks/smartstate@2.0.30': resolution: {integrity: sha512-IuNW8XtSumXIr7g7MIFyWg5PBwLF2mwsymTJbSEycK2Pa9ZLk4yjRHnR907xCilxgiMU9ixQZyNdpa5MMF999A==} - '@push.rocks/smartstream@2.0.8': - resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} - '@push.rocks/smartstream@3.2.5': resolution: {integrity: sha512-PLGGIFDy8JLNVUnnntMSIYN4W081YSbNC7Y/sWpvUT8PAXtbEXXUiDFgK5o3gcI0ptpKQxHAwxhzNlPj0sbFVg==} @@ -1093,10 +875,6 @@ packages: '@push.rocks/smartversion@3.0.5': resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==} - '@push.rocks/smartwatch@6.3.0': - resolution: {integrity: sha512-TeZ1PGBoBMpC4/CK8StIj5InEiFfKp7xWJSm3aYMjB/uaoeRP0vXqv1ORIC/TKYGJuEDuAXUsit8tZVjn0qT1Q==} - engines: {node: '>=20.0.0'} - '@push.rocks/smartxml@2.0.0': resolution: {integrity: sha512-1d06zYJX4Zt8s5w5qFOUg2LAEz9ykrh9d6CQPK4WAgOBIefb1xzVEWHc7yoxicc2OkzNgC3IBCEg3s6BncZKWw==} @@ -1109,9 +887,6 @@ packages: '@push.rocks/taskbuffer@3.5.0': resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==} - '@push.rocks/taskbuffer@4.2.0': - resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==} - '@push.rocks/webrequest@3.0.37': resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==} @@ -1159,9 +934,6 @@ packages: resolution: {integrity: sha512-TxHSar7Cj29E+GOcIj4DeZKWCNVzHKdqnrBRqcBqLqmeYZvzFosLXpFKoaCJDq7MSxuPoCvu5woSdp9YmPXyog==} deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartstring - '@remirror/core-constants@3.0.0': - resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@rolldown/binding-android-arm64@1.0.0-beta.52': resolution: {integrity: sha512-MBGIgysimZPqTDcLXI+i9VveijkP5C3EAncEogXhqfax6YXj1Tr2LY3DVuEOMIjWfMPMhtQSPup4fSTAmgjqIw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1318,9 +1090,6 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@serve.zone/interfaces@5.3.0': - resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} - '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} @@ -1544,31 +1313,6 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@svgdotjs/svg.draggable.js@3.0.6': - resolution: {integrity: sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==} - peerDependencies: - '@svgdotjs/svg.js': ^3.2.4 - - '@svgdotjs/svg.filter.js@3.0.9': - resolution: {integrity: sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==} - engines: {node: '>= 0.8.0'} - - '@svgdotjs/svg.js@3.2.5': - resolution: {integrity: sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==} - - '@svgdotjs/svg.resize.js@2.0.5': - resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==} - engines: {node: '>= 14.18'} - peerDependencies: - '@svgdotjs/svg.js': ^3.2.4 - '@svgdotjs/svg.select.js': ^4.0.1 - - '@svgdotjs/svg.select.js@4.0.3': - resolution: {integrity: sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==} - engines: {node: '>= 14.18'} - peerDependencies: - '@svgdotjs/svg.js': ^3.2.4 - '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -1590,141 +1334,6 @@ packages: vue: optional: true - '@tempfix/webcontainer__api@1.6.1': - resolution: {integrity: sha512-Hgn3cwy0vPzjrVBqeVnY0jNZLaOCW7d+dxBe7Jv9YGHAjJ8udUMS+KbTywSv5paAfld3A/RN/iolmMzOwZxLTA==} - - '@tiptap/core@2.27.2': - resolution: {integrity: sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==} - peerDependencies: - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-blockquote@2.27.2': - resolution: {integrity: sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-bold@2.27.2': - resolution: {integrity: sha512-bR7J5IwjCGQ0s3CIxyMvOCnMFMzIvsc5OVZKscTN5UkXzFsaY6muUAIqtKxayBUucjtUskm5qZowJITCeCb1/A==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-bullet-list@2.27.2': - resolution: {integrity: sha512-gmFuKi97u5f8uFc/GQs+zmezjiulZmFiDYTh3trVoLRoc2SAHOjGEB7qxdx7dsqmMN7gwiAWAEVurLKIi1lnnw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-code-block@2.27.2': - resolution: {integrity: sha512-KgvdQHS4jXr79aU3wZOGBIZYYl9vCB7uDEuRFV4so2rYrfmiYMw3T8bTnlNEEGe4RUeAms1i4fdwwvQp9nR1Dw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-code@2.27.2': - resolution: {integrity: sha512-7X9AgwqiIGXoZX7uvdHQsGsjILnN/JaEVtqfXZnPECzKGaWHeK/Ao4sYvIIIffsyZJA8k5DC7ny2/0sAgr2TuA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-document@2.27.2': - resolution: {integrity: sha512-CFhAYsPnyYnosDC4639sCJnBUnYH4Cat9qH5NZWHVvdgtDwu8GZgZn2eSzaKSYXWH1vJ9DSlCK+7UyC3SNXIBA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-dropcursor@2.27.2': - resolution: {integrity: sha512-oEu/OrktNoQXq1x29NnH/GOIzQZm8ieTQl3FK27nxfBPA89cNoH4mFEUmBL5/OFIENIjiYG3qWpg6voIqzswNw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-gapcursor@2.27.2': - resolution: {integrity: sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-hard-break@2.27.2': - resolution: {integrity: sha512-kSRVGKlCYK6AGR0h8xRkk0WOFGXHIIndod3GKgWU49APuIGDiXd8sziXsSlniUsWmqgDmDXcNnSzPcV7AQ8YNg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-heading@2.27.2': - resolution: {integrity: sha512-iM3yeRWuuQR/IRQ1djwNooJGfn9Jts9zF43qZIUf+U2NY8IlvdNsk2wTOdBgh6E0CamrStPxYGuln3ZS4fuglw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-history@2.27.2': - resolution: {integrity: sha512-+hSyqERoFNTWPiZx4/FCyZ/0eFqB9fuMdTB4AC/q9iwu3RNWAQtlsJg5230bf/qmyO6bZxRUc0k8p4hrV6ybAw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-horizontal-rule@2.27.2': - resolution: {integrity: sha512-WGWUSgX+jCsbtf9Y9OCUUgRZYuwjVoieW5n6mAUohJ9/6gc6sGIOrUpBShf+HHo6WD+gtQjRd+PssmX3NPWMpg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-italic@2.27.2': - resolution: {integrity: sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-link@2.27.2': - resolution: {integrity: sha512-bnP61qkr0Kj9Cgnop1hxn2zbOCBzNtmawxr92bVTOE31fJv6FhtCnQiD6tuPQVGMYhcmAj7eihtvuEMFfqEPcQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-list-item@2.27.2': - resolution: {integrity: sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-ordered-list@2.27.2': - resolution: {integrity: sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-paragraph@2.27.2': - resolution: {integrity: sha512-elYVn2wHJJ+zB9LESENWOAfI4TNT0jqEN34sMA/hCtA4im1ZG2DdLHwkHIshj/c4H0dzQhmsS/YmNC5Vbqab/A==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-strike@2.27.2': - resolution: {integrity: sha512-HHIjhafLhS2lHgfAsCwC1okqMsQzR4/mkGDm4M583Yftyjri1TNA7lzhzXWRFWiiMfJxKtdjHjUAQaHuteRTZw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text-align@2.27.2': - resolution: {integrity: sha512-0Pyks6Hu+Q/+9+5/osoSv0SP6jIerdWMYbi13aaZLsJoj3lBj5WNaE11JtAwSFN5sx0IbqhDSlp1zkvRnzgZ8g==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text-style@2.27.2': - resolution: {integrity: sha512-Omk+uxjJLyEY69KStpCw5fA9asvV+MGcAX2HOxyISDFoLaL49TMrNjhGAuz09P1L1b0KGXo4ml7Q3v/Lfy4WPA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text@2.27.2': - resolution: {integrity: sha512-Xk7nYcigljAY0GO9hAQpZ65ZCxqOqaAlTPDFcKerXmlkQZP/8ndx95OgUb1Xf63kmPOh3xypurGS2is3v0MXSA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-typography@2.27.2': - resolution: {integrity: sha512-NSyqDa8PlAZoVRfTWQuxueTZ6ftOD72EV7UKVpftf3C+Heme727mvwl1YHMnagOlqVoxBhFOrl9CnSs/q5uayQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-underline@2.27.2': - resolution: {integrity: sha512-gPOsbAcw1S07ezpAISwoO8f0RxpjcSH7VsHEFDVuXm4ODE32nhvSinvHQjv2icRLOXev+bnA7oIBu7Oy859gWQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/pm@2.27.2': - resolution: {integrity: sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==} - - '@tiptap/starter-kit@2.27.2': - resolution: {integrity: sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw==} - '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1738,9 +1347,6 @@ packages: '@tsclass/tsclass@4.4.4': resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==} - '@tsclass/tsclass@5.0.0': - resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==} - '@tsclass/tsclass@9.3.0': resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==} @@ -1780,15 +1386,9 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/from2@2.3.6': - resolution: {integrity: sha512-pO5qFhZ85+M4znizw6ihwU2nW+EarU/TuWbYzSEt6YSCis+vET8WJMUlg6/b2HLBzzT9cfKhtEoIciazgv9UXQ==} - '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1810,58 +1410,33 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - '@types/mailparser@3.4.6': resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==} - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/mime-types@2.1.4': resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/minimatch@6.0.0': - resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} - deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@22.19.11': resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} - '@types/pidusage@2.0.5': - resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==} - '@types/ping@0.4.4': resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} @@ -1916,9 +1491,6 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - '@types/whatwg-url@13.0.0': - resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} - '@types/which@3.0.4': resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==} @@ -1934,16 +1506,9 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@yr/monotone-cubic-spline@1.0.3': - resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} - '@zone-eu/mailsplit@5.4.8': resolution: {integrity: sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1976,21 +1541,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - apexcharts@5.3.6: - resolution: {integrity: sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -2077,9 +1631,6 @@ packages: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} - bintrees@1.0.2: - resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=} - bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} @@ -2106,16 +1657,9 @@ packages: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} - bson@7.2.0: - resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} - engines: {node: '>=20.19.0'} - buffer-crc32@0.2.13: resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=} - buffer-json@2.0.0: resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} @@ -2197,9 +1741,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - cloudflare@5.2.0: - resolution: {integrity: sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2242,9 +1783,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -2258,9 +1796,6 @@ packages: typescript: optional: true - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - croner@9.1.0: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} @@ -2368,9 +1903,6 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2378,12 +1910,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} @@ -2393,9 +1919,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2457,10 +1980,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2491,10 +2010,6 @@ packages: resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -2600,9 +2115,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -2615,10 +2127,6 @@ packages: resolution: {integrity: sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=} engines: {node: '>=0.4.x'} - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2627,9 +2135,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - from2@2.3.0: - resolution: {integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=} - fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -2676,10 +2181,6 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -2742,10 +2243,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - hmac-drbg@1.0.1: resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=} @@ -2786,9 +2283,6 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} - ibantools@4.5.1: - resolution: {integrity: sha512-DfKQpLlFq9yEUIEnFuCJzss3XavD7iHZTU5PyqXiAJ+rmaMp+NFP3hboumHKuK8nZjuOJg93WemTzcQ5b9jOZA==} - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2876,9 +2370,6 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} - isarray@1.0.0: - resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} - isexe@2.0.0: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} @@ -2889,9 +2380,6 @@ packages: isopen@1.3.0: resolution: {integrity: sha512-AN6Q9J0UlqHFl1fN/2xJCHCBLCBCFDjZhpGBO1gh3wzgRPsFSFBUL36I2Lbfd9qkuoj58axmE7j83iejTQsk8Q==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.2.3: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} @@ -2919,16 +2407,6 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonwebtoken@9.0.3: - resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} - engines: {node: '>=12', npm: '>=6'} - - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2954,9 +2432,6 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - lit-element@4.2.2: resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} @@ -2994,36 +2469,15 @@ packages: lodash.clonedeep@4.5.0: resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} - lodash.includes@4.3.0: - resolution: {integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=} - lodash.isarguments@3.1.0: resolution: {integrity: sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=} lodash.isarray@3.0.4: resolution: {integrity: sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=} - lodash.isboolean@3.0.3: - resolution: {integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=} - - lodash.isstring@4.0.1: - resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=} - lodash.keys@3.1.2: resolution: {integrity: sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=} - lodash.once@4.1.1: - resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=} - lodash.restparam@3.6.1: resolution: {integrity: sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=} @@ -3037,9 +2491,6 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.5: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} engines: {node: 20 || >=22} @@ -3048,9 +2499,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide@0.563.0: - resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==} - mailparser@3.9.3: resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==} @@ -3064,18 +2512,9 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@14.0.0: - resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} - engines: {node: '>= 18'} - hasBin: true - matcher@5.0.0: resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3126,9 +2565,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3286,16 +2722,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - monaco-editor@0.55.1: - resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} - mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - mongodb-connection-string-url@7.0.1: - resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} - engines: {node: '>=20.19.0'} - mongodb-memory-server-core@10.4.3: resolution: {integrity: sha512-IPjlw73IoSYopnqBibQKxmAXMbOEPf5uGAOsBcaUiNH/TOI7V19WO+K7n5KYtnQ9FqzLGLpvwCGuPOTBSg4s5Q==} engines: {node: '>=16.20.1'} @@ -3331,33 +2760,6 @@ packages: socks: optional: true - mongodb@7.1.0: - resolution: {integrity: sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.806.0 - '@mongodb-js/zstd': ^7.0.0 - gcp-metadata: ^7.0.1 - kerberos: ^7.0.0 - mongodb-client-encryption: '>=7.0.0 <7.1.0' - snappy: ^7.3.2 - socks: ^2.8.6 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3392,19 +2794,6 @@ packages: no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-forge@1.3.3: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} @@ -3451,9 +2840,6 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - os-tmpdir@1.0.2: resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} engines: {node: '>=0.10.0'} @@ -3538,10 +2924,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} @@ -3558,10 +2940,6 @@ packages: hasBin: true bundledDependencies: [] - pdfjs-dist@4.10.38: - resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==} - engines: {node: '>=20'} - peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -3579,15 +2957,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - - pidusage@4.0.1: - resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==} - engines: {node: '>=18'} - ping@0.4.4: resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==} engines: {node: '>=4.0.0'} @@ -3600,78 +2969,13 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - prom-client@15.1.3: - resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} - engines: {node: ^16 || ^18 || >=20} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - prosemirror-changeset@2.3.1: - resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} - - prosemirror-collab@1.3.1: - resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} - - prosemirror-commands@1.7.1: - resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} - - prosemirror-dropcursor@1.8.2: - resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - - prosemirror-gapcursor@1.4.0: - resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} - - prosemirror-history@1.5.0: - resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} - - prosemirror-inputrules@1.5.1: - resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} - - prosemirror-keymap@1.2.3: - resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - - prosemirror-markdown@1.13.4: - resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} - - prosemirror-menu@1.2.5: - resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} - - prosemirror-model@1.25.4: - resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} - - prosemirror-schema-basic@1.2.4: - resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} - - prosemirror-schema-list@1.5.1: - resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} - - prosemirror-state@1.4.4: - resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} - - prosemirror-tables@1.8.5: - resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} - - prosemirror-trailing-node@3.0.0: - resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} - peerDependencies: - prosemirror-model: ^1.22.1 - prosemirror-state: ^1.4.2 - prosemirror-view: ^1.33.8 - - prosemirror-transform@1.11.0: - resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} - - prosemirror-view@1.41.6: - resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==} - proto-list@1.2.4: resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=} @@ -3740,9 +3044,6 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -3812,9 +3113,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3826,9 +3124,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3952,13 +3247,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3969,10 +3257,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -4013,9 +3297,6 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tdigest@0.1.2: - resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} - text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -4044,9 +3325,6 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - tr46@0.0.3: - resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} - tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -4118,9 +3396,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -4184,19 +3459,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - webdriver-bidi-protocol@0.4.0: resolution: {integrity: sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==} - webidl-conversions@3.0.1: - resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -4209,9 +3474,6 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} - whatwg-url@5.0.0: - resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4233,10 +3495,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrappy@1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} @@ -4280,14 +3538,6 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} - xterm-addon-fit@0.8.0: - resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==} - peerDependencies: - xterm: ^5.0.0 - - xterm@5.3.0: - resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4392,52 +3642,6 @@ snapshots: - utf-8-validate - vue - '@api.global/typedserver@8.3.0(@tiptap/pm@2.27.2)': - dependencies: - '@api.global/typedrequest': 3.2.5 - '@api.global/typedrequest-interfaces': 3.0.19 - '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1) - '@cloudflare/workers-types': 4.20260210.0 - '@design.estate/dees-catalog': 3.42.0(@tiptap/pm@2.27.2) - '@design.estate/dees-comms': 1.0.30 - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 6.0.0 - '@push.rocks/smartfeed': 1.4.0 - '@push.rocks/smartfile': 13.1.2 - '@push.rocks/smartfs': 1.3.1 - '@push.rocks/smartjson': 5.2.0 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartlog-destination-devtools': 1.0.12 - '@push.rocks/smartlog-interfaces': 3.0.2 - '@push.rocks/smartmanifest': 2.0.2 - '@push.rocks/smartmatch': 2.0.0 - '@push.rocks/smartmime': 2.0.4 - '@push.rocks/smartntml': 2.0.8 - '@push.rocks/smartopen': 2.0.0 - '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartserve': 2.0.1 - '@push.rocks/smartsitemap': 2.0.4 - '@push.rocks/smartstream': 3.2.5 - '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartwatch': 6.3.0 - '@push.rocks/taskbuffer': 3.5.0 - '@push.rocks/webrequest': 4.0.1 - '@push.rocks/webstore': 2.0.20 - '@tsclass/tsclass': 9.3.0 - lit: 3.3.2 - transitivePeerDependencies: - - '@nuxt/kit' - - '@tiptap/pm' - - bufferutil - - react - - supports-color - - utf-8-validate - - vue - '@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)': dependencies: '@api.global/typedrequest': 3.2.5 @@ -4458,43 +3662,6 @@ snapshots: - utf-8-validate - vue - '@api.global/typedsocket@4.1.0(@push.rocks/smartserve@2.0.1)': - dependencies: - '@api.global/typedrequest': 3.2.5 - '@api.global/typedrequest-interfaces': 3.0.19 - '@push.rocks/isohash': 2.0.1 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartjson': 5.2.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartserve': 2.0.1 - '@push.rocks/smartstring': 4.1.0 - '@push.rocks/smarturl': 3.1.0 - - '@apiclient.xyz/cloudflare@6.4.3': - dependencies: - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartstring': 4.1.0 - '@tsclass/tsclass': 9.3.0 - cloudflare: 5.2.0 - transitivePeerDependencies: - - encoding - - '@apiclient.xyz/cloudflare@7.1.0': - dependencies: - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartstring': 4.1.0 - '@tsclass/tsclass': 9.3.0 - cloudflare: 5.2.0 - transitivePeerDependencies: - - encoding - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -5000,7 +4167,8 @@ snapshots: '@borewit/text-codec@0.2.1': {} - '@cfworker/json-schema@4.1.1': {} + '@cfworker/json-schema@4.1.1': + optional: true '@cloudflare/workers-types@4.20260210.0': {} @@ -5008,41 +4176,6 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@design.estate/dees-catalog@3.42.0(@tiptap/pm@2.27.2)': - dependencies: - '@design.estate/dees-domtools': 2.3.8 - '@design.estate/dees-element': 2.1.6 - '@design.estate/dees-wcctools': 3.8.0 - '@fortawesome/fontawesome-svg-core': 7.1.0 - '@fortawesome/free-brands-svg-icons': 7.1.0 - '@fortawesome/free-regular-svg-icons': 7.1.0 - '@fortawesome/free-solid-svg-icons': 7.1.0 - '@push.rocks/smarti18n': 1.0.4 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartstring': 4.1.0 - '@tempfix/webcontainer__api': 1.6.1 - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-link': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-text-align': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-typography': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/starter-kit': 2.27.2 - '@tsclass/tsclass': 9.3.0 - apexcharts: 5.3.6 - highlight.js: 11.11.1 - ibantools: 4.5.1 - lucide: 0.563.0 - monaco-editor: 0.55.1 - pdfjs-dist: 4.10.38 - xterm: 5.3.0 - xterm-addon-fit: 0.8.0(xterm@5.3.0) - transitivePeerDependencies: - - '@nuxt/kit' - - '@tiptap/pm' - - react - - supports-color - - vue - '@design.estate/dees-comms@1.0.30': dependencies: '@api.global/typedrequest': 3.2.5 @@ -5088,18 +4221,6 @@ snapshots: - supports-color - vue - '@design.estate/dees-wcctools@3.8.0': - dependencies: - '@design.estate/dees-domtools': 2.3.8 - '@design.estate/dees-element': 2.1.6 - '@push.rocks/smartdelay': 3.0.5 - lit: 3.3.2 - transitivePeerDependencies: - - '@nuxt/kit' - - react - - supports-color - - vue - '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -5194,24 +4315,6 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@fortawesome/fontawesome-common-types@7.1.0': {} - - '@fortawesome/fontawesome-svg-core@7.1.0': - dependencies: - '@fortawesome/fontawesome-common-types': 7.1.0 - - '@fortawesome/free-brands-svg-icons@7.1.0': - dependencies: - '@fortawesome/fontawesome-common-types': 7.1.0 - - '@fortawesome/free-regular-svg-icons@7.1.0': - dependencies: - '@fortawesome/fontawesome-common-types': 7.1.0 - - '@fortawesome/free-solid-svg-icons@7.1.0': - dependencies: - '@fortawesome/fontawesome-common-types': 7.1.0 - '@git.zone/tsbuild@4.1.2': dependencies: '@git.zone/tspublish': 1.11.0 @@ -5457,15 +4560,6 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/cliui@9.0.0': {} '@leichtgewicht/ip-codec@2.0.5': {} @@ -5507,54 +4601,6 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 - '@napi-rs/canvas-android-arm64@0.1.91': - optional: true - - '@napi-rs/canvas-darwin-arm64@0.1.91': - optional: true - - '@napi-rs/canvas-darwin-x64@0.1.91': - optional: true - - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': - optional: true - - '@napi-rs/canvas-linux-arm64-gnu@0.1.91': - optional: true - - '@napi-rs/canvas-linux-arm64-musl@0.1.91': - optional: true - - '@napi-rs/canvas-linux-riscv64-gnu@0.1.91': - optional: true - - '@napi-rs/canvas-linux-x64-gnu@0.1.91': - optional: true - - '@napi-rs/canvas-linux-x64-musl@0.1.91': - optional: true - - '@napi-rs/canvas-win32-arm64-msvc@0.1.91': - optional: true - - '@napi-rs/canvas-win32-x64-msvc@0.1.91': - optional: true - - '@napi-rs/canvas@0.1.91': - optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.91 - '@napi-rs/canvas-darwin-arm64': 0.1.91 - '@napi-rs/canvas-darwin-x64': 0.1.91 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.91 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.91 - '@napi-rs/canvas-linux-arm64-musl': 0.1.91 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.91 - '@napi-rs/canvas-linux-x64-gnu': 0.1.91 - '@napi-rs/canvas-linux-x64-musl': 0.1.91 - '@napi-rs/canvas-win32-arm64-msvc': 0.1.91 - '@napi-rs/canvas-win32-x64-msvc': 0.1.91 - optional: true - '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.8.1 @@ -5569,8 +4615,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@opentelemetry/api@1.9.0': {} - '@oxc-project/types@0.99.0': {} '@pdf-lib/standard-fonts@1.0.0': @@ -5671,9 +4715,6 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@pkgjs/parseargs@0.11.0': - optional: true - '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -5785,13 +4826,6 @@ snapshots: - supports-color - vue - '@push.rocks/projectinfo@5.0.2': - dependencies: - '@push.rocks/smartfile': 10.0.41 - '@push.rocks/smartpath': 5.1.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartstring': 4.1.0 - '@push.rocks/qenv@6.1.3': dependencies: '@api.global/typedrequest': 3.2.5 @@ -5800,40 +4834,6 @@ snapshots: '@push.rocks/smartlog': 3.1.10 '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartacme@8.0.0(socks@2.8.7)': - dependencies: - '@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1) - '@apiclient.xyz/cloudflare': 6.4.3 - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdata': 5.16.7(socks@2.8.7) - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartdns': 6.2.2 - '@push.rocks/smartfile': 11.2.7 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartnetwork': 4.4.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 2.1.0 - '@push.rocks/smartstring': 4.1.0 - '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartunique': 3.0.9 - '@tsclass/tsclass': 9.3.0 - acme-client: 5.4.0 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - '@nuxt/kit' - - bare-abort-controller - - encoding - - gcp-metadata - - kerberos - - mongodb-client-encryption - - react - - react-native-b4a - - snappy - - socks - - supports-color - - vue - '@push.rocks/smartarchive@4.2.4': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -5971,55 +4971,10 @@ snapshots: - supports-color - vue - '@push.rocks/smartdata@7.0.15(socks@2.8.7)': - dependencies: - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartmongo': 2.2.0(socks@2.8.7) - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartstring': 4.1.0 - '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartunique': 3.0.9 - '@push.rocks/taskbuffer': 3.5.0 - '@tsclass/tsclass': 9.3.0 - mongodb: 7.1.0(socks@2.8.7) - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - '@nuxt/kit' - - bare-abort-controller - - gcp-metadata - - kerberos - - mongodb-client-encryption - - react - - react-native-b4a - - snappy - - socks - - supports-color - - vue - '@push.rocks/smartdelay@3.0.5': dependencies: '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartdns@6.2.2': - dependencies: - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 5.0.13 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 2.1.0 - '@tsclass/tsclass': 5.0.0 - '@types/dns-packet': 5.6.5 - '@types/elliptic': 6.4.18 - acme-client: 5.4.0 - dns-packet: 5.6.1 - elliptic: 6.6.1 - minimatch: 10.1.2 - transitivePeerDependencies: - - supports-color - '@push.rocks/smartdns@7.6.1': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -6069,25 +5024,6 @@ snapshots: '@push.rocks/smartfile-interfaces@1.0.7': {} - '@push.rocks/smartfile@10.0.41': - dependencies: - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartfile-interfaces': 1.0.7 - '@push.rocks/smarthash': 3.2.6 - '@push.rocks/smartjson': 5.2.0 - '@push.rocks/smartmime': 1.0.6 - '@push.rocks/smartpath': 5.1.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 2.1.0 - '@push.rocks/smartstream': 2.0.8 - '@types/fs-extra': 11.0.4 - '@types/glob': 8.1.0 - '@types/js-yaml': 4.0.9 - fs-extra: 11.3.3 - glob: 10.5.0 - js-yaml: 4.1.1 - '@push.rocks/smartfile@11.2.7': dependencies: '@push.rocks/lik': 6.2.2 @@ -6140,8 +5076,6 @@ snapshots: '@types/through2': 2.0.41 through2: 4.0.2 - '@push.rocks/smarti18n@1.0.4': {} - '@push.rocks/smartinteract@2.0.16': dependencies: '@push.rocks/lik': 6.2.2 @@ -6163,15 +5097,6 @@ snapshots: fast-json-stable-stringify: 2.1.0 lodash.clonedeep: 4.5.0 - '@push.rocks/smartjwt@2.2.1': - dependencies: - '@push.rocks/smartcrypto': 2.0.4 - '@push.rocks/smartguard': 3.1.0 - '@push.rocks/smartjson': 5.2.0 - '@tsclass/tsclass': 4.4.4 - '@types/jsonwebtoken': 9.0.10 - jsonwebtoken: 9.0.3 - '@push.rocks/smartlog-destination-devtools@1.0.12': dependencies: '@push.rocks/smartlog-interfaces': 3.0.2 @@ -6231,20 +5156,6 @@ snapshots: dependencies: matcher: 5.0.0 - '@push.rocks/smartmetrics@2.0.10': - dependencies: - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.10 - '@types/pidusage': 2.0.5 - pidtree: 0.6.0 - pidusage: 4.0.1 - prom-client: 15.1.3 - - '@push.rocks/smartmime@1.0.6': - dependencies: - '@types/mime-types': 2.1.4 - mime-types: 2.1.35 - '@push.rocks/smartmime@2.0.4': dependencies: '@types/mime-types': 2.1.4 @@ -6369,45 +5280,6 @@ snapshots: '@push.rocks/smartpromise@4.2.3': {} - '@push.rocks/smartproxy@23.1.0(socks@2.8.7)': - dependencies: - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartacme': 8.0.0(socks@2.8.7) - '@push.rocks/smartcrypto': 2.0.4 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartfile': 13.1.2 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartnetwork': 4.4.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartrust': 1.1.1 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartstring': 4.1.0 - '@push.rocks/taskbuffer': 4.2.0 - '@tsclass/tsclass': 9.3.0 - '@types/minimatch': 6.0.0 - '@types/ws': 8.18.1 - minimatch: 10.1.2 - pretty-ms: 9.3.0 - ws: 8.19.0 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - '@nuxt/kit' - - bare-abort-controller - - bufferutil - - encoding - - gcp-metadata - - kerberos - - mongodb-client-encryption - - react - - react-native-b4a - - snappy - - socks - - supports-color - - utf-8-validate - - vue - '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -6454,9 +5326,7 @@ snapshots: '@push.rocks/smartrx': 3.0.10 path-to-regexp: 8.3.0 - '@push.rocks/smartrule@2.0.1': {} - - '@push.rocks/smartrust@1.1.1': + '@push.rocks/smartrust@1.2.0': dependencies: '@push.rocks/smartpath': 6.0.0 @@ -6487,6 +5357,7 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@push.rocks/smartshell@3.3.0': dependencies: @@ -6549,15 +5420,6 @@ snapshots: '@push.rocks/smartrx': 3.0.10 '@push.rocks/webstore': 2.0.20 - '@push.rocks/smartstream@2.0.8': - dependencies: - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - '@types/from2': 2.3.6 - '@types/through2': 2.0.41 - from2: 2.3.0 - through2: 4.0.2 - '@push.rocks/smartstream@3.2.5': dependencies: '@push.rocks/lik': 6.2.2 @@ -6593,15 +5455,6 @@ snapshots: '@types/semver': 7.7.1 semver: 7.7.4 - '@push.rocks/smartwatch@6.3.0': - dependencies: - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartenv': 6.0.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - chokidar: 5.0.0 - picomatch: 4.0.3 - '@push.rocks/smartxml@2.0.0': dependencies: fast-xml-parser: 5.3.5 @@ -6631,22 +5484,6 @@ snapshots: - supports-color - vue - '@push.rocks/taskbuffer@4.2.0': - dependencies: - '@design.estate/dees-element': 2.1.6 - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartunique': 3.0.9 - transitivePeerDependencies: - - '@nuxt/kit' - - react - - supports-color - - vue - '@push.rocks/webrequest@3.0.37': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -6726,8 +5563,6 @@ snapshots: strip-indent: 4.1.1 url: 0.11.4 - '@remirror/core-constants@3.0.0': {} - '@rolldown/binding-android-arm64@1.0.0-beta.52': optional: true @@ -6834,12 +5669,6 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@serve.zone/interfaces@5.3.0': - dependencies: - '@api.global/typedrequest-interfaces': 3.0.19 - '@push.rocks/smartlog-interfaces': 3.0.2 - '@tsclass/tsclass': 9.3.0 - '@sindresorhus/is@5.6.0': {} '@smithy/abort-controller@4.2.8': @@ -7182,25 +6011,6 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.5)': - dependencies: - '@svgdotjs/svg.js': 3.2.5 - - '@svgdotjs/svg.filter.js@3.0.9': - dependencies: - '@svgdotjs/svg.js': 3.2.5 - - '@svgdotjs/svg.js@3.2.5': {} - - '@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.5)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5))': - dependencies: - '@svgdotjs/svg.js': 3.2.5 - '@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.5) - - '@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5)': - dependencies: - '@svgdotjs/svg.js': 3.2.5 - '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -7209,156 +6019,6 @@ snapshots: '@tempfix/lenis@1.3.20': {} - '@tempfix/webcontainer__api@1.6.1': {} - - '@tiptap/core@2.27.2(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-blockquote@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-bold@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-bullet-list@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-code-block@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-code@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-document@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-dropcursor@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-gapcursor@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-hard-break@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-heading@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-history@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-horizontal-rule@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - - '@tiptap/extension-italic@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-link@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - linkifyjs: 4.3.2 - - '@tiptap/extension-list-item@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-ordered-list@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-paragraph@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-strike@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-text-align@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-text-style@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-text@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-typography@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/extension-underline@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - - '@tiptap/pm@2.27.2': - dependencies: - prosemirror-changeset: 2.3.1 - prosemirror-collab: 1.3.1 - prosemirror-commands: 1.7.1 - prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.0 - prosemirror-history: 1.5.0 - prosemirror-inputrules: 1.5.1 - prosemirror-keymap: 1.2.3 - prosemirror-markdown: 1.13.4 - prosemirror-menu: 1.2.5 - prosemirror-model: 1.25.4 - prosemirror-schema-basic: 1.2.4 - prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.5 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.6 - - '@tiptap/starter-kit@2.27.2': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-blockquote': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-bold': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-bullet-list': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-code-block': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-document': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-dropcursor': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-gapcursor': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-hard-break': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-heading': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-history': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-horizontal-rule': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) - '@tiptap/extension-italic': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-list-item': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-ordered-list': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-paragraph': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-strike': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/extension-text-style': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/pm': 2.27.2 - '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -7374,10 +6034,6 @@ snapshots: dependencies: type-fest: 4.41.0 - '@tsclass/tsclass@5.0.0': - dependencies: - type-fest: 4.41.0 - '@tsclass/tsclass@9.3.0': dependencies: type-fest: 4.41.0 @@ -7436,20 +6092,11 @@ snapshots: '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 - '@types/from2@2.3.6': - dependencies: - '@types/node': 25.2.3 - '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 '@types/node': 25.2.3 - '@types/glob@8.1.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 25.2.3 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -7472,56 +6119,29 @@ snapshots: dependencies: '@types/node': 25.2.3 - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 25.2.3 - - '@types/linkify-it@5.0.0': {} - '@types/mailparser@3.4.6': dependencies: '@types/node': 25.2.3 iconv-lite: 0.6.3 - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 - '@types/mdurl@2.0.0': {} - '@types/mime-types@2.1.4': {} '@types/minimatch@5.1.2': {} - '@types/minimatch@6.0.0': - dependencies: - minimatch: 10.1.2 - '@types/ms@2.1.0': {} '@types/mute-stream@0.0.4': dependencies: '@types/node': 25.2.3 - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 25.2.3 - form-data: 4.0.5 - '@types/node-forge@1.3.14': dependencies: '@types/node': 25.2.3 - '@types/node@18.19.130': - dependencies: - undici-types: 5.26.5 - '@types/node@22.19.11': dependencies: undici-types: 6.21.0 @@ -7530,8 +6150,6 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/pidusage@2.0.5': {} - '@types/ping@0.4.4': {} '@types/qs@6.14.0': {} @@ -7581,10 +6199,6 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 - '@types/whatwg-url@13.0.0': - dependencies: - '@types/webidl-conversions': 7.0.3 - '@types/which@3.0.4': {} '@types/wrap-ansi@3.0.0': {} @@ -7600,18 +6214,12 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@yr/monotone-cubic-spline@1.0.3': {} - '@zone-eu/mailsplit@5.4.8': dependencies: libbase64: 1.3.0 libmime: 5.3.7 libqp: 2.1.1 - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -7646,23 +6254,10 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - - apexcharts@5.3.6: - dependencies: - '@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.5) - '@svgdotjs/svg.filter.js': 3.0.9 - '@svgdotjs/svg.js': 3.2.5 - '@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.5)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5)) - '@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.5) - '@yr/monotone-cubic-spline': 1.0.3 - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -7742,8 +6337,6 @@ snapshots: basic-ftp@5.1.0: {} - bintrees@1.0.2: {} - bn.js@4.12.2: {} body-parser@2.2.2: @@ -7782,12 +6375,8 @@ snapshots: bson@6.10.4: {} - bson@7.2.0: {} - buffer-crc32@0.2.13: {} - buffer-equal-constant-time@1.0.1: {} - buffer-json@2.0.0: {} buffer@6.0.3: @@ -7869,18 +6458,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - cloudflare@5.2.0: - dependencies: - '@types/node': 18.19.130 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -7912,8 +6489,6 @@ snapshots: cookie@0.7.2: {} - core-util-is@1.0.3: {} - cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -7928,8 +6503,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - crelt@1.0.6: {} - croner@9.1.0: {} cross-spawn@7.0.6: @@ -8018,10 +6591,6 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.7: - optionalDependencies: - '@types/trusted-types': 2.0.7 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -8034,12 +6603,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - ee-first@1.1.1: {} elliptic@6.6.1: @@ -8054,8 +6617,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} encoding-japanese@2.2.0: {} @@ -8150,8 +6711,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} escodegen@2.1.0: @@ -8172,8 +6731,6 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} events-universal@1.0.1: @@ -8325,8 +6882,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@1.7.2: {} - form-data-encoder@2.1.4: {} form-data@4.0.5: @@ -8339,20 +6894,10 @@ snapshots: format@0.2.2: {} - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - forwarded@0.2.0: {} fresh@2.0.0: {} - from2@2.3.0: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -8409,15 +6954,6 @@ snapshots: transitivePeerDependencies: - supports-color - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@11.1.0: dependencies: foreground-child: 3.3.1 @@ -8516,8 +7052,6 @@ snapshots: he@1.2.0: {} - highlight.js@11.11.1: {} - hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 @@ -8584,8 +7118,6 @@ snapshots: dependencies: ms: 2.1.3 - ibantools@4.5.1: {} - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -8658,20 +7190,12 @@ snapshots: dependencies: is-docker: 2.2.1 - isarray@1.0.0: {} - isexe@2.0.0: {} isexe@3.1.5: {} isopen@1.3.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@4.2.3: dependencies: '@isaacs/cliui': 9.0.0 @@ -8699,30 +7223,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.3: - dependencies: - jws: 4.0.1 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.4 - - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8748,8 +7248,6 @@ snapshots: dependencies: uc.micro: 2.1.0 - linkifyjs@4.3.2: {} - lit-element@4.2.2: dependencies: '@lit-labs/ssr-dom-shim': 1.5.1 @@ -8797,30 +7295,16 @@ snapshots: lodash.clonedeep@4.5.0: {} - lodash.includes@4.3.0: {} - lodash.isarguments@3.1.0: {} lodash.isarray@3.0.4: {} - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - lodash.keys@3.1.2: dependencies: lodash._getnative: 3.9.1 lodash.isarguments: 3.1.0 lodash.isarray: 3.0.4 - lodash.once@4.1.1: {} - lodash.restparam@3.6.1: {} longest-streak@3.1.0: {} @@ -8829,14 +7313,10 @@ snapshots: lowercase-keys@3.0.0: {} - lru-cache@10.4.3: {} - lru-cache@11.2.5: {} lru-cache@7.18.3: {} - lucide@0.563.0: {} - mailparser@3.9.3: dependencies: '@zone-eu/mailsplit': 5.4.8 @@ -8860,19 +7340,8 @@ snapshots: make-error@1.3.6: {} - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - markdown-table@3.0.4: {} - marked@14.0.0: {} - matcher@5.0.0: dependencies: escape-string-regexp: 5.0.0 @@ -9006,8 +7475,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdurl@2.0.0: {} - media-typer@1.1.0: {} memory-pager@1.5.0: {} @@ -9254,21 +7721,11 @@ snapshots: mitt@3.0.1: {} - monaco-editor@0.55.1: - dependencies: - dompurify: 3.2.7 - marked: 14.0.0 - mongodb-connection-string-url@3.0.2: dependencies: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 - mongodb-connection-string-url@7.0.1: - dependencies: - '@types/whatwg-url': 13.0.0 - whatwg-url: 14.2.0 - mongodb-memory-server-core@10.4.3(socks@2.8.7): dependencies: async-mutex: 0.5.0 @@ -9319,14 +7776,6 @@ snapshots: optionalDependencies: socks: 2.8.7 - mongodb@7.1.0(socks@2.8.7): - dependencies: - '@mongodb-js/saslprep': 1.4.5 - bson: 7.2.0 - mongodb-connection-string-url: 7.0.1 - optionalDependencies: - socks: 2.8.7 - ms@2.1.3: {} mute-stream@1.0.0: {} @@ -9351,12 +7800,6 @@ snapshots: dependencies: lower-case: 1.1.4 - node-domexception@1.0.0: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-forge@1.3.3: {} nodemailer@7.0.13: {} @@ -9391,8 +7834,6 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - orderedmap@2.1.1: {} - os-tmpdir@1.0.2: {} p-cancelable@3.0.0: {} @@ -9477,11 +7918,6 @@ snapshots: path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-scurry@2.0.1: dependencies: lru-cache: 11.2.5 @@ -9498,10 +7934,6 @@ snapshots: pdf2json@3.2.0: {} - pdfjs-dist@4.10.38: - optionalDependencies: - '@napi-rs/canvas': 0.1.91 - peberminta@0.9.0: {} peek-readable@5.4.2: {} @@ -9512,12 +7944,6 @@ snapshots: picomatch@4.0.3: {} - pidtree@0.6.0: {} - - pidusage@4.0.1: - dependencies: - safe-buffer: 5.2.1 - ping@0.4.4: {} pkg-dir@4.2.0: @@ -9528,120 +7954,10 @@ snapshots: dependencies: parse-ms: 4.0.0 - process-nextick-args@2.0.1: {} - progress@2.0.3: {} - prom-client@15.1.3: - dependencies: - '@opentelemetry/api': 1.9.0 - tdigest: 0.1.2 - property-information@7.1.0: {} - prosemirror-changeset@2.3.1: - dependencies: - prosemirror-transform: 1.11.0 - - prosemirror-collab@1.3.1: - dependencies: - prosemirror-state: 1.4.4 - - prosemirror-commands@1.7.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-dropcursor@1.8.2: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.6 - - prosemirror-gapcursor@1.4.0: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.6 - - prosemirror-history@1.5.0: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.6 - rope-sequence: 1.3.4 - - prosemirror-inputrules@1.5.1: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-keymap@1.2.3: - dependencies: - prosemirror-state: 1.4.4 - w3c-keyname: 2.2.8 - - prosemirror-markdown@1.13.4: - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - prosemirror-model: 1.25.4 - - prosemirror-menu@1.2.5: - dependencies: - crelt: 1.0.6 - prosemirror-commands: 1.7.1 - prosemirror-history: 1.5.0 - prosemirror-state: 1.4.4 - - prosemirror-model@1.25.4: - dependencies: - orderedmap: 2.1.1 - - prosemirror-schema-basic@1.2.4: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-schema-list@1.5.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-state@1.4.4: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.6 - - prosemirror-tables@1.8.5: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.6 - - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6): - dependencies: - '@remirror/core-constants': 3.0.0 - escape-string-regexp: 4.0.0 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.6 - - prosemirror-transform@1.11.0: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-view@1.41.6: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - proto-list@1.2.4: {} proxy-addr@2.0.7: @@ -9743,16 +8059,6 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -9854,8 +8160,6 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.52 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.52 - rope-sequence@1.3.4: {} - router@2.2.0: dependencies: debug: 4.4.3 @@ -9872,8 +8176,6 @@ snapshots: dependencies: tslib: 2.8.1 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -10054,16 +8356,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -10077,10 +8369,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - strip-indent@4.1.1: {} strip-json-comments@2.0.1: {} @@ -10125,10 +8413,6 @@ snapshots: - bare-abort-controller - react-native-b4a - tdigest@0.1.2: - dependencies: - bintrees: 1.0.2 - text-decoder@1.2.3: dependencies: b4a: 1.7.3 @@ -10168,8 +8452,6 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tr46@0.0.3: {} - tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -10223,8 +8505,6 @@ snapshots: uint8array-extras@1.5.0: {} - undici-types@5.26.5: {} - undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -10293,14 +8573,8 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - w3c-keyname@2.2.8: {} - - web-streams-polyfill@4.0.0-beta.3: {} - webdriver-bidi-protocol@0.4.0: {} - webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} whatwg-mimetype@3.0.0: {} @@ -10310,11 +8584,6 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -10337,12 +8606,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - wrappy@1.0.2: {} ws@8.17.1: {} @@ -10353,12 +8616,6 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} - xterm-addon-fit@0.8.0(xterm@5.3.0): - dependencies: - xterm: 5.3.0 - - xterm@5.3.0: {} - y18n@5.0.8: {} yaml@2.8.2: {} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5ce0e55..7633882 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -274,15 +274,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -356,16 +347,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -913,16 +894,6 @@ version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1004,6 +975,7 @@ dependencies = [ name = "mailer-bin" version = "0.1.0" dependencies = [ + "base64", "clap", "dashmap", "hickory-resolver 0.25.2", @@ -1014,6 +986,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "uuid", ] [[package]] @@ -1021,48 +994,28 @@ name = "mailer-core" version = "0.1.0" dependencies = [ "base64", - "bytes", "mailparse", "regex", "serde", "serde_json", "thiserror", - "tracing", "uuid", ] -[[package]] -name = "mailer-napi" -version = "0.1.0" -dependencies = [ - "mailer-core", - "mailer-security", - "mailer-smtp", - "napi", - "napi-build", - "napi-derive", - "serde", - "serde_json", - "tokio", -] - [[package]] name = "mailer-security" version = "0.1.0" dependencies = [ "hickory-resolver 0.25.2", - "ipnet", "mail-auth", "mailer-core", "psl", "regex", - "ring", "rustls-pki-types", "serde", "serde_json", "thiserror", "tokio", - "tracing", ] [[package]] @@ -1070,7 +1023,6 @@ name = "mailer-smtp" version = "0.1.0" dependencies = [ "base64", - "bytes", "dashmap", "hickory-resolver 0.25.2", "mailer-core", @@ -1087,6 +1039,7 @@ dependencies = [ "tokio-rustls", "tracing", "uuid", + "webpki-roots 0.26.11", ] [[package]] @@ -1144,66 +1097,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "napi" -version = "2.16.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" -dependencies = [ - "bitflags", - "ctor", - "napi-derive", - "napi-sys", - "once_cell", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "napi-build" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" - -[[package]] -name = "napi-derive" -version = "2.16.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" -dependencies = [ - "cfg-if", - "convert_case", - "napi-derive-backend", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "napi-derive-backend" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" -dependencies = [ - "convert_case", - "once_cell", - "proc-macro2", - "quote", - "regex", - "semver", - "syn", -] - -[[package]] -name = "napi-sys" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" -dependencies = [ - "libloading", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -1543,12 +1436,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -1859,12 +1746,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "untrusted" version = "0.9.0" @@ -1972,6 +1853,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "widestring" version = "1.2.1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 57029b6..d07b146 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/mailer-core", "crates/mailer-smtp", "crates/mailer-security", - "crates/mailer-napi", "crates/mailer-bin", ] @@ -19,19 +18,14 @@ tokio-rustls = "0.26" hickory-resolver = "0.25" mail-auth = "0.7" mailparse = "0.16" -napi = { version = "2", features = ["napi9", "async", "serde-json"] } -napi-derive = "2" -ring = "0.17" dashmap = "6" thiserror = "2" tracing = "0.1" serde = { version = "1", features = ["derive"] } serde_json = "1" -bytes = "1" regex = "1" base64 = "0.22" uuid = { version = "1", features = ["v4"] } -ipnet = "2" rustls-pki-types = "1" psl = "2" clap = { version = "4", features = ["derive"] } diff --git a/rust/crates/mailer-bin/Cargo.toml b/rust/crates/mailer-bin/Cargo.toml index d212549..6b7ea8d 100644 --- a/rust/crates/mailer-bin/Cargo.toml +++ b/rust/crates/mailer-bin/Cargo.toml @@ -19,3 +19,5 @@ serde_json.workspace = true clap.workspace = true hickory-resolver.workspace = true dashmap.workspace = true +base64.workspace = true +uuid.workspace = true diff --git a/rust/crates/mailer-bin/src/main.rs b/rust/crates/mailer-bin/src/main.rs index 18cb8d7..5fb4132 100644 --- a/rust/crates/mailer-bin/src/main.rs +++ b/rust/crates/mailer-bin/src/main.rs @@ -327,6 +327,7 @@ struct ManagementState { callbacks: Arc, smtp_handle: Option, smtp_event_rx: Option>, + smtp_client_manager: Arc, } /// Run in management/IPC mode for smartrust bridge. @@ -349,10 +350,12 @@ fn run_management_mode() { let rt = tokio::runtime::Runtime::new().unwrap(); let callbacks = Arc::new(PendingCallbacks::new()); + let smtp_client_manager = Arc::new(mailer_smtp::client::SmtpClientManager::new()); let mut state = ManagementState { callbacks: callbacks.clone(), smtp_handle: None, smtp_event_rx: None, + smtp_client_manager: smtp_client_manager.clone(), }; // We need to read stdin in a separate thread (blocking I/O) @@ -833,6 +836,28 @@ async fn handle_ipc_request(req: &IpcRequest, state: &mut ManagementState) -> Ip } } + // --- SMTP Client commands --- + + "sendEmail" => { + handle_send_email(req, state).await + } + + "sendRawEmail" => { + handle_send_raw_email(req, state).await + } + + "verifySmtpConnection" => { + handle_verify_smtp_connection(req, state).await + } + + "closeSmtpPool" => { + handle_close_smtp_pool(req, state).await + } + + "getSmtpPoolStatus" => { + handle_get_smtp_pool_status(req, state) + } + _ => IpcResponse { id: req.id.clone(), success: false, @@ -1052,3 +1077,297 @@ fn parse_smtp_config( Ok(config) } + +// --------------------------------------------------------------------------- +// SMTP Client IPC handlers +// --------------------------------------------------------------------------- + +/// Structured email to build a MIME message from. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct OutboundEmail { + from: String, + to: Vec, + #[serde(default)] + cc: Vec, + #[serde(default)] + bcc: Vec, + #[serde(default)] + subject: String, + #[serde(default)] + text: String, + #[serde(default)] + html: Option, + #[serde(default)] + headers: std::collections::HashMap, +} + +impl OutboundEmail { + /// Convert to `mailer_core::Email` for proper RFC 5322 MIME building. + fn to_core_email(&self) -> mailer_core::Email { + let mut email = mailer_core::Email::new(&self.from, &self.subject, &self.text); + for addr in &self.to { + email.add_to(addr); + } + for addr in &self.cc { + email.add_cc(addr); + } + for addr in &self.bcc { + email.add_bcc(addr); + } + if let Some(html) = &self.html { + email.set_html(html); + } + for (key, value) in &self.headers { + email.add_header(key, value); + } + email + } + + /// Build an RFC 5322 compliant message using `mailer_core::build_rfc822`. + fn to_rfc822(&self) -> Vec { + let email = self.to_core_email(); + match mailer_core::build_rfc822(&email) { + Ok(msg) => msg.into_bytes(), + Err(e) => { + eprintln!("Failed to build RFC 822 message: {e}"); + // Fallback: minimal message + format!( + "From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n{}", + self.from, + self.to.join(", "), + self.subject, + self.text + ) + .into_bytes() + } + } + } + + /// Collect all recipients (to + cc + bcc). + fn all_recipients(&self) -> Vec { + let mut all = self.to.clone(); + all.extend(self.cc.clone()); + all.extend(self.bcc.clone()); + all + } +} + +/// Handle sendEmail IPC command — build MIME, optional DKIM sign, send via pool. +async fn handle_send_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + // Parse client config from params + let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { + Ok(c) => c, + Err(e) => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Invalid config: {}", e)), + }; + } + }; + + // Parse the email + let email: OutboundEmail = match req.params.get("email").and_then(|v| serde_json::from_value(v.clone()).ok()) { + Some(e) => e, + None => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some("Missing or invalid 'email' field".into()), + }; + } + }; + + // Build raw message + let mut raw_message = email.to_rfc822(); + + // Optional DKIM signing + if let Some(dkim_val) = req.params.get("dkim") { + if let Ok(dkim_config) = serde_json::from_value::(dkim_val.clone()) { + match mailer_security::sign_dkim( + &raw_message, + &dkim_config.domain, + &dkim_config.selector, + &dkim_config.private_key, + ) { + Ok(header) => { + // Prepend DKIM header to the message + let mut signed = header.into_bytes(); + signed.extend_from_slice(&raw_message); + raw_message = signed; + } + Err(e) => { + // Log but don't fail — send unsigned + eprintln!("DKIM signing failed: {}", e); + } + } + } + } + + let all_recipients = email.all_recipients(); + let sender = &email.from; + + match state + .smtp_client_manager + .send_message(&config, sender, &all_recipients, &raw_message) + .await + { + Ok(result) => IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }, + Err(e) => IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(serde_json::to_string(&serde_json::json!({ + "message": e.to_string(), + "errorType": e.error_type(), + "retryable": e.is_retryable(), + "smtpCode": e.smtp_code(), + })) + .unwrap()), + }, + } +} + +/// Handle sendRawEmail IPC command — send a pre-formatted message. +async fn handle_send_raw_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + // Parse client config from params + let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { + Ok(c) => c, + Err(e) => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Invalid config: {}", e)), + }; + } + }; + + let envelope_from = req + .params + .get("envelopeFrom") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let envelope_to: Vec = req + .params + .get("envelopeTo") + .and_then(|v| v.as_array()) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + let raw_b64 = req + .params + .get("rawMessageBase64") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Decode base64 message + use base64::Engine; + let raw_message = match base64::engine::general_purpose::STANDARD.decode(raw_b64) { + Ok(data) => data, + Err(e) => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Invalid base64 message: {}", e)), + }; + } + }; + + match state + .smtp_client_manager + .send_message(&config, envelope_from, &envelope_to, &raw_message) + .await + { + Ok(result) => IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }, + Err(e) => IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(serde_json::to_string(&serde_json::json!({ + "message": e.to_string(), + "errorType": e.error_type(), + "retryable": e.is_retryable(), + "smtpCode": e.smtp_code(), + })) + .unwrap()), + }, + } +} + +/// Handle verifySmtpConnection IPC command. +async fn handle_verify_smtp_connection( + req: &IpcRequest, + state: &ManagementState, +) -> IpcResponse { + let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { + Ok(c) => c, + Err(e) => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Invalid config: {}", e)), + }; + } + }; + + match state.smtp_client_manager.verify_connection(&config).await { + Ok(result) => IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }, + Err(e) => IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(e.to_string()), + }, + } +} + +/// Handle closeSmtpPool IPC command. +async fn handle_close_smtp_pool(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + if let Some(pool_key) = req.params.get("poolKey").and_then(|v| v.as_str()) { + state.smtp_client_manager.close_pool(pool_key).await; + } else { + state.smtp_client_manager.close_all_pools().await; + } + + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"closed": true})), + error: None, + } +} + +/// Handle getSmtpPoolStatus IPC command. +fn handle_get_smtp_pool_status(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + let pools = state.smtp_client_manager.pool_status(); + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"pools": pools})), + error: None, + } +} diff --git a/rust/crates/mailer-core/Cargo.toml b/rust/crates/mailer-core/Cargo.toml index c0d3751..0eb784b 100644 --- a/rust/crates/mailer-core/Cargo.toml +++ b/rust/crates/mailer-core/Cargo.toml @@ -8,8 +8,6 @@ license.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true -tracing.workspace = true -bytes.workspace = true mailparse.workspace = true regex.workspace = true base64.workspace = true diff --git a/rust/crates/mailer-napi/Cargo.toml b/rust/crates/mailer-napi/Cargo.toml deleted file mode 100644 index 54d80c2..0000000 --- a/rust/crates/mailer-napi/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "mailer-napi" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -crate-type = ["cdylib"] - -[dependencies] -mailer-core = { path = "../mailer-core" } -mailer-smtp = { path = "../mailer-smtp" } -mailer-security = { path = "../mailer-security" } -napi.workspace = true -napi-derive.workspace = true -tokio.workspace = true -serde.workspace = true -serde_json.workspace = true - -[build-dependencies] -napi-build = "2" diff --git a/rust/crates/mailer-napi/build.rs b/rust/crates/mailer-napi/build.rs deleted file mode 100644 index 9fc2367..0000000 --- a/rust/crates/mailer-napi/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate napi_build; - -fn main() { - napi_build::setup(); -} diff --git a/rust/crates/mailer-napi/src/lib.rs b/rust/crates/mailer-napi/src/lib.rs deleted file mode 100644 index 2cf01a1..0000000 --- a/rust/crates/mailer-napi/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! mailer-napi: N-API bindings exposing Rust mailer to Node.js/TypeScript. - -use napi_derive::napi; - -/// Returns the version of the native mailer module. -#[napi] -pub fn get_version() -> String { - format!( - "mailer-napi v{} (core: {}, smtp: {}, security: {})", - env!("CARGO_PKG_VERSION"), - mailer_core::version(), - mailer_smtp::version(), - mailer_security::version(), - ) -} diff --git a/rust/crates/mailer-security/Cargo.toml b/rust/crates/mailer-security/Cargo.toml index 263a815..e42c7a8 100644 --- a/rust/crates/mailer-security/Cargo.toml +++ b/rust/crates/mailer-security/Cargo.toml @@ -7,14 +7,11 @@ license.workspace = true [dependencies] mailer-core = { path = "../mailer-core" } mail-auth.workspace = true -ring.workspace = true thiserror.workspace = true -tracing.workspace = true serde.workspace = true serde_json.workspace = true tokio.workspace = true hickory-resolver.workspace = true -ipnet.workspace = true rustls-pki-types.workspace = true psl.workspace = true regex.workspace = true diff --git a/rust/crates/mailer-security/src/content_scanner.rs b/rust/crates/mailer-security/src/content_scanner.rs index f20c771..0faa8d1 100644 --- a/rust/crates/mailer-security/src/content_scanner.rs +++ b/rust/crates/mailer-security/src/content_scanner.rs @@ -111,16 +111,18 @@ static MACRO_DOCUMENT_EXTENSIONS: LazyLock> = LazyLock::new(|| // HTML helpers // --------------------------------------------------------------------------- +/// Regexes for HTML text extraction (compiled once via LazyLock). +static HTML_STYLE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?is)]*>.*?").unwrap()); +static HTML_SCRIPT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?is)]*>.*?").unwrap()); +static HTML_TAG_RE: LazyLock = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap()); + /// Strip HTML tags and decode common entities to produce plain text. fn extract_text_from_html(html: &str) -> String { - // Remove style and script blocks first - let no_style = Regex::new(r"(?is)]*>.*?").unwrap(); - let no_script = Regex::new(r"(?is)]*>.*?").unwrap(); - let no_tags = Regex::new(r"<[^>]+>").unwrap(); - - let text = no_style.replace_all(html, " "); - let text = no_script.replace_all(&text, " "); - let text = no_tags.replace_all(&text, " "); + let text = HTML_STYLE_RE.replace_all(html, " "); + let text = HTML_SCRIPT_RE.replace_all(&text, " "); + let text = HTML_TAG_RE.replace_all(&text, " "); text.replace(" ", " ") .replace("<", "<") diff --git a/rust/crates/mailer-smtp/Cargo.toml b/rust/crates/mailer-smtp/Cargo.toml index 2a7ca6b..95563fc 100644 --- a/rust/crates/mailer-smtp/Cargo.toml +++ b/rust/crates/mailer-smtp/Cargo.toml @@ -13,13 +13,13 @@ hickory-resolver.workspace = true dashmap.workspace = true thiserror.workspace = true tracing.workspace = true -bytes.workspace = true serde.workspace = true -serde_json = "1" -regex = "1" -uuid = { version = "1", features = ["v4"] } +serde_json.workspace = true +regex.workspace = true +uuid.workspace = true base64.workspace = true rustls-pki-types.workspace = true rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } rustls-pemfile = "2" mailparse.workspace = true +webpki-roots = "0.26" diff --git a/rust/crates/mailer-smtp/src/client/config.rs b/rust/crates/mailer-smtp/src/client/config.rs new file mode 100644 index 0000000..25c0f90 --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/config.rs @@ -0,0 +1,157 @@ +//! SMTP client configuration types. + +use serde::Deserialize; + +/// Configuration for connecting to an SMTP server. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SmtpClientConfig { + /// Target SMTP server hostname. + pub host: String, + + /// Target port (25 = SMTP, 465 = implicit TLS, 587 = submission). + pub port: u16, + + /// Use implicit TLS (port 465). If false, STARTTLS is attempted. + #[serde(default)] + pub secure: bool, + + /// Domain to use in EHLO command. Defaults to "localhost". + #[serde(default = "default_domain")] + pub domain: String, + + /// Authentication credentials (optional). + pub auth: Option, + + /// Connection timeout in seconds. Default: 30. + #[serde(default = "default_connection_timeout")] + pub connection_timeout_secs: u64, + + /// Socket read/write timeout in seconds. Default: 120. + #[serde(default = "default_socket_timeout")] + pub socket_timeout_secs: u64, + + /// Pool key override. Defaults to "host:port". + pub pool_key: Option, + + /// Maximum connections per pool. Default: 10. + #[serde(default = "default_max_pool_connections")] + pub max_pool_connections: usize, +} + +/// Authentication configuration. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SmtpAuthConfig { + /// Username. + pub user: String, + /// Password. + pub pass: String, + /// Method: "PLAIN" or "LOGIN". Default: "PLAIN". + #[serde(default = "default_auth_method")] + pub method: String, +} + +/// DKIM signing configuration (applied before sending). +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DkimSignConfig { + /// Signing domain (e.g. "example.com"). + pub domain: String, + /// DKIM selector (e.g. "default" or "mta"). + pub selector: String, + /// PEM-encoded RSA private key. + pub private_key: String, +} + +impl SmtpClientConfig { + /// Get the effective pool key for this config. + pub fn effective_pool_key(&self) -> String { + self.pool_key + .clone() + .unwrap_or_else(|| format!("{}:{}", self.host, self.port)) + } +} + +fn default_domain() -> String { + "localhost".to_string() +} + +fn default_connection_timeout() -> u64 { + 30 +} + +fn default_socket_timeout() -> u64 { + 120 +} + +fn default_max_pool_connections() -> usize { + 10 +} + +fn default_auth_method() -> String { + "PLAIN".to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_minimal_config() { + let json = r#"{"host":"mail.example.com","port":25}"#; + let config: SmtpClientConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.host, "mail.example.com"); + assert_eq!(config.port, 25); + assert!(!config.secure); + assert_eq!(config.domain, "localhost"); + assert!(config.auth.is_none()); + assert_eq!(config.connection_timeout_secs, 30); + assert_eq!(config.socket_timeout_secs, 120); + assert_eq!(config.max_pool_connections, 10); + } + + #[test] + fn test_deserialize_full_config() { + let json = r#"{ + "host": "smtp.gmail.com", + "port": 465, + "secure": true, + "domain": "myserver.com", + "auth": { "user": "u", "pass": "p", "method": "LOGIN" }, + "connectionTimeoutSecs": 60, + "socketTimeoutSecs": 300, + "poolKey": "gmail", + "maxPoolConnections": 5 + }"#; + let config: SmtpClientConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.host, "smtp.gmail.com"); + assert_eq!(config.port, 465); + assert!(config.secure); + assert_eq!(config.domain, "myserver.com"); + assert_eq!(config.connection_timeout_secs, 60); + assert_eq!(config.socket_timeout_secs, 300); + assert_eq!(config.effective_pool_key(), "gmail"); + assert_eq!(config.max_pool_connections, 5); + let auth = config.auth.unwrap(); + assert_eq!(auth.user, "u"); + assert_eq!(auth.pass, "p"); + assert_eq!(auth.method, "LOGIN"); + } + + #[test] + fn test_effective_pool_key_default() { + let json = r#"{"host":"mx.example.com","port":587}"#; + let config: SmtpClientConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.effective_pool_key(), "mx.example.com:587"); + } + + #[test] + fn test_dkim_config_deserialize() { + let json = r#"{"domain":"example.com","selector":"mta","privateKey":"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----"}"#; + let dkim: DkimSignConfig = serde_json::from_str(json).unwrap(); + assert_eq!(dkim.domain, "example.com"); + assert_eq!(dkim.selector, "mta"); + assert!(dkim.private_key.contains("RSA PRIVATE KEY")); + } +} diff --git a/rust/crates/mailer-smtp/src/client/connection.rs b/rust/crates/mailer-smtp/src/client/connection.rs new file mode 100644 index 0000000..07cff87 --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/connection.rs @@ -0,0 +1,206 @@ +//! TCP/TLS connection management for the SMTP client. + +use super::error::SmtpClientError; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; +use tokio::time::{timeout, Duration}; +use tokio_rustls::client::TlsStream; +use tracing::debug; + +/// A client-side SMTP stream that may be plain or TLS. +pub enum ClientSmtpStream { + Plain(BufReader), + Tls(BufReader>), +} + +impl std::fmt::Debug for ClientSmtpStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClientSmtpStream::Plain(_) => write!(f, "ClientSmtpStream::Plain"), + ClientSmtpStream::Tls(_) => write!(f, "ClientSmtpStream::Tls"), + } + } +} + +impl ClientSmtpStream { + /// Read a line from the stream (CRLF-terminated). + pub async fn read_line(&mut self, buf: &mut String) -> Result { + match self { + ClientSmtpStream::Plain(reader) => reader.read_line(buf).await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("Read error: {e}"), + } + }), + ClientSmtpStream::Tls(reader) => reader.read_line(buf).await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("TLS read error: {e}"), + } + }), + } + } + + /// Write bytes to the stream. + pub async fn write_all(&mut self, data: &[u8]) -> Result<(), SmtpClientError> { + match self { + ClientSmtpStream::Plain(reader) => { + reader.get_mut().write_all(data).await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("Write error: {e}"), + } + }) + } + ClientSmtpStream::Tls(reader) => { + reader.get_mut().write_all(data).await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("TLS write error: {e}"), + } + }) + } + } + } + + /// Flush the stream. + pub async fn flush(&mut self) -> Result<(), SmtpClientError> { + match self { + ClientSmtpStream::Plain(reader) => { + reader.get_mut().flush().await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("Flush error: {e}"), + } + }) + } + ClientSmtpStream::Tls(reader) => { + reader.get_mut().flush().await.map_err(|e| { + SmtpClientError::ConnectionError { + message: format!("TLS flush error: {e}"), + } + }) + } + } + } + + /// Consume this stream and return the inner TcpStream (for STARTTLS upgrade). + /// Only works on Plain streams; returns an error on TLS streams. + pub fn into_tcp_stream(self) -> Result { + match self { + ClientSmtpStream::Plain(reader) => Ok(reader.into_inner()), + ClientSmtpStream::Tls(_) => Err(SmtpClientError::TlsError { + message: "Cannot extract TcpStream from an already-TLS stream".into(), + }), + } + } +} + +/// Connect to an SMTP server via plain TCP. +pub async fn connect_plain( + host: &str, + port: u16, + timeout_secs: u64, +) -> Result { + debug!("Connecting to {}:{} (plain)", host, port); + let addr = format!("{host}:{port}"); + let stream = timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)) + .await + .map_err(|_| SmtpClientError::TimeoutError { + message: format!("Connection to {addr} timed out after {timeout_secs}s"), + })? + .map_err(|e| SmtpClientError::ConnectionError { + message: format!("Failed to connect to {addr}: {e}"), + })?; + + Ok(ClientSmtpStream::Plain(BufReader::new(stream))) +} + +/// Connect to an SMTP server via implicit TLS (port 465). +pub async fn connect_tls( + host: &str, + port: u16, + timeout_secs: u64, +) -> Result { + debug!("Connecting to {}:{} (implicit TLS)", host, port); + let addr = format!("{host}:{port}"); + + let tcp_stream = timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)) + .await + .map_err(|_| SmtpClientError::TimeoutError { + message: format!("Connection to {addr} timed out after {timeout_secs}s"), + })? + .map_err(|e| SmtpClientError::ConnectionError { + message: format!("Failed to connect to {addr}: {e}"), + })?; + + let tls_stream = perform_tls_handshake(tcp_stream, host).await?; + Ok(ClientSmtpStream::Tls(BufReader::new(tls_stream))) +} + +/// Upgrade a plain TCP connection to TLS (STARTTLS). +pub async fn upgrade_to_tls( + stream: ClientSmtpStream, + hostname: &str, +) -> Result { + debug!("Upgrading connection to TLS (STARTTLS) for {}", hostname); + let tcp_stream = stream.into_tcp_stream()?; + let tls_stream = perform_tls_handshake(tcp_stream, hostname).await?; + Ok(ClientSmtpStream::Tls(BufReader::new(tls_stream))) +} + +/// Perform the TLS handshake on a TCP stream using webpki-roots. +async fn perform_tls_handshake( + tcp_stream: TcpStream, + hostname: &str, +) -> Result, SmtpClientError> { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let tls_config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let connector = tokio_rustls::TlsConnector::from(Arc::new(tls_config)); + let server_name = rustls_pki_types::ServerName::try_from(hostname.to_string()).map_err(|e| { + SmtpClientError::TlsError { + message: format!("Invalid server name '{hostname}': {e}"), + } + })?; + + let tls_stream = connector + .connect(server_name, tcp_stream) + .await + .map_err(|e| SmtpClientError::TlsError { + message: format!("TLS handshake with {hostname} failed: {e}"), + })?; + + Ok(tls_stream) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_connect_plain_refused() { + // Connecting to a port that's not listening should fail + let result = connect_plain("127.0.0.1", 19999, 2).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, SmtpClientError::ConnectionError { .. })); + assert!(err.is_retryable()); + } + + #[tokio::test] + async fn test_connect_tls_refused() { + let result = connect_tls("127.0.0.1", 19998, 2).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_connect_timeout() { + // 192.0.2.1 is TEST-NET, should time out + let result = connect_plain("192.0.2.1", 25, 1).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + // May be timeout or connection error depending on network + assert!(err.is_retryable()); + } +} diff --git a/rust/crates/mailer-smtp/src/client/error.rs b/rust/crates/mailer-smtp/src/client/error.rs new file mode 100644 index 0000000..c889818 --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/error.rs @@ -0,0 +1,160 @@ +//! SMTP client error types. + +use serde::Serialize; + +/// Errors that can occur during SMTP client operations. +#[derive(Debug, thiserror::Error, Serialize)] +pub enum SmtpClientError { + #[error("Connection error: {message}")] + ConnectionError { message: String }, + + #[error("Timeout: {message}")] + TimeoutError { message: String }, + + #[error("TLS error: {message}")] + TlsError { message: String }, + + #[error("Authentication failed: {message}")] + AuthenticationError { message: String }, + + #[error("Protocol error ({code}): {message}")] + ProtocolError { code: u16, message: String }, + + #[error("Pool exhausted: {message}")] + PoolExhausted { message: String }, + + #[error("Invalid configuration: {message}")] + ConfigError { message: String }, +} + +impl SmtpClientError { + /// Whether this error is retryable (temporary failure). + /// Permanent failures (5xx, auth failures) are not retryable. + pub fn is_retryable(&self) -> bool { + match self { + SmtpClientError::ConnectionError { .. } => true, + SmtpClientError::TimeoutError { .. } => true, + SmtpClientError::TlsError { .. } => false, + SmtpClientError::AuthenticationError { .. } => false, + SmtpClientError::ProtocolError { code, .. } => *code >= 400 && *code < 500, + SmtpClientError::PoolExhausted { .. } => true, + SmtpClientError::ConfigError { .. } => false, + } + } + + /// The error type as a string for IPC serialization. + pub fn error_type(&self) -> &'static str { + match self { + SmtpClientError::ConnectionError { .. } => "connection", + SmtpClientError::TimeoutError { .. } => "timeout", + SmtpClientError::TlsError { .. } => "tls", + SmtpClientError::AuthenticationError { .. } => "authentication", + SmtpClientError::ProtocolError { .. } => "protocol", + SmtpClientError::PoolExhausted { .. } => "pool_exhausted", + SmtpClientError::ConfigError { .. } => "config", + } + } + + /// The SMTP code if this is a protocol error. + pub fn smtp_code(&self) -> Option { + match self { + SmtpClientError::ProtocolError { code, .. } => Some(*code), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_retryable_errors() { + assert!(SmtpClientError::ConnectionError { + message: "refused".into() + } + .is_retryable()); + assert!(SmtpClientError::TimeoutError { + message: "timed out".into() + } + .is_retryable()); + assert!(SmtpClientError::PoolExhausted { + message: "full".into() + } + .is_retryable()); + assert!(SmtpClientError::ProtocolError { + code: 421, + message: "try later".into() + } + .is_retryable()); + assert!(SmtpClientError::ProtocolError { + code: 450, + message: "mailbox busy".into() + } + .is_retryable()); + } + + #[test] + fn test_non_retryable_errors() { + assert!(!SmtpClientError::AuthenticationError { + message: "bad creds".into() + } + .is_retryable()); + assert!(!SmtpClientError::TlsError { + message: "cert invalid".into() + } + .is_retryable()); + assert!(!SmtpClientError::ProtocolError { + code: 550, + message: "no such user".into() + } + .is_retryable()); + assert!(!SmtpClientError::ProtocolError { + code: 554, + message: "rejected".into() + } + .is_retryable()); + assert!(!SmtpClientError::ConfigError { + message: "bad config".into() + } + .is_retryable()); + } + + #[test] + fn test_error_type_strings() { + assert_eq!( + SmtpClientError::ConnectionError { + message: "x".into() + } + .error_type(), + "connection" + ); + assert_eq!( + SmtpClientError::ProtocolError { + code: 550, + message: "x".into() + } + .error_type(), + "protocol" + ); + } + + #[test] + fn test_smtp_code() { + assert_eq!( + SmtpClientError::ProtocolError { + code: 550, + message: "x".into() + } + .smtp_code(), + Some(550) + ); + assert_eq!( + SmtpClientError::ConnectionError { + message: "x".into() + } + .smtp_code(), + None + ); + } +} diff --git a/rust/crates/mailer-smtp/src/client/mod.rs b/rust/crates/mailer-smtp/src/client/mod.rs new file mode 100644 index 0000000..b4daffc --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/mod.rs @@ -0,0 +1,16 @@ +//! SMTP client module for outbound email delivery. +//! +//! Provides connection pooling, SMTP protocol, TLS, and authentication +//! for sending outbound emails through remote SMTP servers. + +pub mod config; +pub mod connection; +pub mod error; +pub mod pool; +pub mod protocol; + +// Re-export key types for convenience. +pub use config::{DkimSignConfig, SmtpAuthConfig, SmtpClientConfig}; +pub use error::SmtpClientError; +pub use pool::{SmtpClientManager, SmtpSendResult, SmtpVerifyResult}; +pub use protocol::{dot_stuff, EhloCapabilities, SmtpClientResponse}; diff --git a/rust/crates/mailer-smtp/src/client/pool.rs b/rust/crates/mailer-smtp/src/client/pool.rs new file mode 100644 index 0000000..b3d7499 --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/pool.rs @@ -0,0 +1,503 @@ +//! Connection pooling for the SMTP client. +//! +//! Manages reusable connections per destination `host:port`. + +use super::config::SmtpClientConfig; +use super::connection::{connect_plain, connect_tls, ClientSmtpStream}; +use super::error::SmtpClientError; +use super::protocol::{self, EhloCapabilities}; +use dashmap::DashMap; +use serde::Serialize; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Mutex; +use tracing::{debug, info}; + +/// Maximum age of a pooled connection (5 minutes). +const MAX_CONNECTION_AGE_SECS: u64 = 300; + +/// Maximum idle time before a connection is reaped (30 seconds). +const MAX_IDLE_SECS: u64 = 30; + +/// Maximum messages per pooled connection before it's recycled. +const MAX_MESSAGES_PER_CONNECTION: u32 = 100; + +/// A pooled SMTP connection. +pub struct PooledConnection { + pub stream: ClientSmtpStream, + pub capabilities: EhloCapabilities, + pub created_at: Instant, + pub last_used: Instant, + pub message_count: u32, + pub idle: bool, +} + +/// Check if a pooled connection is stale (too old, too many messages, or idle too long). +fn is_connection_stale(conn: &PooledConnection) -> bool { + conn.created_at.elapsed().as_secs() > MAX_CONNECTION_AGE_SECS + || conn.message_count >= MAX_MESSAGES_PER_CONNECTION + || (conn.idle && conn.last_used.elapsed().as_secs() > MAX_IDLE_SECS) +} + +/// Per-destination connection pool. +pub struct ConnectionPool { + connections: Vec, + max_connections: usize, + config: SmtpClientConfig, +} + +impl ConnectionPool { + fn new(config: SmtpClientConfig) -> Self { + let max_connections = config.max_pool_connections; + Self { + connections: Vec::new(), + max_connections, + config, + } + } + + /// Get an idle connection or create a new one. + async fn acquire(&mut self) -> Result { + // Remove stale connections first + self.cleanup_stale(); + + // Find an idle connection + if let Some(idx) = self + .connections + .iter() + .position(|c| c.idle && !is_connection_stale(c)) + { + let mut conn = self.connections.remove(idx); + conn.idle = false; + conn.last_used = Instant::now(); + debug!( + "Reusing pooled connection (age={}s, msgs={})", + conn.created_at.elapsed().as_secs(), + conn.message_count + ); + return Ok(conn); + } + + // Check if we can create a new connection + if self.connections.len() >= self.max_connections { + return Err(SmtpClientError::PoolExhausted { + message: format!( + "Pool for {} is at max capacity ({})", + self.config.effective_pool_key(), + self.max_connections + ), + }); + } + + // Create a new connection + self.create_connection().await + } + + /// Return a connection to the pool (or close it if it's expired). + fn release(&mut self, mut conn: PooledConnection) { + conn.message_count += 1; + conn.last_used = Instant::now(); + conn.idle = true; + + // Don't return if it's stale + if is_connection_stale(&conn) || self.connections.len() >= self.max_connections { + debug!("Discarding stale/excess pooled connection"); + // Drop the connection (stream will be closed) + return; + } + + self.connections.push(conn); + } + + /// Create a fresh SMTP connection and complete the handshake. + async fn create_connection(&self) -> Result { + let mut stream = if self.config.secure { + connect_tls( + &self.config.host, + self.config.port, + self.config.connection_timeout_secs, + ) + .await? + } else { + connect_plain( + &self.config.host, + self.config.port, + self.config.connection_timeout_secs, + ) + .await? + }; + + // Read greeting + protocol::read_greeting(&mut stream, self.config.socket_timeout_secs).await?; + + // Send EHLO + let mut capabilities = + protocol::send_ehlo(&mut stream, &self.config.domain, self.config.socket_timeout_secs) + .await?; + + // STARTTLS if available and not already secure + if !self.config.secure && capabilities.starttls { + protocol::send_starttls(&mut stream, self.config.socket_timeout_secs).await?; + stream = + super::connection::upgrade_to_tls(stream, &self.config.host).await?; + + // Re-EHLO after STARTTLS — use updated capabilities for auth + capabilities = protocol::send_ehlo( + &mut stream, + &self.config.domain, + self.config.socket_timeout_secs, + ) + .await?; + } + + // Authenticate if credentials provided + if let Some(auth) = &self.config.auth { + protocol::authenticate( + &mut stream, + auth, + &capabilities, + self.config.socket_timeout_secs, + ) + .await?; + } + + info!( + "New SMTP connection to {} established", + self.config.effective_pool_key() + ); + + Ok(PooledConnection { + stream, + capabilities, + created_at: Instant::now(), + last_used: Instant::now(), + message_count: 0, + idle: false, + }) + } + + fn cleanup_stale(&mut self) { + self.connections.retain(|c| !is_connection_stale(c)); + } + + /// Number of connections in the pool. + fn total(&self) -> usize { + self.connections.len() + } + + /// Number of idle connections. + fn idle_count(&self) -> usize { + self.connections.iter().filter(|c| c.idle).count() + } + + /// Close all connections. + fn close_all(&mut self) { + self.connections.clear(); + } +} + +/// Status report for a single pool. +#[derive(Debug, Clone, Serialize)] +pub struct PoolStatus { + pub total: usize, + pub active: usize, + pub idle: usize, +} + +/// Manages connection pools for multiple SMTP destinations. +pub struct SmtpClientManager { + pools: DashMap>>, +} + +impl SmtpClientManager { + pub fn new() -> Self { + Self { + pools: DashMap::new(), + } + } + + /// Get or create a pool for the given config. + fn get_pool(&self, config: &SmtpClientConfig) -> Arc> { + let key = config.effective_pool_key(); + self.pools + .entry(key) + .or_insert_with(|| Arc::new(Mutex::new(ConnectionPool::new(config.clone())))) + .clone() + } + + /// Acquire a connection from the pool, send a message, and release it. + pub async fn send_message( + &self, + config: &SmtpClientConfig, + sender: &str, + recipients: &[String], + message: &[u8], + ) -> Result { + let pool_arc = self.get_pool(config); + let mut pool = pool_arc.lock().await; + + let mut conn = pool.acquire().await?; + drop(pool); // Release the pool lock while we do network I/O + + // Reset server state if reusing a connection that has already sent messages + if conn.message_count > 0 { + protocol::send_rset(&mut conn.stream, config.socket_timeout_secs).await?; + } + + // Perform the SMTP transaction + let result = + Self::perform_send(&mut conn.stream, sender, recipients, message, config).await; + + // Re-acquire the pool lock and release the connection + let mut pool = pool_arc.lock().await; + match &result { + Ok(_) => pool.release(conn), + Err(_) => { + // Don't return failed connections to the pool + debug!("Discarding connection after send failure"); + } + } + + result + } + + /// Perform the SMTP send transaction on a connected stream. + async fn perform_send( + stream: &mut ClientSmtpStream, + sender: &str, + recipients: &[String], + message: &[u8], + config: &SmtpClientConfig, + ) -> Result { + let timeout_secs = config.socket_timeout_secs; + + // MAIL FROM + protocol::send_mail_from(stream, sender, timeout_secs).await?; + + // RCPT TO for each recipient + let mut accepted = Vec::new(); + let mut rejected = Vec::new(); + + for rcpt in recipients { + match protocol::send_rcpt_to(stream, rcpt, timeout_secs).await { + Ok(resp) => { + if resp.is_success() { + accepted.push(rcpt.clone()); + } else { + rejected.push(rcpt.clone()); + } + } + Err(_) => { + rejected.push(rcpt.clone()); + } + } + } + + // If no recipients were accepted, fail + if accepted.is_empty() { + return Err(SmtpClientError::ProtocolError { + code: 550, + message: "All recipients were rejected".into(), + }); + } + + // DATA + let data_resp = protocol::send_data(stream, message, timeout_secs).await?; + + // Extract message ID from the response if present + let message_id = data_resp + .lines + .iter() + .find_map(|line| { + // Look for a pattern like "queued as XXXX" or message-id + if line.contains("queued") || line.contains("id=") { + Some(line.clone()) + } else { + None + } + }); + + Ok(SmtpSendResult { + accepted, + rejected, + message_id, + response: data_resp.full_message(), + envelope: SmtpEnvelope { + from: sender.to_string(), + to: recipients.to_vec(), + }, + }) + } + + /// Verify connectivity to an SMTP server (connect, EHLO, QUIT). + pub async fn verify_connection( + &self, + config: &SmtpClientConfig, + ) -> Result { + let mut stream = if config.secure { + connect_tls( + &config.host, + config.port, + config.connection_timeout_secs, + ) + .await? + } else { + connect_plain( + &config.host, + config.port, + config.connection_timeout_secs, + ) + .await? + }; + + let greeting = protocol::read_greeting(&mut stream, config.socket_timeout_secs).await?; + let caps = + protocol::send_ehlo(&mut stream, &config.domain, config.socket_timeout_secs).await?; + let _ = protocol::send_quit(&mut stream, config.socket_timeout_secs).await; + + Ok(SmtpVerifyResult { + reachable: true, + greeting: Some(greeting.full_message()), + capabilities: Some(caps.extensions), + }) + } + + /// Get status of all pools. + pub fn pool_status(&self) -> std::collections::HashMap { + let mut result = std::collections::HashMap::new(); + + for entry in self.pools.iter() { + let key = entry.key().clone(); + // Try to get the lock without blocking — if locked, report as active + match entry.value().try_lock() { + Ok(pool) => { + let total = pool.total(); + let idle = pool.idle_count(); + result.insert( + key, + PoolStatus { + total, + active: total - idle, + idle, + }, + ); + } + Err(_) => { + // Pool is in use; report as busy + result.insert( + key, + PoolStatus { + total: 0, + active: 1, + idle: 0, + }, + ); + } + } + } + + result + } + + /// Close a specific pool. + pub async fn close_pool(&self, key: &str) { + if let Some(pool_ref) = self.pools.get(key) { + let mut pool = pool_ref.lock().await; + pool.close_all(); + } + self.pools.remove(key); + } + + /// Close all pools. + pub async fn close_all_pools(&self) { + let keys: Vec = self.pools.iter().map(|e| e.key().clone()).collect(); + for key in keys { + self.close_pool(&key).await; + } + } +} + +/// Result of sending an email via SMTP. +#[derive(Debug, Clone, Serialize)] +pub struct SmtpSendResult { + pub accepted: Vec, + pub rejected: Vec, + #[serde(rename = "messageId")] + pub message_id: Option, + pub response: String, + pub envelope: SmtpEnvelope, +} + +/// SMTP envelope (sender + recipients). +#[derive(Debug, Clone, Serialize)] +pub struct SmtpEnvelope { + pub from: String, + pub to: Vec, +} + +/// Result of verifying an SMTP connection. +#[derive(Debug, Clone, Serialize)] +pub struct SmtpVerifyResult { + pub reachable: bool, + pub greeting: Option, + pub capabilities: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pool_status_serialization() { + let status = PoolStatus { + total: 5, + active: 2, + idle: 3, + }; + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains("\"total\":5")); + assert!(json.contains("\"active\":2")); + assert!(json.contains("\"idle\":3")); + } + + #[test] + fn test_send_result_serialization() { + let result = SmtpSendResult { + accepted: vec!["a@b.com".into()], + rejected: vec![], + message_id: Some("abc123".into()), + response: "250 OK".into(), + envelope: SmtpEnvelope { + from: "from@test.com".into(), + to: vec!["a@b.com".into()], + }, + }; + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("\"messageId\":\"abc123\"")); + assert!(json.contains("\"accepted\":[\"a@b.com\"]")); + } + + #[test] + fn test_verify_result_serialization() { + let result = SmtpVerifyResult { + reachable: true, + greeting: Some("220 mail.example.com".into()), + capabilities: Some(vec!["SIZE 10485760".into(), "STARTTLS".into()]), + }; + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("\"reachable\":true")); + } + + #[test] + fn test_smtp_client_manager_new() { + let mgr = SmtpClientManager::new(); + assert!(mgr.pool_status().is_empty()); + } + + #[tokio::test] + async fn test_close_all_empty() { + let mgr = SmtpClientManager::new(); + mgr.close_all_pools().await; + assert!(mgr.pool_status().is_empty()); + } +} diff --git a/rust/crates/mailer-smtp/src/client/protocol.rs b/rust/crates/mailer-smtp/src/client/protocol.rs new file mode 100644 index 0000000..ecfed17 --- /dev/null +++ b/rust/crates/mailer-smtp/src/client/protocol.rs @@ -0,0 +1,520 @@ +//! SMTP client protocol engine. +//! +//! Implements the SMTP command/response flow for sending outbound email. + +use super::config::SmtpAuthConfig; +use super::connection::ClientSmtpStream; +use super::error::SmtpClientError; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use serde::{Deserialize, Serialize}; +use tokio::time::{timeout, Duration}; +use tracing::debug; + +/// Parsed SMTP response (from the remote server). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SmtpClientResponse { + pub code: u16, + pub lines: Vec, +} + +impl SmtpClientResponse { + pub fn is_success(&self) -> bool { + self.code >= 200 && self.code < 300 + } + + pub fn is_positive_intermediate(&self) -> bool { + self.code >= 300 && self.code < 400 + } + + pub fn is_temp_error(&self) -> bool { + self.code >= 400 && self.code < 500 + } + + pub fn is_perm_error(&self) -> bool { + self.code >= 500 + } + + /// Full response text (all lines joined). + pub fn full_message(&self) -> String { + self.lines.join(" ") + } + + /// Convert to a protocol error if this is an error response. + pub fn to_error(&self) -> SmtpClientError { + SmtpClientError::ProtocolError { + code: self.code, + message: self.full_message(), + } + } +} + +/// Server capabilities parsed from EHLO response. +#[derive(Debug, Clone, Default)] +pub struct EhloCapabilities { + pub extensions: Vec, + pub max_size: Option, + pub starttls: bool, + pub auth_methods: Vec, + pub pipelining: bool, + pub eight_bit_mime: bool, +} + +/// Read a multi-line SMTP response from the server. +pub async fn read_response( + stream: &mut ClientSmtpStream, + timeout_secs: u64, +) -> Result { + let mut lines = Vec::new(); + let mut code: u16; + + loop { + let mut line = String::new(); + let n = timeout( + Duration::from_secs(timeout_secs), + stream.read_line(&mut line), + ) + .await + .map_err(|_| SmtpClientError::TimeoutError { + message: format!("Timeout reading SMTP response after {timeout_secs}s"), + })??; + + if n == 0 { + return Err(SmtpClientError::ConnectionError { + message: "Connection closed while reading response".into(), + }); + } + + // Guard against unbounded lines from malicious servers (RFC 5321 §4.5.3.1.4 says 512 max) + if line.len() > 4096 { + return Err(SmtpClientError::ProtocolError { + code: 0, + message: format!("Response line too long ({} bytes, max 4096)", line.len()), + }); + } + + let line = line.trim_end_matches('\n').trim_end_matches('\r'); + + if line.len() < 3 { + return Err(SmtpClientError::ProtocolError { + code: 0, + message: format!("Invalid response line: {line}"), + }); + } + + // Parse the 3-digit code + let parsed_code: u16 = line[..3].parse().map_err(|_| SmtpClientError::ProtocolError { + code: 0, + message: format!("Invalid response code in: {line}"), + })?; + code = parsed_code; + + // Text after the code (skip the separator character) + let text = if line.len() > 4 { &line[4..] } else { "" }; + lines.push(text.to_string()); + + // Check for continuation: "250-" means more lines, "250 " means last line + if line.len() >= 4 && line.as_bytes()[3] == b'-' { + continue; + } else { + break; + } + } + + debug!("SMTP response: {} {}", code, lines.join(" | ")); + Ok(SmtpClientResponse { code, lines }) +} + +/// Read the server greeting (first response after connect). +pub async fn read_greeting( + stream: &mut ClientSmtpStream, + timeout_secs: u64, +) -> Result { + let resp = read_response(stream, timeout_secs).await?; + if resp.code == 220 { + Ok(resp) + } else { + Err(SmtpClientError::ProtocolError { + code: resp.code, + message: format!("Unexpected greeting: {}", resp.full_message()), + }) + } +} + +/// Send a raw command and read the response. +async fn send_command( + stream: &mut ClientSmtpStream, + command: &str, + timeout_secs: u64, +) -> Result { + debug!("SMTP C: {}", command); + stream + .write_all(format!("{command}\r\n").as_bytes()) + .await?; + stream.flush().await?; + read_response(stream, timeout_secs).await +} + +/// Send EHLO and parse capabilities. +pub async fn send_ehlo( + stream: &mut ClientSmtpStream, + domain: &str, + timeout_secs: u64, +) -> Result { + let resp = send_command(stream, &format!("EHLO {domain}"), timeout_secs).await?; + + if !resp.is_success() { + // Fall back to HELO + let helo_resp = send_command(stream, &format!("HELO {domain}"), timeout_secs).await?; + if !helo_resp.is_success() { + return Err(helo_resp.to_error()); + } + return Ok(EhloCapabilities::default()); + } + + let mut caps = EhloCapabilities::default(); + + // First line is the greeting, remaining lines are capabilities + for line in resp.lines.iter().skip(1) { + let upper = line.to_uppercase(); + if upper.starts_with("SIZE ") { + caps.max_size = upper[5..].trim().parse().ok(); + } else if upper == "STARTTLS" { + caps.starttls = true; + } else if upper.starts_with("AUTH ") { + caps.auth_methods = upper[5..] + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + } else if upper == "PIPELINING" { + caps.pipelining = true; + } else if upper == "8BITMIME" { + caps.eight_bit_mime = true; + } + caps.extensions.push(line.clone()); + } + + Ok(caps) +} + +/// Send STARTTLS command (does not perform the TLS handshake itself). +pub async fn send_starttls( + stream: &mut ClientSmtpStream, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + let resp = send_command(stream, "STARTTLS", timeout_secs).await?; + if resp.code != 220 { + return Err(SmtpClientError::ProtocolError { + code: resp.code, + message: format!("STARTTLS rejected: {}", resp.full_message()), + }); + } + Ok(()) +} + +/// Authenticate using AUTH PLAIN. +pub async fn send_auth_plain( + stream: &mut ClientSmtpStream, + user: &str, + pass: &str, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + // AUTH PLAIN sends \0user\0pass in base64 + let credentials = format!("\x00{user}\x00{pass}"); + let encoded = BASE64.encode(credentials.as_bytes()); + let resp = send_command(stream, &format!("AUTH PLAIN {encoded}"), timeout_secs).await?; + + if resp.code != 235 { + return Err(SmtpClientError::AuthenticationError { + message: format!("AUTH PLAIN failed ({}): {}", resp.code, resp.full_message()), + }); + } + Ok(()) +} + +/// Authenticate using AUTH LOGIN. +pub async fn send_auth_login( + stream: &mut ClientSmtpStream, + user: &str, + pass: &str, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + // Step 1: Send AUTH LOGIN + let resp = send_command(stream, "AUTH LOGIN", timeout_secs).await?; + if resp.code != 334 { + return Err(SmtpClientError::AuthenticationError { + message: format!( + "AUTH LOGIN challenge failed ({}): {}", + resp.code, + resp.full_message() + ), + }); + } + + // Step 2: Send base64 username + let user_b64 = BASE64.encode(user.as_bytes()); + let resp = send_command(stream, &user_b64, timeout_secs).await?; + if resp.code != 334 { + return Err(SmtpClientError::AuthenticationError { + message: format!( + "AUTH LOGIN username rejected ({}): {}", + resp.code, + resp.full_message() + ), + }); + } + + // Step 3: Send base64 password + let pass_b64 = BASE64.encode(pass.as_bytes()); + let resp = send_command(stream, &pass_b64, timeout_secs).await?; + if resp.code != 235 { + return Err(SmtpClientError::AuthenticationError { + message: format!( + "AUTH LOGIN password rejected ({}): {}", + resp.code, + resp.full_message() + ), + }); + } + + Ok(()) +} + +/// Authenticate using the configured method. +pub async fn authenticate( + stream: &mut ClientSmtpStream, + auth: &SmtpAuthConfig, + _caps: &EhloCapabilities, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + match auth.method.to_uppercase().as_str() { + "LOGIN" => send_auth_login(stream, &auth.user, &auth.pass, timeout_secs).await, + _ => send_auth_plain(stream, &auth.user, &auth.pass, timeout_secs).await, + } +} + +/// Send MAIL FROM. +pub async fn send_mail_from( + stream: &mut ClientSmtpStream, + sender: &str, + timeout_secs: u64, +) -> Result { + let resp = send_command(stream, &format!("MAIL FROM:<{sender}>"), timeout_secs).await?; + if !resp.is_success() { + return Err(resp.to_error()); + } + Ok(resp) +} + +/// Send RCPT TO. Returns per-recipient success/failure. +pub async fn send_rcpt_to( + stream: &mut ClientSmtpStream, + recipient: &str, + timeout_secs: u64, +) -> Result { + let resp = send_command(stream, &format!("RCPT TO:<{recipient}>"), timeout_secs).await?; + // We don't fail the entire send on per-recipient errors; + // the caller decides based on the response code. + Ok(resp) +} + +/// Send DATA command, followed by the message body with dot-stuffing. +pub async fn send_data( + stream: &mut ClientSmtpStream, + message: &[u8], + timeout_secs: u64, +) -> Result { + // Send DATA command + let resp = send_command(stream, "DATA", timeout_secs).await?; + if !resp.is_positive_intermediate() { + return Err(resp.to_error()); + } + + // Send the message body with dot-stuffing + let stuffed = dot_stuff(message); + stream.write_all(&stuffed).await?; + + // Send terminator: CRLF.CRLF + // If the message doesn't end with CRLF, add one + if !stuffed.ends_with(b"\r\n") { + stream.write_all(b"\r\n").await?; + } + stream.write_all(b".\r\n").await?; + stream.flush().await?; + + // Read final response + let final_resp = read_response(stream, timeout_secs).await?; + if !final_resp.is_success() { + return Err(final_resp.to_error()); + } + + Ok(final_resp) +} + +/// Send RSET command to reset the server state between messages on a reused connection. +pub async fn send_rset( + stream: &mut ClientSmtpStream, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + let resp = send_command(stream, "RSET", timeout_secs).await?; + if !resp.is_success() { + return Err(resp.to_error()); + } + Ok(()) +} + +/// Send QUIT command. +pub async fn send_quit( + stream: &mut ClientSmtpStream, + timeout_secs: u64, +) -> Result<(), SmtpClientError> { + // Best-effort QUIT — ignore errors since we're closing anyway + let _ = send_command(stream, "QUIT", timeout_secs).await; + Ok(()) +} + +/// Apply SMTP dot-stuffing to a message body. +/// +/// Any line starting with a period gets an extra period prepended. +/// Also normalizes bare LF to CRLF. +pub fn dot_stuff(data: &[u8]) -> Vec { + let mut result = Vec::with_capacity(data.len() + data.len() / 40); + let mut at_line_start = true; + + for i in 0..data.len() { + let byte = data[i]; + + // Normalize bare LF to CRLF + if byte == b'\n' && (i == 0 || data[i - 1] != b'\r') { + result.push(b'\r'); + result.push(b'\n'); + at_line_start = true; + continue; + } + + // Dot-stuff: add extra dot at start of line + if at_line_start && byte == b'.' { + result.push(b'.'); + } + + result.push(byte); + at_line_start = byte == b'\n'; + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dot_stuffing_basic() { + assert_eq!( + dot_stuff(b"Hello\r\n.World\r\n"), + b"Hello\r\n..World\r\n" + ); + } + + #[test] + fn test_dot_stuffing_leading_dot() { + assert_eq!(dot_stuff(b".starts with dot\r\n"), b"..starts with dot\r\n"); + } + + #[test] + fn test_dot_stuffing_multiple_dots() { + assert_eq!( + dot_stuff(b"ok\r\n.line1\r\n..line2\r\n"), + b"ok\r\n..line1\r\n...line2\r\n" + ); + } + + #[test] + fn test_dot_stuffing_bare_lf() { + assert_eq!( + dot_stuff(b"line1\nline2\n"), + b"line1\r\nline2\r\n" + ); + } + + #[test] + fn test_dot_stuffing_bare_lf_with_dot() { + assert_eq!( + dot_stuff(b"ok\n.dotline\n"), + b"ok\r\n..dotline\r\n" + ); + } + + #[test] + fn test_dot_stuffing_no_change() { + assert_eq!( + dot_stuff(b"Hello World\r\nNo dots here\r\n"), + b"Hello World\r\nNo dots here\r\n" + ); + } + + #[test] + fn test_dot_stuffing_empty() { + assert_eq!(dot_stuff(b""), b""); + } + + #[test] + fn test_response_is_success() { + let resp = SmtpClientResponse { + code: 250, + lines: vec!["OK".into()], + }; + assert!(resp.is_success()); + assert!(!resp.is_temp_error()); + assert!(!resp.is_perm_error()); + } + + #[test] + fn test_response_temp_error() { + let resp = SmtpClientResponse { + code: 450, + lines: vec!["Mailbox busy".into()], + }; + assert!(!resp.is_success()); + assert!(resp.is_temp_error()); + } + + #[test] + fn test_response_perm_error() { + let resp = SmtpClientResponse { + code: 550, + lines: vec!["No such user".into()], + }; + assert!(!resp.is_success()); + assert!(resp.is_perm_error()); + } + + #[test] + fn test_response_positive_intermediate() { + let resp = SmtpClientResponse { + code: 354, + lines: vec!["Start mail input".into()], + }; + assert!(resp.is_positive_intermediate()); + assert!(!resp.is_success()); + } + + #[test] + fn test_response_full_message() { + let resp = SmtpClientResponse { + code: 250, + lines: vec!["OK".into(), "SIZE 10485760".into()], + }; + assert_eq!(resp.full_message(), "OK SIZE 10485760"); + } + + #[test] + fn test_ehlo_capabilities_default() { + let caps = EhloCapabilities::default(); + assert!(!caps.starttls); + assert!(!caps.pipelining); + assert!(!caps.eight_bit_mime); + assert!(caps.auth_methods.is_empty()); + assert!(caps.max_size.is_none()); + } +} diff --git a/rust/crates/mailer-smtp/src/lib.rs b/rust/crates/mailer-smtp/src/lib.rs index 888580a..1e39059 100644 --- a/rust/crates/mailer-smtp/src/lib.rs +++ b/rust/crates/mailer-smtp/src/lib.rs @@ -12,6 +12,7 @@ //! - TCP/TLS server (`server`) //! - Connection handling (`connection`) +pub mod client; pub mod command; pub mod config; pub mod connection; diff --git a/scripts/compile-all.sh b/scripts/compile-all.sh deleted file mode 100755 index 1b27494..0000000 --- a/scripts/compile-all.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -set -e - -# Get version from deno.json -VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4) -BINARY_DIR="dist/binaries" - -echo "================================================" -echo " MAILER Compilation Script" -echo " Version: ${VERSION}" -echo "================================================" -echo "" -echo "Compiling for all supported platforms..." -echo "" - -# Clean up old binaries and create fresh directory -rm -rf "$BINARY_DIR" -mkdir -p "$BINARY_DIR" -echo "→ Cleaned old binaries from $BINARY_DIR" -echo "" - -# Linux x86_64 -echo "→ Compiling for Linux x86_64..." -deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-linux-x64" \ - --target x86_64-unknown-linux-gnu mod.ts -echo " ✓ Linux x86_64 complete" -echo "" - -# Linux ARM64 -echo "→ Compiling for Linux ARM64..." -deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-linux-arm64" \ - --target aarch64-unknown-linux-gnu mod.ts -echo " ✓ Linux ARM64 complete" -echo "" - -# macOS x86_64 -echo "→ Compiling for macOS x86_64..." -deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-macos-x64" \ - --target x86_64-apple-darwin mod.ts -echo " ✓ macOS x86_64 complete" -echo "" - -# macOS ARM64 -echo "→ Compiling for macOS ARM64..." -deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-macos-arm64" \ - --target aarch64-apple-darwin mod.ts -echo " ✓ macOS ARM64 complete" -echo "" - -# Windows x86_64 -echo "→ Compiling for Windows x86_64..." -deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-windows-x64.exe" \ - --target x86_64-pc-windows-msvc mod.ts -echo " ✓ Windows x86_64 complete" -echo "" - -echo "================================================" -echo " Compilation Summary" -echo "================================================" -echo "" -ls -lh "$BINARY_DIR/" | tail -n +2 -echo "" -echo "✓ All binaries compiled successfully!" -echo "" -echo "Binary location: $BINARY_DIR/" -echo "" diff --git a/test/helpers/smtp.client.ts b/test/helpers/smtp.client.ts deleted file mode 100644 index 834abfe..0000000 --- a/test/helpers/smtp.client.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { smtpClientMod } from '../../ts/mail/delivery/index.js'; -import type { ISmtpClientOptions, SmtpClient } from '../../ts/mail/delivery/smtpclient/index.js'; -import { Email } from '../../ts/mail/core/classes.email.js'; - -/** - * Create a test SMTP client - */ -export function createTestSmtpClient(options: Partial = {}): SmtpClient { - const defaultOptions: ISmtpClientOptions = { - host: options.host || 'localhost', - port: options.port || 2525, - secure: options.secure || false, - auth: options.auth, - connectionTimeout: options.connectionTimeout || 5000, - socketTimeout: options.socketTimeout || 5000, - maxConnections: options.maxConnections || 5, - maxMessages: options.maxMessages || 100, - debug: options.debug || false, - tls: options.tls || { - rejectUnauthorized: false - } - }; - - return smtpClientMod.createSmtpClient(defaultOptions); -} - -/** - * Send test email using SMTP client - */ -export async function sendTestEmail( - client: SmtpClient, - options: { - from?: string; - to?: string | string[]; - subject?: string; - text?: string; - html?: string; - } = {} -): Promise { - const mailOptions = { - from: options.from || 'test@example.com', - to: options.to || 'recipient@example.com', - subject: options.subject || 'Test Email', - text: options.text || 'This is a test email', - html: options.html - }; - - const email = new Email({ - from: mailOptions.from, - to: mailOptions.to, - subject: mailOptions.subject, - text: mailOptions.text, - html: mailOptions.html - }); - return client.sendMail(email); -} - -/** - * Test SMTP client connection - */ -export async function testClientConnection( - host: string, - port: number, - timeout: number = 5000 -): Promise { - const client = createTestSmtpClient({ - host, - port, - connectionTimeout: timeout - }); - - try { - const result = await client.verify(); - return result; - } catch (error) { - throw error; - } finally { - if (client.close) { - await client.close(); - } - } -} - -/** - * Create authenticated SMTP client - */ -export function createAuthenticatedClient( - host: string, - port: number, - username: string, - password: string, - authMethod: 'PLAIN' | 'LOGIN' = 'PLAIN' -): SmtpClient { - return createTestSmtpClient({ - host, - port, - auth: { - user: username, - pass: password, - method: authMethod - }, - secure: false - }); -} - -/** - * Create TLS-enabled SMTP client - */ -export function createTlsClient( - host: string, - port: number, - options: { - secure?: boolean; - rejectUnauthorized?: boolean; - } = {} -): SmtpClient { - return createTestSmtpClient({ - host, - port, - secure: options.secure || false, - tls: { - rejectUnauthorized: options.rejectUnauthorized || false - } - }); -} - -/** - * Test client pool status - */ -export async function testClientPoolStatus(client: SmtpClient): Promise { - if (typeof client.getPoolStatus === 'function') { - return client.getPoolStatus(); - } - - // Fallback for clients without pool status - return { - size: 1, - available: 1, - pending: 0, - connecting: 0, - active: 0 - }; -} - -/** - * Send multiple emails concurrently - */ -export async function sendConcurrentEmails( - client: SmtpClient, - count: number, - emailOptions: { - from?: string; - to?: string; - subject?: string; - text?: string; - } = {} -): Promise { - const promises = []; - - for (let i = 0; i < count; i++) { - promises.push( - sendTestEmail(client, { - ...emailOptions, - subject: `${emailOptions.subject || 'Test Email'} ${i + 1}` - }) - ); - } - - return Promise.all(promises); -} - -/** - * Measure client throughput - */ -export async function measureClientThroughput( - client: SmtpClient, - duration: number = 10000, - emailOptions: { - from?: string; - to?: string; - subject?: string; - text?: string; - } = {} -): Promise<{ totalSent: number; successCount: number; errorCount: number; throughput: number }> { - const startTime = Date.now(); - let totalSent = 0; - let successCount = 0; - let errorCount = 0; - - while (Date.now() - startTime < duration) { - try { - await sendTestEmail(client, emailOptions); - successCount++; - } catch (error) { - errorCount++; - } - totalSent++; - } - - const actualDuration = (Date.now() - startTime) / 1000; // in seconds - const throughput = totalSent / actualDuration; - - return { - totalSent, - successCount, - errorCount, - throughput - }; -} \ No newline at end of file diff --git a/test/test.smtp.client.compatibility.ts b/test/test.smtp.client.compatibility.ts deleted file mode 100644 index b94dc7e..0000000 --- a/test/test.smtp.client.compatibility.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { smtpClientMod } from '../ts/mail/delivery/index.js'; -import type { ISmtpClientOptions, SmtpClient } from '../ts/mail/delivery/smtpclient/index.js'; -import { Email } from '../ts/mail/core/classes.email.js'; - -/** - * Compatibility tests for the legacy SMTP client facade - */ - -tap.test('verify backward compatibility - client creation', async () => { - // Create test configuration - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false, - connectionTimeout: 10000, - domain: 'test.example.com' - }; - - // Create SMTP client instance using legacy constructor - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Verify instance was created correctly - expect(smtpClient).toBeTruthy(); - expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected -}); - -tap.test('verify backward compatibility - methods exist', async () => { - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Verify all expected methods exist - expect(typeof smtpClient.sendMail === 'function').toBeTruthy(); - expect(typeof smtpClient.verify === 'function').toBeTruthy(); - expect(typeof smtpClient.isConnected === 'function').toBeTruthy(); - expect(typeof smtpClient.getPoolStatus === 'function').toBeTruthy(); - expect(typeof smtpClient.updateOptions === 'function').toBeTruthy(); - expect(typeof smtpClient.close === 'function').toBeTruthy(); - expect(typeof smtpClient.on === 'function').toBeTruthy(); - expect(typeof smtpClient.off === 'function').toBeTruthy(); - expect(typeof smtpClient.emit === 'function').toBeTruthy(); -}); - -tap.test('verify backward compatibility - options update', async () => { - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Test option updates don't throw - expect(() => smtpClient.updateOptions({ - host: 'new-smtp.example.com', - port: 465, - secure: true - })).not.toThrow(); - - expect(() => smtpClient.updateOptions({ - debug: true, - connectionTimeout: 5000 - })).not.toThrow(); -}); - -tap.test('verify backward compatibility - connection failure handling', async () => { - const options: ISmtpClientOptions = { - host: 'nonexistent.invalid.domain', - port: 587, - secure: false, - connectionTimeout: 1000 // Short timeout for faster test - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // verify() should return false for invalid hosts - const isValid = await smtpClient.verify(); - expect(isValid).toBeFalsy(); - - // sendMail should fail gracefully for invalid hosts - const email = new Email({ - from: 'test@example.com', - to: 'recipient@example.com', - subject: 'Test Email', - text: 'This is a test email' - }); - - try { - const result = await smtpClient.sendMail(email); - expect(result.success).toBeFalsy(); - expect(result.error).toBeTruthy(); - } catch (error) { - // Connection errors are expected for invalid domains - expect(error).toBeTruthy(); - } -}); - -tap.test('verify backward compatibility - pool status', async () => { - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false, - pool: true, - maxConnections: 5 - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Get pool status - const status = smtpClient.getPoolStatus(); - expect(status).toBeTruthy(); - expect(typeof status.total === 'number').toBeTruthy(); - expect(typeof status.active === 'number').toBeTruthy(); - expect(typeof status.idle === 'number').toBeTruthy(); - expect(typeof status.pending === 'number').toBeTruthy(); - - // Initially should have no connections - expect(status.total).toEqual(0); - expect(status.active).toEqual(0); - expect(status.idle).toEqual(0); - expect(status.pending).toEqual(0); -}); - -tap.test('verify backward compatibility - event handling', async () => { - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Test event listener methods don't throw - const testListener = () => {}; - - expect(() => smtpClient.on('test', testListener)).not.toThrow(); - expect(() => smtpClient.off('test', testListener)).not.toThrow(); - expect(() => smtpClient.emit('test')).not.toThrow(); -}); - -tap.test('clean up after compatibility tests', async () => { - // No-op - just to make sure everything is cleaned up properly -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.smtp.client.rust.node.ts b/test/test.smtp.client.rust.node.ts new file mode 100644 index 0000000..bc5484d --- /dev/null +++ b/test/test.smtp.client.rust.node.ts @@ -0,0 +1,107 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { RustSecurityBridge, BridgeState } from '../ts/security/classes.rustsecuritybridge.js'; +import type { ISmtpSendOptions, ISmtpPoolStatus } from '../ts/security/classes.rustsecuritybridge.js'; + +let bridge: RustSecurityBridge; + +tap.test('Rust SMTP Client - should start bridge', async () => { + RustSecurityBridge.resetInstance(); + bridge = RustSecurityBridge.getInstance(); + const ok = await bridge.start(); + if (!ok) { + console.log('WARNING: Rust binary not available — skipping Rust SMTP client tests'); + console.log('Build it with: cd rust && cargo build --release'); + } + expect(typeof ok).toEqual('boolean'); +}); + +tap.test('Rust SMTP Client - getSmtpPoolStatus returns valid structure', async () => { + if (!bridge.running) { + console.log('SKIP: bridge not running'); + return; + } + const status: ISmtpPoolStatus = await bridge.getSmtpPoolStatus(); + expect(status).toBeTruthy(); + expect(status.pools).toBeTruthy(); + expect(typeof status.pools).toEqual('object'); +}); + +tap.test('Rust SMTP Client - verifySmtpConnection with unreachable host', async () => { + if (!bridge.running) { + console.log('SKIP: bridge not running'); + return; + } + try { + // Use a non-routable IP to test connection failure handling + const result = await bridge.verifySmtpConnection({ + host: '192.0.2.1', // TEST-NET-1 (RFC 5737) - guaranteed unreachable + port: 25, + secure: false, + domain: 'test.example.com', + }); + // If it returns rather than throwing, reachable should be false + expect(result.reachable).toBeFalse(); + } catch (err) { + // Connection errors are expected for unreachable hosts + expect(err).toBeTruthy(); + expect(err.message || String(err)).toBeTruthy(); + } +}); + +tap.test('Rust SMTP Client - sendEmail with connection refused error', async () => { + if (!bridge.running) { + console.log('SKIP: bridge not running'); + return; + } + const opts: ISmtpSendOptions = { + host: '127.0.0.1', + port: 52599, // random high port - should be refused + secure: false, + domain: 'test.example.com', + email: { + from: 'sender@example.com', + to: ['recipient@example.com'], + subject: 'Test email', + text: 'This is a test.', + }, + connectionTimeoutSecs: 5, + socketTimeoutSecs: 5, + }; + + try { + await bridge.sendOutboundEmail(opts); + // Should not succeed — no server is listening + throw new Error('Expected sendEmail to fail on connection refused'); + } catch (err) { + // We expect a connection error + expect(err).toBeTruthy(); + const msg = err.message || String(err); + expect(msg.length).toBeGreaterThan(0); + } +}); + +tap.test('Rust SMTP Client - closeSmtpPool cleans up', async () => { + if (!bridge.running) { + console.log('SKIP: bridge not running'); + return; + } + // Should not throw, even when no pools exist + await bridge.closeSmtpPool(); + + // Verify pool status is empty after close + const status = await bridge.getSmtpPoolStatus(); + expect(status.pools).toBeTruthy(); + const poolKeys = Object.keys(status.pools); + expect(poolKeys.length).toEqual(0); +}); + +tap.test('Rust SMTP Client - stop bridge', async () => { + if (!bridge.running) { + console.log('SKIP: bridge not running'); + return; + } + await bridge.stop(); + expect(bridge.state).toEqual(BridgeState.Stopped); +}); + +export default tap.start(); diff --git a/test/test.smtp.client.ts b/test/test.smtp.client.ts deleted file mode 100644 index 0f5e2d8..0000000 --- a/test/test.smtp.client.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.js'; -import * as paths from '../ts/paths.js'; -import { smtpClientMod } from '../ts/mail/delivery/index.js'; -import type { ISmtpClientOptions, SmtpClient } from '../ts/mail/delivery/smtpclient/index.js'; -import { Email } from '../ts/mail/core/classes.email.js'; - -/** - * Tests for the SMTP client class - */ -tap.test('verify SMTP client initialization', async () => { - // Create test configuration - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false, - connectionTimeout: 10000, - domain: 'test.example.com' - }; - - // Create SMTP client instance - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Verify instance was created correctly - expect(smtpClient).toBeTruthy(); - expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected -}); - -tap.test('test SMTP client configuration update', async () => { - // Create test configuration - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false - }; - - // Create SMTP client instance - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Update configuration - smtpClient.updateOptions({ - host: 'new-smtp.example.com', - port: 465, - secure: true - }); - - // Can't directly test private fields, but we can verify it doesn't throw - expect(() => smtpClient.updateOptions({ - tls: { - rejectUnauthorized: false - } - })).not.toThrow(); -}); - -// Mocked SMTP server for testing -class MockSmtpServer { - private responses: Map; - - constructor() { - this.responses = new Map(); - - // Default responses - this.responses.set('connect', '220 smtp.example.com ESMTP ready'); - this.responses.set('EHLO', '250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 HELP'); - this.responses.set('MAIL FROM', '250 OK'); - this.responses.set('RCPT TO', '250 OK'); - this.responses.set('DATA', '354 Start mail input; end with .'); - this.responses.set('data content', '250 OK: message accepted'); - this.responses.set('QUIT', '221 Bye'); - } - - public setResponse(command: string, response: string): void { - this.responses.set(command, response); - } - - public getResponse(command: string): string { - if (command.startsWith('MAIL FROM')) { - return this.responses.get('MAIL FROM') || '250 OK'; - } else if (command.startsWith('RCPT TO')) { - return this.responses.get('RCPT TO') || '250 OK'; - } else if (command.startsWith('EHLO') || command.startsWith('HELO')) { - return this.responses.get('EHLO') || '250 OK'; - } else if (command === 'DATA') { - return this.responses.get('DATA') || '354 Start mail input; end with .'; - } else if (command.includes('Content-Type')) { - return this.responses.get('data content') || '250 OK: message accepted'; - } else if (command === 'QUIT') { - return this.responses.get('QUIT') || '221 Bye'; - } - - return this.responses.get(command) || '250 OK'; - } -} - -/** - * This test validates the SMTP client public interface - */ -tap.test('verify SMTP client email delivery functionality with mock', async () => { - // Create a test email - const testEmail = new Email({ - from: 'sender@example.com', - to: ['recipient@example.com'], - subject: 'Test Email', - text: 'This is a test email' - }); - - // Create SMTP client options - const options: ISmtpClientOptions = { - host: 'smtp.example.com', - port: 587, - secure: false, - domain: 'test.example.com', - auth: { - user: 'testuser', - pass: 'testpass' - } - }; - - // Create SMTP client instance - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Test public methods exist and have correct signatures - expect(typeof smtpClient.sendMail).toEqual('function'); - expect(typeof smtpClient.verify).toEqual('function'); - expect(typeof smtpClient.isConnected).toEqual('function'); - expect(typeof smtpClient.getPoolStatus).toEqual('function'); - expect(typeof smtpClient.updateOptions).toEqual('function'); - expect(typeof smtpClient.close).toEqual('function'); - - // Test connection status before any operation - expect(smtpClient.isConnected()).toBeFalsy(); - - // Test pool status - const poolStatus = smtpClient.getPoolStatus(); - expect(poolStatus).toBeTruthy(); - expect(typeof poolStatus.active).toEqual('number'); - expect(typeof poolStatus.idle).toEqual('number'); - expect(typeof poolStatus.total).toEqual('number'); - - // Since we can't connect to a real server, we'll skip the actual send test - // and just verify the client was created correctly - expect(smtpClient).toBeTruthy(); -}); - -tap.test('test SMTP client error handling with mock', async () => { - // Create SMTP client instance - const smtpClient = smtpClientMod.createSmtpClient({ - host: 'smtp.example.com', - port: 587, - secure: false - }); - - // Test with valid email (Email class might allow any string) - const testEmail = new Email({ - from: 'sender@example.com', - to: ['recipient@example.com'], - subject: 'Test Email', - text: 'This is a test email' - }); - - // Test event listener methods - const mockListener = () => {}; - smtpClient.on('test-event', mockListener); - smtpClient.off('test-event', mockListener); - - // Test update options - smtpClient.updateOptions({ - auth: { - user: 'newuser', - pass: 'newpass' - } - }); - - // Verify client is still functional - expect(smtpClient.isConnected()).toBeFalsy(); - - // Test close on a non-connected client - await smtpClient.close(); - expect(smtpClient.isConnected()).toBeFalsy(); -}); - -// Final clean-up test -tap.test('clean up after tests', async () => { - // No-op - just to make sure everything is cleaned up properly -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index d75bf39..66d160f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartmta', - version: '3.0.0', + version: '4.0.0', description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' } diff --git a/ts/mail/delivery/classes.delivery.system.ts b/ts/mail/delivery/classes.delivery.system.ts index 1c4bec9..c9c57d3 100644 --- a/ts/mail/delivery/classes.delivery.system.ts +++ b/ts/mail/delivery/classes.delivery.system.ts @@ -1,17 +1,14 @@ import * as plugins from '../../plugins.js'; import { EventEmitter } from 'node:events'; -import * as net from 'node:net'; -import * as tls from 'node:tls'; import { logger } from '../../logger.js'; -import { - SecurityLogger, - SecurityLogLevel, - SecurityEventType +import { + SecurityLogger, + SecurityLogLevel, + SecurityEventType } from '../../security/index.js'; import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js'; import type { Email } from '../core/classes.email.js'; import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; -import type { SmtpClient } from './smtpclient/smtp-client.js'; import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js'; /** @@ -117,7 +114,7 @@ export class MultiModeDeliverySystem extends EventEmitter { * Create a new multi-mode delivery system * @param queue Unified delivery queue * @param options Delivery options - * @param emailServer Optional reference to unified email server for SmtpClient access + * @param emailServer Optional reference to unified email server for outbound delivery */ constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions, emailServer?: UnifiedEmailServer) { super(); @@ -433,191 +430,52 @@ export class MultiModeDeliverySystem extends EventEmitter { */ private async handleForwardDelivery(item: IQueueItem): Promise { logger.log('info', `Forward delivery for item ${item.id}`); - + const email = item.processingResult as Email; const route = item.route; - + // Get target server information const targetServer = route?.action.forward?.host; const targetPort = route?.action.forward?.port || 25; - const useTls = false; // TLS configuration can be enhanced later - + if (!targetServer) { throw new Error('No target server configured for forward mode'); } - - logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`); - + + logger.log('info', `Forwarding email to ${targetServer}:${targetPort}`); + try { - // Get SMTP client from email server if available if (!this.emailServer) { - // Fall back to raw socket implementation if no email server - logger.log('warn', 'No email server available, falling back to raw socket implementation'); - return this.handleForwardDeliveryLegacy(item); + throw new Error('No email server available for forward delivery'); } - - // Get SMTP client from UnifiedEmailServer - const smtpClient = this.emailServer.getSmtpClient(targetServer, targetPort); - - // Apply DKIM signing if configured in the route - if (item.route?.action.options?.mtaOptions?.dkimSign) { - await this.applyDkimSigning(email, item.route.action.options.mtaOptions); - } - - // Send the email using SmtpClient - const result = await smtpClient.sendMail(email); - - if (result.success) { - logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`); - - return { - targetServer: targetServer, - targetPort: targetPort, - recipients: result.acceptedRecipients.length, - messageId: result.messageId, - rejectedRecipients: result.rejectedRecipients - }; - } else { - throw new Error(result.error?.message || 'Failed to forward email'); - } - } catch (error: any) { - logger.log('error', `Failed to forward email: ${error.message}`); - throw error; - } - } - - /** - * Legacy forward delivery using raw sockets (fallback) - * @param item Queue item - */ - private async handleForwardDeliveryLegacy(item: IQueueItem): Promise { - const email = item.processingResult as Email; - const route = item.route; - - // Get target server information - const targetServer = route?.action.forward?.host; - const targetPort = route?.action.forward?.port || 25; - const useTls = false; // TLS configuration can be enhanced later - - if (!targetServer) { - throw new Error('No target server configured for forward mode'); - } - - // Create a socket connection to the target server - const socket = new net.Socket(); - - // Set timeout - socket.setTimeout(this.options.socketTimeout); - - try { - // Connect to the target server - await new Promise((resolve, reject) => { - // Handle connection events - socket.on('connect', () => { - logger.log('debug', `Connected to ${targetServer}:${targetPort}`); - resolve(); - }); - - socket.on('timeout', () => { - reject(new Error(`Connection timeout to ${targetServer}:${targetPort}`)); - }); - - socket.on('error', (err) => { - reject(new Error(`Connection error to ${targetServer}:${targetPort}: ${err.message}`)); - }); - - // Connect to the server - socket.connect({ - host: targetServer, - port: targetPort - }); + + // Build DKIM options from route config + const dkimDomain = item.route?.action.options?.mtaOptions?.dkimSign + ? (item.route.action.options.mtaOptions.dkimOptions?.domainName || email.from.split('@')[1]) + : undefined; + const dkimSelector = item.route?.action.options?.mtaOptions?.dkimOptions?.keySelector || 'default'; + + // Build auth options from route forward config + const auth = route?.action.forward?.auth as { user: string; pass: string } | undefined; + + // Send via Rust SMTP client + const result = await this.emailServer.sendOutboundEmail(targetServer, targetPort, email, { + auth, + dkimDomain, + dkimSelector, }); - - // Send EHLO - await this.smtpCommand(socket, `EHLO ${route?.action.options?.mtaOptions?.domain || 'localhost'}`); - - // Start TLS if required - if (useTls) { - await this.smtpCommand(socket, 'STARTTLS'); - - // Upgrade to TLS - const tlsSocket = await this.upgradeTls(socket, targetServer); - - // Send EHLO again after STARTTLS - await this.smtpCommand(tlsSocket, `EHLO ${route?.action.options?.mtaOptions?.domain || 'localhost'}`); - - // Use tlsSocket for remaining commands - return this.completeSMTPExchange(tlsSocket, email, route); - } - - // Complete the SMTP exchange - return this.completeSMTPExchange(socket, email, route); - } catch (error: any) { - logger.log('error', `Failed to forward email: ${error.message}`); - - // Close the connection - socket.destroy(); - - throw error; - } - } - - /** - * Complete the SMTP exchange after connection and initial setup - * @param socket Network socket - * @param email Email to send - * @param rule Domain rule - */ - private async completeSMTPExchange(socket: net.Socket | tls.TLSSocket, email: Email, route: any): Promise { - try { - // Authenticate if credentials provided - if (route?.action?.forward?.auth?.user && route?.action?.forward?.auth?.pass) { - // Send AUTH LOGIN - await this.smtpCommand(socket, 'AUTH LOGIN'); - - // Send username (base64) - const username = Buffer.from(route.action.forward.auth.user).toString('base64'); - await this.smtpCommand(socket, username); - - // Send password (base64) - const password = Buffer.from(route.action.forward.auth.pass).toString('base64'); - await this.smtpCommand(socket, password); - } - - // Send MAIL FROM - await this.smtpCommand(socket, `MAIL FROM:<${email.from}>`); - - // Send RCPT TO for each recipient - for (const recipient of email.getAllRecipients()) { - await this.smtpCommand(socket, `RCPT TO:<${recipient}>`); - } - - // Send DATA - await this.smtpCommand(socket, 'DATA'); - - // Send email content (simplified) - const emailContent = await this.getFormattedEmail(email); - await this.smtpData(socket, emailContent); - - // Send QUIT - await this.smtpCommand(socket, 'QUIT'); - - // Close the connection - socket.end(); - - logger.log('info', `Email forwarded successfully to ${route?.action?.forward?.host}:${route?.action?.forward?.port || 25}`); - + + logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`); + return { - targetServer: route?.action?.forward?.host, - targetPort: route?.action?.forward?.port || 25, - recipients: email.getAllRecipients().length + targetServer, + targetPort, + recipients: result.accepted.length, + messageId: result.messageId, + rejectedRecipients: result.rejected, }; } catch (error: any) { logger.log('error', `Failed to forward email: ${error.message}`); - - // Close the connection - socket.destroy(); - throw error; } } @@ -790,210 +648,6 @@ export class MultiModeDeliverySystem extends EventEmitter { } } - /** - * Format email for SMTP transmission - * @param email Email to format - */ - private async getFormattedEmail(email: Email): Promise { - // This is a simplified implementation - // In a full implementation, this would use proper MIME formatting - - let content = ''; - - // Add headers - content += `From: ${email.from}\r\n`; - content += `To: ${email.to.join(', ')}\r\n`; - content += `Subject: ${email.subject}\r\n`; - - // Add additional headers - for (const [name, value] of Object.entries(email.headers || {})) { - content += `${name}: ${value}\r\n`; - } - - // Add content type for multipart - if (email.attachments && email.attachments.length > 0) { - const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`; - content += `MIME-Version: 1.0\r\n`; - content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`; - content += `\r\n`; - - // Add text part - content += `--${boundary}\r\n`; - content += `Content-Type: text/plain; charset="UTF-8"\r\n`; - content += `\r\n`; - content += `${email.text}\r\n`; - - // Add HTML part if present - if (email.html) { - content += `--${boundary}\r\n`; - content += `Content-Type: text/html; charset="UTF-8"\r\n`; - content += `\r\n`; - content += `${email.html}\r\n`; - } - - // Add attachments - for (const attachment of email.attachments) { - content += `--${boundary}\r\n`; - content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`; - content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`; - content += `Content-Transfer-Encoding: base64\r\n`; - content += `\r\n`; - - // Add base64 encoded content - const base64Content = attachment.content.toString('base64'); - - // Split into lines of 76 characters - for (let i = 0; i < base64Content.length; i += 76) { - content += base64Content.substring(i, i + 76) + '\r\n'; - } - } - - // End boundary - content += `--${boundary}--\r\n`; - } else { - // Simple email with just text - content += `Content-Type: text/plain; charset="UTF-8"\r\n`; - content += `\r\n`; - content += `${email.text}\r\n`; - } - - return content; - } - - /** - * Send SMTP command and wait for response - * @param socket Socket connection - * @param command SMTP command to send - */ - private async smtpCommand(socket: net.Socket, command: string): Promise { - return new Promise((resolve, reject) => { - const onData = (data: Buffer) => { - const response = data.toString().trim(); - - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - // Check response code - if (response.charAt(0) === '2' || response.charAt(0) === '3') { - resolve(response); - } else { - reject(new Error(`SMTP error: ${response}`)); - } - }; - - const onError = (err: Error) => { - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - reject(err); - }; - - const onTimeout = () => { - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - reject(new Error('SMTP command timeout')); - }; - - // Set up listeners - socket.once('data', onData); - socket.once('error', onError); - socket.once('timeout', onTimeout); - - // Send command - socket.write(command + '\r\n'); - }); - } - - /** - * Send SMTP DATA command with content - * @param socket Socket connection - * @param data Email content to send - */ - private async smtpData(socket: net.Socket, data: string): Promise { - return new Promise((resolve, reject) => { - const onData = (responseData: Buffer) => { - const response = responseData.toString().trim(); - - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - // Check response code - if (response.charAt(0) === '2') { - resolve(response); - } else { - reject(new Error(`SMTP error: ${response}`)); - } - }; - - const onError = (err: Error) => { - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - reject(err); - }; - - const onTimeout = () => { - // Clean up listeners - socket.removeListener('data', onData); - socket.removeListener('error', onError); - socket.removeListener('timeout', onTimeout); - - reject(new Error('SMTP data timeout')); - }; - - // Set up listeners - socket.once('data', onData); - socket.once('error', onError); - socket.once('timeout', onTimeout); - - // Send data and end with CRLF.CRLF - socket.write(data + '\r\n.\r\n'); - }); - } - - /** - * Upgrade socket to TLS - * @param socket Socket connection - * @param hostname Target hostname for TLS - */ - private async upgradeTls(socket: net.Socket, hostname: string): Promise { - return new Promise((resolve, reject) => { - const tlsOptions: tls.ConnectionOptions = { - socket, - servername: hostname, - rejectUnauthorized: this.options.verifyCertificates, - minVersion: this.options.tlsMinVersion as tls.SecureVersion - }; - - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.once('secureConnect', () => { - resolve(tlsSocket); - }); - - tlsSocket.once('error', (err) => { - reject(new Error(`TLS error: ${err.message}`)); - }); - - tlsSocket.setTimeout(this.options.socketTimeout); - - tlsSocket.once('timeout', () => { - reject(new Error('TLS connection timeout')); - }); - }); - } - /** * Update delivery time statistics */ diff --git a/ts/mail/delivery/classes.emailsendjob.ts b/ts/mail/delivery/classes.emailsendjob.ts deleted file mode 100644 index 6bbe1f0..0000000 --- a/ts/mail/delivery/classes.emailsendjob.ts +++ /dev/null @@ -1,425 +0,0 @@ -import * as plugins from '../../plugins.js'; -import * as paths from '../../paths.js'; -import { Email } from '../core/classes.email.js'; -import { EmailSignJob } from './classes.emailsignjob.js'; -import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; -import type { SmtpClient } from './smtpclient/smtp-client.js'; -import type { ISmtpSendResult } from './smtpclient/interfaces.js'; - -// Configuration options for email sending -export interface IEmailSendOptions { - maxRetries?: number; - retryDelay?: number; // in milliseconds - connectionTimeout?: number; // in milliseconds - tlsOptions?: plugins.tls.ConnectionOptions; - debugMode?: boolean; -} - -// Email delivery status -export enum DeliveryStatus { - PENDING = 'pending', - SENDING = 'sending', - DELIVERED = 'delivered', - FAILED = 'failed', - DEFERRED = 'deferred' // Temporary failure, will retry -} - -// Detailed information about delivery attempts -export interface DeliveryInfo { - status: DeliveryStatus; - attempts: number; - error?: Error; - lastAttempt?: Date; - nextAttempt?: Date; - mxServer?: string; - deliveryTime?: Date; - logs: string[]; -} - -export class EmailSendJob { - emailServerRef: UnifiedEmailServer; - private email: Email; - private mxServers: string[] = []; - private currentMxIndex = 0; - private options: IEmailSendOptions; - public deliveryInfo: DeliveryInfo; - - constructor(emailServerRef: UnifiedEmailServer, emailArg: Email, options: IEmailSendOptions = {}) { - this.email = emailArg; - this.emailServerRef = emailServerRef; - - // Set default options - this.options = { - maxRetries: options.maxRetries || 3, - retryDelay: options.retryDelay || 30000, // 30 seconds - connectionTimeout: options.connectionTimeout || 60000, // 60 seconds - tlsOptions: options.tlsOptions || {}, - debugMode: options.debugMode || false - }; - - // Initialize delivery info - this.deliveryInfo = { - status: DeliveryStatus.PENDING, - attempts: 0, - logs: [] - }; - } - - /** - * Send the email to its recipients - */ - async send(): Promise { - try { - // Check if the email is valid before attempting to send - this.validateEmail(); - - // Resolve MX records for the recipient domain - await this.resolveMxRecords(); - - // Try to send the email - return await this.attemptDelivery(); - } catch (error) { - this.log(`Critical error in send process: ${error.message}`); - this.deliveryInfo.status = DeliveryStatus.FAILED; - this.deliveryInfo.error = error; - - // Save failed email for potential future retry or analysis - await this.saveFailed(); - return DeliveryStatus.FAILED; - } - } - - /** - * Validate the email before sending - */ - private validateEmail(): void { - if (!this.email.to || this.email.to.length === 0) { - throw new Error('No recipients specified'); - } - - if (!this.email.from) { - throw new Error('No sender specified'); - } - - const fromDomain = this.email.getFromDomain(); - if (!fromDomain) { - throw new Error('Invalid sender domain'); - } - } - - /** - * Resolve MX records for the recipient domain - */ - private async resolveMxRecords(): Promise { - const domain = this.email.getPrimaryRecipient()?.split('@')[1]; - if (!domain) { - throw new Error('Invalid recipient domain'); - } - - this.log(`Resolving MX records for domain: ${domain}`); - try { - const addresses = await this.resolveMx(domain); - - // Sort by priority (lowest number = highest priority) - addresses.sort((a, b) => a.priority - b.priority); - - this.mxServers = addresses.map(mx => mx.exchange); - this.log(`Found ${this.mxServers.length} MX servers: ${this.mxServers.join(', ')}`); - - if (this.mxServers.length === 0) { - throw new Error(`No MX records found for domain: ${domain}`); - } - } catch (error) { - this.log(`Failed to resolve MX records: ${error.message}`); - throw new Error(`MX lookup failed for ${domain}: ${error.message}`); - } - } - - /** - * Attempt to deliver the email with retries - */ - private async attemptDelivery(): Promise { - while (this.deliveryInfo.attempts < this.options.maxRetries) { - this.deliveryInfo.attempts++; - this.deliveryInfo.lastAttempt = new Date(); - this.deliveryInfo.status = DeliveryStatus.SENDING; - - try { - this.log(`Delivery attempt ${this.deliveryInfo.attempts} of ${this.options.maxRetries}`); - - // Try each MX server in order of priority - while (this.currentMxIndex < this.mxServers.length) { - const currentMx = this.mxServers[this.currentMxIndex]; - this.deliveryInfo.mxServer = currentMx; - - try { - this.log(`Attempting delivery to MX server: ${currentMx}`); - await this.connectAndSend(currentMx); - - // If we get here, email was sent successfully - this.deliveryInfo.status = DeliveryStatus.DELIVERED; - this.deliveryInfo.deliveryTime = new Date(); - this.log(`Email delivered successfully to ${currentMx}`); - - // Record delivery for sender reputation monitoring - this.recordDeliveryEvent('delivered'); - - // Save successful email record - await this.saveSuccess(); - return DeliveryStatus.DELIVERED; - } catch (error) { - this.log(`Failed to deliver to ${currentMx}: ${error.message}`); - this.currentMxIndex++; - - // If this MX server failed, try the next one - if (this.currentMxIndex >= this.mxServers.length) { - throw error; // No more MX servers to try - } - } - } - - throw new Error('All MX servers failed'); - } catch (error) { - this.deliveryInfo.error = error; - - // Check if this is a permanent failure - if (this.isPermanentFailure(error)) { - this.log('Permanent failure detected, not retrying'); - this.deliveryInfo.status = DeliveryStatus.FAILED; - - // Record permanent failure for bounce management - this.recordDeliveryEvent('bounced', true); - - await this.saveFailed(); - return DeliveryStatus.FAILED; - } - - // This is a temporary failure - if (this.deliveryInfo.attempts < this.options.maxRetries) { - this.log(`Temporary failure, will retry in ${this.options.retryDelay}ms`); - this.deliveryInfo.status = DeliveryStatus.DEFERRED; - this.deliveryInfo.nextAttempt = new Date(Date.now() + this.options.retryDelay); - - // Record temporary failure for monitoring - this.recordDeliveryEvent('deferred'); - - // Reset MX server index for next retry - this.currentMxIndex = 0; - - // Wait before retrying - await this.delay(this.options.retryDelay); - } - } - } - - // If we get here, all retries failed - this.deliveryInfo.status = DeliveryStatus.FAILED; - await this.saveFailed(); - return DeliveryStatus.FAILED; - } - - /** - * Connect to a specific MX server and send the email using SmtpClient - */ - private async connectAndSend(mxServer: string): Promise { - this.log(`Connecting to ${mxServer}:25`); - - try { - // Get SMTP client from UnifiedEmailServer - const smtpClient = this.emailServerRef.getSmtpClient(mxServer, 25); - - // Sign the email with DKIM if available - let signedEmail = this.email; - try { - const fromDomain = this.email.getFromDomain(); - if (fromDomain && this.emailServerRef.hasDkimKey(fromDomain)) { - // Convert email to RFC822 format for signing - const emailMessage = this.email.toRFC822String(); - - // Create sign job with proper options - const emailSignJob = new EmailSignJob(this.emailServerRef, { - domain: fromDomain, - selector: 'default', // Using default selector - headers: {}, // Headers will be extracted from emailMessage - body: emailMessage - }); - - // Get the DKIM signature header - const signatureHeader = await emailSignJob.getSignatureHeader(emailMessage); - - // Add the signature to the email - if (signatureHeader) { - // For now, we'll use the email as-is since SmtpClient will handle DKIM - this.log(`Email ready for DKIM signing for domain: ${fromDomain}`); - } - } - } catch (error) { - this.log(`Failed to prepare DKIM: ${error.message}`); - } - - // Send the email using SmtpClient - const result: ISmtpSendResult = await smtpClient.sendMail(signedEmail); - - if (result.success) { - this.log(`Email sent successfully: ${result.response}`); - - // Record the send for reputation monitoring - this.recordDeliveryEvent('delivered'); - } else { - throw new Error(result.error?.message || 'Failed to send email'); - } - } catch (error) { - this.log(`Failed to send email via ${mxServer}: ${error.message}`); - throw error; - } - } - - /** - * Record delivery event for monitoring - */ - private recordDeliveryEvent( - eventType: 'delivered' | 'bounced' | 'deferred', - isHardBounce: boolean = false - ): void { - try { - const domain = this.email.getFromDomain(); - if (domain) { - if (eventType === 'delivered') { - this.emailServerRef.recordDelivery(domain); - } else if (eventType === 'bounced') { - // Get the receiving domain for bounce recording - let receivingDomain = null; - const primaryRecipient = this.email.getPrimaryRecipient(); - if (primaryRecipient) { - receivingDomain = primaryRecipient.split('@')[1]; - } - - if (receivingDomain) { - this.emailServerRef.recordBounce( - domain, - receivingDomain, - isHardBounce ? 'hard' : 'soft', - this.deliveryInfo.error?.message || 'Unknown error' - ); - } - } - } - } catch (error) { - this.log(`Failed to record delivery event: ${error.message}`); - } - } - - /** - * Check if an error represents a permanent failure - */ - private isPermanentFailure(error: Error): boolean { - const permanentFailurePatterns = [ - 'User unknown', - 'No such user', - 'Mailbox not found', - 'Invalid recipient', - 'Account disabled', - 'Account suspended', - 'Domain not found', - 'No such domain', - 'Invalid domain', - 'Relay access denied', - 'Access denied', - 'Blacklisted', - 'Blocked', - '550', // Permanent failure SMTP code - '551', - '552', - '553', - '554' - ]; - - const errorMessage = error.message.toLowerCase(); - return permanentFailurePatterns.some(pattern => - errorMessage.includes(pattern.toLowerCase()) - ); - } - - /** - * Resolve MX records for a domain - */ - private resolveMx(domain: string): Promise { - return new Promise((resolve, reject) => { - plugins.dns.resolveMx(domain, (err, addresses) => { - if (err) { - reject(err); - } else { - resolve(addresses || []); - } - }); - }); - } - - /** - * Log a message with timestamp - */ - private log(message: string): void { - const timestamp = new Date().toISOString(); - const logEntry = `[${timestamp}] ${message}`; - this.deliveryInfo.logs.push(logEntry); - - if (this.options.debugMode) { - console.log(`[EmailSendJob] ${logEntry}`); - } - } - - /** - * Save successful email to storage - */ - private async saveSuccess(): Promise { - try { - // Use the existing email storage path - const emailContent = this.email.toRFC822String(); - const fileName = `${Date.now()}_${this.email.from}_to_${this.email.to[0]}_success.eml`; - const filePath = plugins.path.join(paths.sentEmailsDir, fileName); - - await plugins.smartfs.directory(paths.sentEmailsDir).recursive().create(); - await plugins.smartfs.file(filePath).write(emailContent); - - // Also save delivery info - const infoFileName = `${Date.now()}_${this.email.from}_to_${this.email.to[0]}_info.json`; - const infoPath = plugins.path.join(paths.sentEmailsDir, infoFileName); - await plugins.smartfs.file(infoPath).write(JSON.stringify(this.deliveryInfo, null, 2)); - - this.log(`Email saved to ${fileName}`); - } catch (error) { - this.log(`Failed to save email: ${error.message}`); - } - } - - /** - * Save failed email to storage - */ - private async saveFailed(): Promise { - try { - // Use the existing email storage path - const emailContent = this.email.toRFC822String(); - const fileName = `${Date.now()}_${this.email.from}_to_${this.email.to[0]}_failed.eml`; - const filePath = plugins.path.join(paths.failedEmailsDir, fileName); - - await plugins.smartfs.directory(paths.failedEmailsDir).recursive().create(); - await plugins.smartfs.file(filePath).write(emailContent); - - // Also save delivery info with error details - const infoFileName = `${Date.now()}_${this.email.from}_to_${this.email.to[0]}_error.json`; - const infoPath = plugins.path.join(paths.failedEmailsDir, infoFileName); - await plugins.smartfs.file(infoPath).write(JSON.stringify(this.deliveryInfo, null, 2)); - - this.log(`Failed email saved to ${fileName}`); - } catch (error) { - this.log(`Failed to save failed email: ${error.message}`); - } - } - - /** - * Delay for specified milliseconds - */ - private delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/classes.emailsignjob.ts b/ts/mail/delivery/classes.emailsignjob.ts deleted file mode 100644 index b838b8a..0000000 --- a/ts/mail/delivery/classes.emailsignjob.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as plugins from '../../plugins.js'; -import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; -import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js'; - -interface Headers { - [key: string]: string; -} - -interface IEmailSignJobOptions { - domain: string; - selector: string; - headers: Headers; - body: string; -} - -export class EmailSignJob { - emailServerRef: UnifiedEmailServer; - jobOptions: IEmailSignJobOptions; - - constructor(emailServerRef: UnifiedEmailServer, options: IEmailSignJobOptions) { - this.emailServerRef = emailServerRef; - this.jobOptions = options; - } - - async loadPrivateKey(): Promise { - const keyInfo = await this.emailServerRef.dkimCreator.readDKIMKeys(this.jobOptions.domain); - return keyInfo.privateKey; - } - - public async getSignatureHeader(emailMessage: string): Promise { - const privateKey = await this.loadPrivateKey(); - const bridge = RustSecurityBridge.getInstance(); - const signResult = await bridge.signDkim({ - rawMessage: emailMessage, - domain: this.jobOptions.domain, - selector: this.jobOptions.selector, - privateKey, - }); - return signResult.header; - } -} diff --git a/ts/mail/delivery/classes.mta.config.ts b/ts/mail/delivery/classes.mta.config.ts deleted file mode 100644 index bb1fa3a..0000000 --- a/ts/mail/delivery/classes.mta.config.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as plugins from '../../plugins.js'; -import * as paths from '../../paths.js'; -import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; - -/** - * Configures email server storage settings - * @param emailServer Reference to the unified email server - * @param options Configuration options containing storage paths - */ -export async function configureEmailStorage(emailServer: UnifiedEmailServer, options: any): Promise { - // Extract the receivedEmailsPath if available - if (options?.emailPortConfig?.receivedEmailsPath) { - const receivedEmailsPath = options.emailPortConfig.receivedEmailsPath; - - // Ensure the directory exists - await plugins.smartfs.directory(receivedEmailsPath).recursive().create(); - - // Set path for received emails - if (emailServer) { - // Storage paths are now handled by the unified email server system - await plugins.smartfs.directory(paths.receivedEmailsDir).recursive().create(); - - console.log(`Configured email server to store received emails to: ${receivedEmailsPath}`); - } - } -} - -/** - * Configure email server with port and storage settings - * @param emailServer Reference to the unified email server - * @param config Configuration settings for email server - */ -export async function configureEmailServer( - emailServer: UnifiedEmailServer, - config: { - ports?: number[]; - hostname?: string; - tls?: { - certPath?: string; - keyPath?: string; - caPath?: string; - }; - storagePath?: string; - } -): Promise { - if (!emailServer) { - console.error('Email server not available'); - return false; - } - - // Configure the email server with updated options - const serverOptions = { - ports: config.ports || [25, 587, 465], - hostname: config.hostname || 'localhost', - tls: config.tls - }; - - // Update the email server options - emailServer.updateOptions(serverOptions); - - console.log(`Configured email server on ports ${serverOptions.ports.join(', ')}`); - - // Set up storage path if provided - if (config.storagePath) { - await configureEmailStorage(emailServer, { - emailPortConfig: { - receivedEmailsPath: config.storagePath - } - }); - } - - return true; -} \ No newline at end of file diff --git a/ts/mail/delivery/classes.ratelimiter.ts b/ts/mail/delivery/classes.ratelimiter.ts deleted file mode 100644 index 26bb4d3..0000000 --- a/ts/mail/delivery/classes.ratelimiter.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { logger } from '../../logger.js'; - -/** - * Configuration options for rate limiter - */ -export interface IRateLimitConfig { - /** Maximum tokens per period */ - maxPerPeriod: number; - - /** Time period in milliseconds */ - periodMs: number; - - /** Whether to apply per domain/key (vs globally) */ - perKey: boolean; - - /** Initial token count (defaults to max) */ - initialTokens?: number; - - /** Grace tokens to allow occasional bursts */ - burstTokens?: number; - - /** Apply global limit in addition to per-key limits */ - useGlobalLimit?: boolean; -} - -/** - * Token bucket for an individual key - */ -interface TokenBucket { - /** Current number of tokens */ - tokens: number; - - /** Last time tokens were refilled */ - lastRefill: number; - - /** Total allowed requests */ - allowed: number; - - /** Total denied requests */ - denied: number; - - /** Error count for blocking decisions */ - errors: number; - - /** Timestamp of first error in current window */ - firstErrorTime: number; -} - -/** - * Rate limiter using token bucket algorithm - * Provides more sophisticated rate limiting with burst handling - */ -export class RateLimiter { - /** Rate limit configuration */ - private config: IRateLimitConfig; - - /** Token buckets per key */ - private buckets: Map = new Map(); - - /** Global bucket for non-keyed rate limiting */ - private globalBucket: TokenBucket; - - /** - * Create a new rate limiter - * @param config Rate limiter configuration - */ - constructor(config: IRateLimitConfig) { - // Set defaults - this.config = { - maxPerPeriod: config.maxPerPeriod, - periodMs: config.periodMs, - perKey: config.perKey ?? true, - initialTokens: config.initialTokens ?? config.maxPerPeriod, - burstTokens: config.burstTokens ?? 0, - useGlobalLimit: config.useGlobalLimit ?? false - }; - - // Initialize global bucket - this.globalBucket = { - tokens: this.config.initialTokens, - lastRefill: Date.now(), - allowed: 0, - denied: 0, - errors: 0, - firstErrorTime: 0 - }; - - // Log initialization - logger.log('info', `Rate limiter initialized: ${this.config.maxPerPeriod} per ${this.config.periodMs}ms${this.config.perKey ? ' per key' : ''}`); - } - - /** - * Check if a request is allowed under rate limits - * @param key Key to check rate limit for (e.g. domain, user, IP) - * @param cost Token cost (defaults to 1) - * @returns Whether the request is allowed - */ - public isAllowed(key: string = 'global', cost: number = 1): boolean { - // If using global bucket directly, just check that - if (key === 'global' || !this.config.perKey) { - return this.checkBucket(this.globalBucket, cost); - } - - // Get the key-specific bucket - const bucket = this.getBucket(key); - - // If we also need to check global limit - if (this.config.useGlobalLimit) { - // Both key bucket and global bucket must have tokens - return this.checkBucket(bucket, cost) && this.checkBucket(this.globalBucket, cost); - } else { - // Only need to check the key-specific bucket - return this.checkBucket(bucket, cost); - } - } - - /** - * Check if a bucket has enough tokens and consume them - * @param bucket The token bucket to check - * @param cost Token cost - * @returns Whether tokens were consumed - */ - private checkBucket(bucket: TokenBucket, cost: number): boolean { - // Refill tokens based on elapsed time - this.refillBucket(bucket); - - // Check if we have enough tokens - if (bucket.tokens >= cost) { - // Use tokens - bucket.tokens -= cost; - bucket.allowed++; - return true; - } else { - // Rate limit exceeded - bucket.denied++; - return false; - } - } - - /** - * Consume tokens for a request (if available) - * @param key Key to consume tokens for - * @param cost Token cost (defaults to 1) - * @returns Whether tokens were consumed - */ - public consume(key: string = 'global', cost: number = 1): boolean { - const isAllowed = this.isAllowed(key, cost); - return isAllowed; - } - - /** - * Get the remaining tokens for a key - * @param key Key to check - * @returns Number of remaining tokens - */ - public getRemainingTokens(key: string = 'global'): number { - const bucket = this.getBucket(key); - this.refillBucket(bucket); - return bucket.tokens; - } - - /** - * Get stats for a specific key - * @param key Key to get stats for - * @returns Rate limit statistics - */ - public getStats(key: string = 'global'): { - remaining: number; - limit: number; - resetIn: number; - allowed: number; - denied: number; - } { - const bucket = this.getBucket(key); - this.refillBucket(bucket); - - // Calculate time until next token - const resetIn = bucket.tokens < this.config.maxPerPeriod ? - Math.ceil(this.config.periodMs / this.config.maxPerPeriod) : - 0; - - return { - remaining: bucket.tokens, - limit: this.config.maxPerPeriod, - resetIn, - allowed: bucket.allowed, - denied: bucket.denied - }; - } - - /** - * Get or create a token bucket for a key - * @param key The rate limit key - * @returns Token bucket - */ - private getBucket(key: string): TokenBucket { - if (!this.config.perKey || key === 'global') { - return this.globalBucket; - } - - if (!this.buckets.has(key)) { - // Create new bucket - this.buckets.set(key, { - tokens: this.config.initialTokens, - lastRefill: Date.now(), - allowed: 0, - denied: 0, - errors: 0, - firstErrorTime: 0 - }); - } - - return this.buckets.get(key); - } - - /** - * Refill tokens in a bucket based on elapsed time - * @param bucket Token bucket to refill - */ - private refillBucket(bucket: TokenBucket): void { - const now = Date.now(); - const elapsedMs = now - bucket.lastRefill; - - // Calculate how many tokens to add - const rate = this.config.maxPerPeriod / this.config.periodMs; - const tokensToAdd = elapsedMs * rate; - - if (tokensToAdd >= 0.1) { // Allow for partial token refills - // Add tokens, but don't exceed the normal maximum (without burst) - // This ensures burst tokens are only used for bursts and don't refill - const normalMax = this.config.maxPerPeriod; - bucket.tokens = Math.min( - // Don't exceed max + burst - this.config.maxPerPeriod + (this.config.burstTokens || 0), - // Don't exceed normal max when refilling - Math.min(normalMax, bucket.tokens + tokensToAdd) - ); - - // Update last refill time - bucket.lastRefill = now; - } - } - - /** - * Reset rate limits for a specific key - * @param key Key to reset - */ - public reset(key: string = 'global'): void { - if (key === 'global' || !this.config.perKey) { - this.globalBucket.tokens = this.config.initialTokens; - this.globalBucket.lastRefill = Date.now(); - } else if (this.buckets.has(key)) { - const bucket = this.buckets.get(key); - bucket.tokens = this.config.initialTokens; - bucket.lastRefill = Date.now(); - } - } - - /** - * Reset all rate limiters - */ - public resetAll(): void { - this.globalBucket.tokens = this.config.initialTokens; - this.globalBucket.lastRefill = Date.now(); - - for (const bucket of this.buckets.values()) { - bucket.tokens = this.config.initialTokens; - bucket.lastRefill = Date.now(); - } - } - - /** - * Cleanup old buckets to prevent memory leaks - * @param maxAge Maximum age in milliseconds - */ - public cleanup(maxAge: number = 24 * 60 * 60 * 1000): void { - const now = Date.now(); - let removed = 0; - - for (const [key, bucket] of this.buckets.entries()) { - if (now - bucket.lastRefill > maxAge) { - this.buckets.delete(key); - removed++; - } - } - - if (removed > 0) { - logger.log('debug', `Cleaned up ${removed} stale rate limit buckets`); - } - } - - /** - * Record an error for a key (e.g., IP address) and determine if blocking is needed - * RFC 5321 Section 4.5.4.1 suggests limiting errors to prevent abuse - * - * @param key Key to record error for (typically an IP address) - * @param errorWindow Time window for error tracking in ms (default: 5 minutes) - * @param errorThreshold Maximum errors before blocking (default: 10) - * @returns true if the key should be blocked due to excessive errors - */ - public recordError(key: string, errorWindow: number = 5 * 60 * 1000, errorThreshold: number = 10): boolean { - const bucket = this.getBucket(key); - const now = Date.now(); - - // Reset error count if the time window has expired - if (bucket.firstErrorTime === 0 || now - bucket.firstErrorTime > errorWindow) { - bucket.errors = 0; - bucket.firstErrorTime = now; - } - - // Increment error count - bucket.errors++; - - // Log error tracking - logger.log('debug', `Error recorded for ${key}: ${bucket.errors}/${errorThreshold} in window`); - - // Check if threshold exceeded - if (bucket.errors >= errorThreshold) { - logger.log('warn', `Error threshold exceeded for ${key}: ${bucket.errors} errors`); - return true; // Should block - } - - return false; // Continue allowing - } -} \ No newline at end of file diff --git a/ts/mail/delivery/index.ts b/ts/mail/delivery/index.ts index e3066a9..3392ac9 100644 --- a/ts/mail/delivery/index.ts +++ b/ts/mail/delivery/index.ts @@ -1,23 +1,5 @@ // Email delivery components -export * from './classes.emailsignjob.js'; export * from './classes.delivery.queue.js'; export * from './classes.delivery.system.js'; - -// Handle exports with naming conflicts -export { EmailSendJob } from './classes.emailsendjob.js'; export { DeliveryStatus } from './classes.delivery.system.js'; - -// Rate limiter exports - fix naming conflict -export { RateLimiter } from './classes.ratelimiter.js'; -export type { IRateLimitConfig } from './classes.ratelimiter.js'; - -// Unified rate limiter export * from './classes.unified.rate.limiter.js'; - -// SMTP client and configuration -export * from './classes.mta.config.js'; - -// Import and export SMTP modules as namespaces to avoid conflicts -import * as smtpClientMod from './smtpclient/index.js'; - -export { smtpClientMod }; \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/auth-handler.ts b/ts/mail/delivery/smtpclient/auth-handler.ts deleted file mode 100644 index ee64f6f..0000000 --- a/ts/mail/delivery/smtpclient/auth-handler.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * SMTP Client Authentication Handler - * Authentication mechanisms implementation - */ - -import { AUTH_METHODS } from './constants.js'; -import type { - ISmtpConnection, - ISmtpAuthOptions, - ISmtpClientOptions, - ISmtpResponse, - IOAuth2Options -} from './interfaces.js'; -import { - encodeAuthPlain, - encodeAuthLogin, - generateOAuth2String, - isSuccessCode -} from './utils/helpers.js'; -import { logAuthentication, logDebug } from './utils/logging.js'; -import type { CommandHandler } from './command-handler.js'; - -export class AuthHandler { - private options: ISmtpClientOptions; - private commandHandler: CommandHandler; - - constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) { - this.options = options; - this.commandHandler = commandHandler; - } - - /** - * Authenticate using the configured method - */ - public async authenticate(connection: ISmtpConnection): Promise { - if (!this.options.auth) { - logDebug('No authentication configured', this.options); - return; - } - - const authOptions = this.options.auth; - const capabilities = connection.capabilities; - - if (!capabilities || capabilities.authMethods.size === 0) { - throw new Error('Server does not support authentication'); - } - - // Determine authentication method - const method = this.selectAuthMethod(authOptions, capabilities.authMethods); - - logAuthentication('start', method, this.options); - - try { - switch (method) { - case AUTH_METHODS.PLAIN: - await this.authenticatePlain(connection, authOptions); - break; - case AUTH_METHODS.LOGIN: - await this.authenticateLogin(connection, authOptions); - break; - case AUTH_METHODS.OAUTH2: - await this.authenticateOAuth2(connection, authOptions); - break; - default: - throw new Error(`Unsupported authentication method: ${method}`); - } - - logAuthentication('success', method, this.options); - } catch (error) { - logAuthentication('failure', method, this.options, { error }); - throw error; - } - } - - /** - * Authenticate using AUTH PLAIN - */ - private async authenticatePlain(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise { - if (!auth.user || !auth.pass) { - throw new Error('Username and password required for PLAIN authentication'); - } - - const credentials = encodeAuthPlain(auth.user, auth.pass); - const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.PLAIN, credentials); - - if (!isSuccessCode(response.code)) { - throw new Error(`PLAIN authentication failed: ${response.message}`); - } - } - - /** - * Authenticate using AUTH LOGIN - */ - private async authenticateLogin(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise { - if (!auth.user || !auth.pass) { - throw new Error('Username and password required for LOGIN authentication'); - } - - // Step 1: Send AUTH LOGIN - let response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.LOGIN); - - if (response.code !== 334) { - throw new Error(`LOGIN authentication initiation failed: ${response.message}`); - } - - // Step 2: Send username - const encodedUser = encodeAuthLogin(auth.user); - response = await this.commandHandler.sendCommand(connection, encodedUser); - - if (response.code !== 334) { - throw new Error(`LOGIN username failed: ${response.message}`); - } - - // Step 3: Send password - const encodedPass = encodeAuthLogin(auth.pass); - response = await this.commandHandler.sendCommand(connection, encodedPass); - - if (!isSuccessCode(response.code)) { - throw new Error(`LOGIN password failed: ${response.message}`); - } - } - - /** - * Authenticate using OAuth2 - */ - private async authenticateOAuth2(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise { - if (!auth.oauth2) { - throw new Error('OAuth2 configuration required for OAUTH2 authentication'); - } - - let accessToken = auth.oauth2.accessToken; - - // Refresh token if needed - if (!accessToken || this.isTokenExpired(auth.oauth2)) { - accessToken = await this.refreshOAuth2Token(auth.oauth2); - } - - const authString = generateOAuth2String(auth.oauth2.user, accessToken); - const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.OAUTH2, authString); - - if (!isSuccessCode(response.code)) { - throw new Error(`OAUTH2 authentication failed: ${response.message}`); - } - } - - /** - * Select appropriate authentication method - */ - private selectAuthMethod(auth: ISmtpAuthOptions, serverMethods: Set): string { - // If method is explicitly specified, use it - if (auth.method && auth.method !== 'AUTO') { - const method = auth.method === 'OAUTH2' ? AUTH_METHODS.OAUTH2 : auth.method; - if (serverMethods.has(method)) { - return method; - } - throw new Error(`Requested authentication method ${auth.method} not supported by server`); - } - - // Auto-select based on available credentials and server support - if (auth.oauth2 && serverMethods.has(AUTH_METHODS.OAUTH2)) { - return AUTH_METHODS.OAUTH2; - } - - if (auth.user && auth.pass) { - // Prefer PLAIN over LOGIN for simplicity - if (serverMethods.has(AUTH_METHODS.PLAIN)) { - return AUTH_METHODS.PLAIN; - } - if (serverMethods.has(AUTH_METHODS.LOGIN)) { - return AUTH_METHODS.LOGIN; - } - } - - throw new Error('No compatible authentication method found'); - } - - /** - * Check if OAuth2 token is expired - */ - private isTokenExpired(oauth2: IOAuth2Options): boolean { - if (!oauth2.expires) { - return false; // No expiry information, assume valid - } - - const now = Date.now(); - const buffer = 300000; // 5 minutes buffer - - return oauth2.expires < (now + buffer); - } - - /** - * Refresh OAuth2 access token - */ - private async refreshOAuth2Token(oauth2: IOAuth2Options): Promise { - // This is a simplified implementation - // In a real implementation, you would make an HTTP request to the OAuth2 provider - logDebug('OAuth2 token refresh required', this.options); - - if (!oauth2.refreshToken) { - throw new Error('Refresh token required for OAuth2 token refresh'); - } - - // TODO: Implement actual OAuth2 token refresh - // For now, throw an error to indicate this needs to be implemented - throw new Error('OAuth2 token refresh not implemented. Please provide a valid access token.'); - } - - /** - * Validate authentication configuration - */ - public validateAuthConfig(auth: ISmtpAuthOptions): string[] { - const errors: string[] = []; - - if (auth.method === 'OAUTH2' || auth.oauth2) { - if (!auth.oauth2) { - errors.push('OAuth2 configuration required when using OAUTH2 method'); - } else { - if (!auth.oauth2.user) errors.push('OAuth2 user required'); - if (!auth.oauth2.clientId) errors.push('OAuth2 clientId required'); - if (!auth.oauth2.clientSecret) errors.push('OAuth2 clientSecret required'); - if (!auth.oauth2.refreshToken && !auth.oauth2.accessToken) { - errors.push('OAuth2 refreshToken or accessToken required'); - } - } - } else if (auth.method === 'PLAIN' || auth.method === 'LOGIN' || (!auth.method && (auth.user || auth.pass))) { - if (!auth.user) errors.push('Username required for basic authentication'); - if (!auth.pass) errors.push('Password required for basic authentication'); - } - - return errors; - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/command-handler.ts b/ts/mail/delivery/smtpclient/command-handler.ts deleted file mode 100644 index 3bb907d..0000000 --- a/ts/mail/delivery/smtpclient/command-handler.ts +++ /dev/null @@ -1,343 +0,0 @@ -/** - * SMTP Client Command Handler - * SMTP command sending and response parsing - */ - -import { EventEmitter } from 'node:events'; -import { SMTP_COMMANDS, SMTP_CODES, LINE_ENDINGS } from './constants.js'; -import type { - ISmtpConnection, - ISmtpResponse, - ISmtpClientOptions, - ISmtpCapabilities -} from './interfaces.js'; -import { - parseSmtpResponse, - parseEhloResponse, - formatCommand, - isSuccessCode -} from './utils/helpers.js'; -import { logCommand, logDebug } from './utils/logging.js'; - -export class CommandHandler extends EventEmitter { - private options: ISmtpClientOptions; - private responseBuffer: string = ''; - private pendingCommand: { resolve: Function; reject: Function; command: string } | null = null; - private commandTimeout: NodeJS.Timeout | null = null; - - constructor(options: ISmtpClientOptions) { - super(); - this.options = options; - } - - /** - * Send EHLO command and parse capabilities - */ - public async sendEhlo(connection: ISmtpConnection, domain?: string): Promise { - const hostname = domain || this.options.domain || 'localhost'; - const command = `${SMTP_COMMANDS.EHLO} ${hostname}`; - - const response = await this.sendCommand(connection, command); - - if (!isSuccessCode(response.code)) { - throw new Error(`EHLO failed: ${response.message}`); - } - - const capabilities = parseEhloResponse(response.raw); - connection.capabilities = capabilities; - - logDebug('EHLO capabilities parsed', this.options, { capabilities }); - return capabilities; - } - - /** - * Send MAIL FROM command - */ - public async sendMailFrom(connection: ISmtpConnection, fromAddress: string): Promise { - // Handle empty return path for bounce messages - const command = fromAddress === '' - ? `${SMTP_COMMANDS.MAIL_FROM}:<>` - : `${SMTP_COMMANDS.MAIL_FROM}:<${fromAddress}>`; - return this.sendCommand(connection, command); - } - - /** - * Send RCPT TO command - */ - public async sendRcptTo(connection: ISmtpConnection, toAddress: string): Promise { - const command = `${SMTP_COMMANDS.RCPT_TO}:<${toAddress}>`; - return this.sendCommand(connection, command); - } - - /** - * Send DATA command - */ - public async sendData(connection: ISmtpConnection): Promise { - return this.sendCommand(connection, SMTP_COMMANDS.DATA); - } - - /** - * Send email data content - */ - public async sendDataContent(connection: ISmtpConnection, emailData: string): Promise { - // Normalize line endings to CRLF - let data = emailData.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'); - - // Ensure email data ends with CRLF - if (!data.endsWith(LINE_ENDINGS.CRLF)) { - data += LINE_ENDINGS.CRLF; - } - - // Perform dot stuffing (escape lines starting with a dot) - data = data.replace(/\r\n\./g, '\r\n..'); - - // Add termination sequence - data += '.' + LINE_ENDINGS.CRLF; - - return this.sendRawData(connection, data); - } - - /** - * Send RSET command - */ - public async sendRset(connection: ISmtpConnection): Promise { - return this.sendCommand(connection, SMTP_COMMANDS.RSET); - } - - /** - * Send NOOP command - */ - public async sendNoop(connection: ISmtpConnection): Promise { - return this.sendCommand(connection, SMTP_COMMANDS.NOOP); - } - - /** - * Send QUIT command - */ - public async sendQuit(connection: ISmtpConnection): Promise { - return this.sendCommand(connection, SMTP_COMMANDS.QUIT); - } - - /** - * Send STARTTLS command - */ - public async sendStartTls(connection: ISmtpConnection): Promise { - return this.sendCommand(connection, SMTP_COMMANDS.STARTTLS); - } - - /** - * Send AUTH command - */ - public async sendAuth(connection: ISmtpConnection, method: string, credentials?: string): Promise { - const command = credentials ? - `${SMTP_COMMANDS.AUTH} ${method} ${credentials}` : - `${SMTP_COMMANDS.AUTH} ${method}`; - return this.sendCommand(connection, command); - } - - /** - * Send a generic SMTP command - */ - public async sendCommand(connection: ISmtpConnection, command: string): Promise { - return new Promise((resolve, reject) => { - if (this.pendingCommand) { - reject(new Error('Another command is already pending')); - return; - } - - this.pendingCommand = { resolve, reject, command }; - - // Set command timeout - const timeout = 30000; // 30 seconds - this.commandTimeout = setTimeout(() => { - this.pendingCommand = null; - this.commandTimeout = null; - reject(new Error(`Command timeout: ${command}`)); - }, timeout); - - // Set up data handler - const dataHandler = (data: Buffer) => { - this.handleIncomingData(data.toString()); - }; - - connection.socket.on('data', dataHandler); - - // Clean up function - const cleanup = () => { - connection.socket.removeListener('data', dataHandler); - if (this.commandTimeout) { - clearTimeout(this.commandTimeout); - this.commandTimeout = null; - } - }; - - // Send command - const formattedCommand = command.endsWith(LINE_ENDINGS.CRLF) ? command : formatCommand(command); - - logCommand(command, undefined, this.options); - logDebug(`Sending command: ${command}`, this.options); - - connection.socket.write(formattedCommand, (error) => { - if (error) { - cleanup(); - this.pendingCommand = null; - reject(error); - } - }); - - // Override resolve/reject to include cleanup - const originalResolve = resolve; - const originalReject = reject; - - this.pendingCommand.resolve = (response: ISmtpResponse) => { - cleanup(); - this.pendingCommand = null; - logCommand(command, response, this.options); - originalResolve(response); - }; - - this.pendingCommand.reject = (error: Error) => { - cleanup(); - this.pendingCommand = null; - originalReject(error); - }; - }); - } - - /** - * Send raw data without command formatting - */ - public async sendRawData(connection: ISmtpConnection, data: string): Promise { - return new Promise((resolve, reject) => { - if (this.pendingCommand) { - reject(new Error('Another command is already pending')); - return; - } - - this.pendingCommand = { resolve, reject, command: 'DATA_CONTENT' }; - - // Set data timeout - const timeout = 60000; // 60 seconds for data - this.commandTimeout = setTimeout(() => { - this.pendingCommand = null; - this.commandTimeout = null; - reject(new Error('Data transmission timeout')); - }, timeout); - - // Set up data handler - const dataHandler = (chunk: Buffer) => { - this.handleIncomingData(chunk.toString()); - }; - - connection.socket.on('data', dataHandler); - - // Clean up function - const cleanup = () => { - connection.socket.removeListener('data', dataHandler); - if (this.commandTimeout) { - clearTimeout(this.commandTimeout); - this.commandTimeout = null; - } - }; - - // Override resolve/reject to include cleanup - const originalResolve = resolve; - const originalReject = reject; - - this.pendingCommand.resolve = (response: ISmtpResponse) => { - cleanup(); - this.pendingCommand = null; - originalResolve(response); - }; - - this.pendingCommand.reject = (error: Error) => { - cleanup(); - this.pendingCommand = null; - originalReject(error); - }; - - // Send data - connection.socket.write(data, (error) => { - if (error) { - cleanup(); - this.pendingCommand = null; - reject(error); - } - }); - }); - } - - /** - * Wait for server greeting - */ - public async waitForGreeting(connection: ISmtpConnection): Promise { - return new Promise((resolve, reject) => { - const timeout = 30000; // 30 seconds - let timeoutHandler: NodeJS.Timeout; - - const dataHandler = (data: Buffer) => { - this.responseBuffer += data.toString(); - - if (this.isCompleteResponse(this.responseBuffer)) { - clearTimeout(timeoutHandler); - connection.socket.removeListener('data', dataHandler); - - const response = parseSmtpResponse(this.responseBuffer); - this.responseBuffer = ''; - - if (isSuccessCode(response.code)) { - resolve(response); - } else { - reject(new Error(`Server greeting failed: ${response.message}`)); - } - } - }; - - timeoutHandler = setTimeout(() => { - connection.socket.removeListener('data', dataHandler); - reject(new Error('Greeting timeout')); - }, timeout); - - connection.socket.on('data', dataHandler); - }); - } - - private handleIncomingData(data: string): void { - if (!this.pendingCommand) { - return; - } - - this.responseBuffer += data; - - if (this.isCompleteResponse(this.responseBuffer)) { - const response = parseSmtpResponse(this.responseBuffer); - this.responseBuffer = ''; - - if (isSuccessCode(response.code) || (response.code >= 300 && response.code < 400) || response.code >= 400) { - this.pendingCommand.resolve(response); - } else { - this.pendingCommand.reject(new Error(`Command failed: ${response.message}`)); - } - } - } - - private isCompleteResponse(buffer: string): boolean { - // Check if we have a complete response - const lines = buffer.split(/\r?\n/); - - if (lines.length < 1) { - return false; - } - - // Check the last non-empty line - for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i].trim(); - if (line.length > 0) { - // Response is complete if line starts with "XXX " (space after code) - return /^\d{3} /.test(line); - } - } - - return false; - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/connection-manager.ts b/ts/mail/delivery/smtpclient/connection-manager.ts deleted file mode 100644 index 9b9dd59..0000000 --- a/ts/mail/delivery/smtpclient/connection-manager.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * SMTP Client Connection Manager - * Connection pooling and lifecycle management - */ - -import * as net from 'node:net'; -import * as tls from 'node:tls'; -import { EventEmitter } from 'node:events'; -import { DEFAULTS, CONNECTION_STATES } from './constants.js'; -import type { - ISmtpClientOptions, - ISmtpConnection, - IConnectionPoolStatus, - ConnectionState -} from './interfaces.js'; -import { logConnection, logDebug } from './utils/logging.js'; -import { generateConnectionId } from './utils/helpers.js'; - -export class ConnectionManager extends EventEmitter { - private options: ISmtpClientOptions; - private connections: Map = new Map(); - private pendingConnections: Set = new Set(); - private idleTimeout: NodeJS.Timeout | null = null; - - constructor(options: ISmtpClientOptions) { - super(); - this.options = options; - this.setupIdleCleanup(); - } - - /** - * Get or create a connection - */ - public async getConnection(): Promise { - // Try to reuse an idle connection if pooling is enabled - if (this.options.pool) { - const idleConnection = this.findIdleConnection(); - if (idleConnection) { - const connectionId = this.getConnectionId(idleConnection) || 'unknown'; - logDebug('Reusing idle connection', this.options, { connectionId }); - return idleConnection; - } - - // Check if we can create a new connection - if (this.getActiveConnectionCount() >= (this.options.maxConnections || DEFAULTS.MAX_CONNECTIONS)) { - throw new Error('Maximum number of connections reached'); - } - } - - return this.createConnection(); - } - - /** - * Create a new connection - */ - public async createConnection(): Promise { - const connectionId = generateConnectionId(); - - try { - this.pendingConnections.add(connectionId); - logConnection('connecting', this.options, { connectionId }); - - const socket = await this.establishSocket(); - const connection: ISmtpConnection = { - socket, - state: CONNECTION_STATES.CONNECTED as ConnectionState, - options: this.options, - secure: this.options.secure || false, - createdAt: new Date(), - lastActivity: new Date(), - messageCount: 0 - }; - - this.setupSocketHandlers(socket, connectionId); - this.connections.set(connectionId, connection); - this.pendingConnections.delete(connectionId); - - logConnection('connected', this.options, { connectionId }); - this.emit('connection', connection); - - return connection; - } catch (error) { - this.pendingConnections.delete(connectionId); - logConnection('error', this.options, { connectionId, error }); - throw error; - } - } - - /** - * Release a connection back to the pool or close it - */ - public releaseConnection(connection: ISmtpConnection): void { - const connectionId = this.getConnectionId(connection); - - if (!connectionId || !this.connections.has(connectionId)) { - return; - } - - if (this.options.pool && this.shouldReuseConnection(connection)) { - // Return to pool - connection.state = CONNECTION_STATES.READY as ConnectionState; - connection.lastActivity = new Date(); - logDebug('Connection returned to pool', this.options, { connectionId }); - } else { - // Close connection - this.closeConnection(connection); - } - } - - /** - * Close a specific connection - */ - public closeConnection(connection: ISmtpConnection): void { - const connectionId = this.getConnectionId(connection); - - if (connectionId) { - this.connections.delete(connectionId); - } - - connection.state = CONNECTION_STATES.CLOSING as ConnectionState; - - try { - if (!connection.socket.destroyed) { - connection.socket.destroy(); - } - } catch (error) { - logDebug('Error closing connection', this.options, { error }); - } - - logConnection('disconnected', this.options, { connectionId }); - this.emit('disconnect', connection); - } - - /** - * Close all connections - */ - public closeAllConnections(): void { - logDebug('Closing all connections', this.options); - - for (const connection of this.connections.values()) { - this.closeConnection(connection); - } - - this.connections.clear(); - this.pendingConnections.clear(); - - if (this.idleTimeout) { - clearInterval(this.idleTimeout); - this.idleTimeout = null; - } - } - - /** - * Get connection pool status - */ - public getPoolStatus(): IConnectionPoolStatus { - const total = this.connections.size; - const active = Array.from(this.connections.values()) - .filter(conn => conn.state === CONNECTION_STATES.BUSY).length; - const idle = total - active; - const pending = this.pendingConnections.size; - - return { total, active, idle, pending }; - } - - /** - * Update connection activity timestamp - */ - public updateActivity(connection: ISmtpConnection): void { - connection.lastActivity = new Date(); - } - - private async establishSocket(): Promise { - return new Promise((resolve, reject) => { - const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT; - let socket: net.Socket | tls.TLSSocket; - - if (this.options.secure) { - // Direct TLS connection - socket = tls.connect({ - host: this.options.host, - port: this.options.port, - ...this.options.tls - }); - } else { - // Plain connection - socket = new net.Socket(); - socket.connect(this.options.port, this.options.host); - } - - const timeoutHandler = setTimeout(() => { - socket.destroy(); - reject(new Error(`Connection timeout after ${timeout}ms`)); - }, timeout); - - // For TLS connections, we need to wait for 'secureConnect' instead of 'connect' - const successEvent = this.options.secure ? 'secureConnect' : 'connect'; - - socket.once(successEvent, () => { - clearTimeout(timeoutHandler); - resolve(socket); - }); - - socket.once('error', (error) => { - clearTimeout(timeoutHandler); - reject(error); - }); - }); - } - - private setupSocketHandlers(socket: net.Socket | tls.TLSSocket, connectionId: string): void { - const socketTimeout = this.options.socketTimeout || DEFAULTS.SOCKET_TIMEOUT; - - socket.setTimeout(socketTimeout); - - socket.on('timeout', () => { - logDebug('Socket timeout', this.options, { connectionId }); - socket.destroy(); - }); - - socket.on('error', (error) => { - logConnection('error', this.options, { connectionId, error }); - this.connections.delete(connectionId); - }); - - socket.on('close', () => { - this.connections.delete(connectionId); - logDebug('Socket closed', this.options, { connectionId }); - }); - } - - private findIdleConnection(): ISmtpConnection | null { - for (const connection of this.connections.values()) { - if (connection.state === CONNECTION_STATES.READY) { - return connection; - } - } - return null; - } - - private shouldReuseConnection(connection: ISmtpConnection): boolean { - const maxMessages = this.options.maxMessages || DEFAULTS.MAX_MESSAGES; - const maxAge = 300000; // 5 minutes - const age = Date.now() - connection.createdAt.getTime(); - - return connection.messageCount < maxMessages && - age < maxAge && - !connection.socket.destroyed; - } - - private getActiveConnectionCount(): number { - return this.connections.size + this.pendingConnections.size; - } - - private getConnectionId(connection: ISmtpConnection): string | null { - for (const [id, conn] of this.connections.entries()) { - if (conn === connection) { - return id; - } - } - return null; - } - - private setupIdleCleanup(): void { - if (!this.options.pool) { - return; - } - - const cleanupInterval = DEFAULTS.POOL_IDLE_TIMEOUT; - - this.idleTimeout = setInterval(() => { - const now = Date.now(); - const connectionsToClose: ISmtpConnection[] = []; - - for (const connection of this.connections.values()) { - const idleTime = now - connection.lastActivity.getTime(); - - if (connection.state === CONNECTION_STATES.READY && idleTime > cleanupInterval) { - connectionsToClose.push(connection); - } - } - - for (const connection of connectionsToClose) { - logDebug('Closing idle connection', this.options); - this.closeConnection(connection); - } - }, cleanupInterval); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/constants.ts b/ts/mail/delivery/smtpclient/constants.ts deleted file mode 100644 index 6c6cf36..0000000 --- a/ts/mail/delivery/smtpclient/constants.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * SMTP Client Constants and Error Codes - * All constants, error codes, and enums for SMTP client operations - */ - -/** - * SMTP response codes - */ -export const SMTP_CODES = { - // Positive completion replies - SERVICE_READY: 220, - SERVICE_CLOSING: 221, - AUTHENTICATION_SUCCESSFUL: 235, - REQUESTED_ACTION_OK: 250, - USER_NOT_LOCAL: 251, - CANNOT_VERIFY_USER: 252, - - // Positive intermediate replies - START_MAIL_INPUT: 354, - - // Transient negative completion replies - SERVICE_NOT_AVAILABLE: 421, - MAILBOX_BUSY: 450, - LOCAL_ERROR: 451, - INSUFFICIENT_STORAGE: 452, - UNABLE_TO_ACCOMMODATE: 455, - - // Permanent negative completion replies - SYNTAX_ERROR: 500, - SYNTAX_ERROR_PARAMETERS: 501, - COMMAND_NOT_IMPLEMENTED: 502, - BAD_SEQUENCE: 503, - PARAMETER_NOT_IMPLEMENTED: 504, - MAILBOX_UNAVAILABLE: 550, - USER_NOT_LOCAL_TRY_FORWARD: 551, - EXCEEDED_STORAGE: 552, - MAILBOX_NAME_NOT_ALLOWED: 553, - TRANSACTION_FAILED: 554 -} as const; - -/** - * SMTP command names - */ -export const SMTP_COMMANDS = { - HELO: 'HELO', - EHLO: 'EHLO', - MAIL_FROM: 'MAIL FROM', - RCPT_TO: 'RCPT TO', - DATA: 'DATA', - RSET: 'RSET', - NOOP: 'NOOP', - QUIT: 'QUIT', - STARTTLS: 'STARTTLS', - AUTH: 'AUTH' -} as const; - -/** - * Authentication methods - */ -export const AUTH_METHODS = { - PLAIN: 'PLAIN', - LOGIN: 'LOGIN', - OAUTH2: 'XOAUTH2', - CRAM_MD5: 'CRAM-MD5' -} as const; - -/** - * Common SMTP extensions - */ -export const SMTP_EXTENSIONS = { - PIPELINING: 'PIPELINING', - SIZE: 'SIZE', - STARTTLS: 'STARTTLS', - AUTH: 'AUTH', - EIGHT_BIT_MIME: '8BITMIME', - CHUNKING: 'CHUNKING', - ENHANCED_STATUS_CODES: 'ENHANCEDSTATUSCODES', - DSN: 'DSN' -} as const; - -/** - * Default configuration values - */ -export const DEFAULTS = { - CONNECTION_TIMEOUT: 60000, // 60 seconds - SOCKET_TIMEOUT: 300000, // 5 minutes - COMMAND_TIMEOUT: 30000, // 30 seconds - MAX_CONNECTIONS: 5, - MAX_MESSAGES: 100, - PORT_SMTP: 25, - PORT_SUBMISSION: 587, - PORT_SMTPS: 465, - RETRY_ATTEMPTS: 3, - RETRY_DELAY: 1000, - POOL_IDLE_TIMEOUT: 30000 // 30 seconds -} as const; - -/** - * Error types for classification - */ -export enum SmtpErrorType { - CONNECTION_ERROR = 'CONNECTION_ERROR', - AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR', - PROTOCOL_ERROR = 'PROTOCOL_ERROR', - TIMEOUT_ERROR = 'TIMEOUT_ERROR', - TLS_ERROR = 'TLS_ERROR', - SYNTAX_ERROR = 'SYNTAX_ERROR', - MAILBOX_ERROR = 'MAILBOX_ERROR', - QUOTA_ERROR = 'QUOTA_ERROR', - UNKNOWN_ERROR = 'UNKNOWN_ERROR' -} - -/** - * Regular expressions for parsing - */ -export const REGEX_PATTERNS = { - EMAIL_ADDRESS: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - RESPONSE_CODE: /^(\d{3})([ -])(.*)/, - ENHANCED_STATUS: /^(\d\.\d\.\d)\s/, - AUTH_CAPABILITIES: /AUTH\s+(.+)/i, - SIZE_EXTENSION: /SIZE\s+(\d+)/i -} as const; - -/** - * Line endings and separators - */ -export const LINE_ENDINGS = { - CRLF: '\r\n', - LF: '\n', - CR: '\r' -} as const; - -/** - * Connection states for internal use - */ -export const CONNECTION_STATES = { - DISCONNECTED: 'disconnected', - CONNECTING: 'connecting', - CONNECTED: 'connected', - AUTHENTICATED: 'authenticated', - READY: 'ready', - BUSY: 'busy', - CLOSING: 'closing', - ERROR: 'error' -} as const; \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/create-client.ts b/ts/mail/delivery/smtpclient/create-client.ts deleted file mode 100644 index da90943..0000000 --- a/ts/mail/delivery/smtpclient/create-client.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * SMTP Client Factory - * Factory function for client creation and dependency injection - */ - -import { SmtpClient } from './smtp-client.js'; -import { ConnectionManager } from './connection-manager.js'; -import { CommandHandler } from './command-handler.js'; -import { AuthHandler } from './auth-handler.js'; -import { TlsHandler } from './tls-handler.js'; -import { SmtpErrorHandler } from './error-handler.js'; -import type { ISmtpClientOptions } from './interfaces.js'; -import { validateClientOptions } from './utils/validation.js'; -import { DEFAULTS } from './constants.js'; - -/** - * Create a complete SMTP client with all components - */ -export function createSmtpClient(options: ISmtpClientOptions): SmtpClient { - // Validate options - const errors = validateClientOptions(options); - if (errors.length > 0) { - throw new Error(`Invalid client options: ${errors.join(', ')}`); - } - - // Apply defaults - const clientOptions: ISmtpClientOptions = { - connectionTimeout: DEFAULTS.CONNECTION_TIMEOUT, - socketTimeout: DEFAULTS.SOCKET_TIMEOUT, - maxConnections: DEFAULTS.MAX_CONNECTIONS, - maxMessages: DEFAULTS.MAX_MESSAGES, - pool: false, - secure: false, - debug: false, - ...options - }; - - // Create handlers - const errorHandler = new SmtpErrorHandler(clientOptions); - const connectionManager = new ConnectionManager(clientOptions); - const commandHandler = new CommandHandler(clientOptions); - const authHandler = new AuthHandler(clientOptions, commandHandler); - const tlsHandler = new TlsHandler(clientOptions, commandHandler); - - // Create and return SMTP client - return new SmtpClient({ - options: clientOptions, - connectionManager, - commandHandler, - authHandler, - tlsHandler, - errorHandler - }); -} - -/** - * Create SMTP client with connection pooling enabled - */ -export function createPooledSmtpClient(options: ISmtpClientOptions): SmtpClient { - return createSmtpClient({ - ...options, - pool: true, - maxConnections: options.maxConnections || DEFAULTS.MAX_CONNECTIONS, - maxMessages: options.maxMessages || DEFAULTS.MAX_MESSAGES - }); -} - -/** - * Create SMTP client for high-volume sending - */ -export function createBulkSmtpClient(options: ISmtpClientOptions): SmtpClient { - return createSmtpClient({ - ...options, - pool: true, - maxConnections: Math.max(options.maxConnections || 10, 10), - maxMessages: Math.max(options.maxMessages || 1000, 1000), - connectionTimeout: options.connectionTimeout || 30000, - socketTimeout: options.socketTimeout || 120000 - }); -} - -/** - * Create SMTP client for transactional emails - */ -export function createTransactionalSmtpClient(options: ISmtpClientOptions): SmtpClient { - return createSmtpClient({ - ...options, - pool: false, // Use fresh connections for transactional emails - maxConnections: 1, - maxMessages: 1, - connectionTimeout: options.connectionTimeout || 10000, - socketTimeout: options.socketTimeout || 30000 - }); -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/error-handler.ts b/ts/mail/delivery/smtpclient/error-handler.ts deleted file mode 100644 index 8510bc4..0000000 --- a/ts/mail/delivery/smtpclient/error-handler.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * SMTP Client Error Handler - * Error classification and recovery strategies - */ - -import { SmtpErrorType } from './constants.js'; -import type { ISmtpResponse, ISmtpErrorContext, ISmtpClientOptions } from './interfaces.js'; -import { logDebug } from './utils/logging.js'; - -export class SmtpErrorHandler { - private options: ISmtpClientOptions; - - constructor(options: ISmtpClientOptions) { - this.options = options; - } - - /** - * Classify error type based on response or error - */ - public classifyError(error: Error | ISmtpResponse, context?: ISmtpErrorContext): SmtpErrorType { - logDebug('Classifying error', this.options, { errorMessage: error instanceof Error ? error.message : String(error), context }); - - // Handle Error objects - if (error instanceof Error) { - return this.classifyErrorByMessage(error); - } - - // Handle SMTP response codes - if (typeof error === 'object' && 'code' in error) { - return this.classifyErrorByCode(error.code); - } - - return SmtpErrorType.UNKNOWN_ERROR; - } - - /** - * Determine if error is retryable - */ - public isRetryable(errorType: SmtpErrorType, response?: ISmtpResponse): boolean { - switch (errorType) { - case SmtpErrorType.CONNECTION_ERROR: - case SmtpErrorType.TIMEOUT_ERROR: - return true; - - case SmtpErrorType.PROTOCOL_ERROR: - // Only retry on temporary failures (4xx codes) - return response ? response.code >= 400 && response.code < 500 : false; - - case SmtpErrorType.AUTHENTICATION_ERROR: - case SmtpErrorType.TLS_ERROR: - case SmtpErrorType.SYNTAX_ERROR: - case SmtpErrorType.MAILBOX_ERROR: - case SmtpErrorType.QUOTA_ERROR: - return false; - - default: - return false; - } - } - - /** - * Get retry delay for error type - */ - public getRetryDelay(attempt: number, errorType: SmtpErrorType): number { - const baseDelay = 1000; // 1 second - const maxDelay = 30000; // 30 seconds - - // Exponential backoff with jitter - const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); - const jitter = Math.random() * 0.1 * delay; // 10% jitter - - return Math.floor(delay + jitter); - } - - /** - * Create enhanced error with context - */ - public createError( - message: string, - errorType: SmtpErrorType, - context?: ISmtpErrorContext, - originalError?: Error - ): Error { - const error = new Error(message); - (error as any).type = errorType; - (error as any).context = context; - (error as any).originalError = originalError; - - return error; - } - - private classifyErrorByMessage(error: Error): SmtpErrorType { - const message = error.message.toLowerCase(); - - if (message.includes('timeout') || message.includes('etimedout')) { - return SmtpErrorType.TIMEOUT_ERROR; - } - - if (message.includes('connect') || message.includes('econnrefused') || - message.includes('enotfound') || message.includes('enetunreach')) { - return SmtpErrorType.CONNECTION_ERROR; - } - - if (message.includes('tls') || message.includes('ssl') || - message.includes('certificate') || message.includes('handshake')) { - return SmtpErrorType.TLS_ERROR; - } - - if (message.includes('auth')) { - return SmtpErrorType.AUTHENTICATION_ERROR; - } - - return SmtpErrorType.UNKNOWN_ERROR; - } - - private classifyErrorByCode(code: number): SmtpErrorType { - if (code >= 500) { - // Permanent failures - if (code === 550 || code === 551 || code === 553) { - return SmtpErrorType.MAILBOX_ERROR; - } - if (code === 552) { - return SmtpErrorType.QUOTA_ERROR; - } - if (code === 500 || code === 501 || code === 502 || code === 504) { - return SmtpErrorType.SYNTAX_ERROR; - } - return SmtpErrorType.PROTOCOL_ERROR; - } - - if (code >= 400) { - // Temporary failures - if (code === 450 || code === 451 || code === 452) { - return SmtpErrorType.QUOTA_ERROR; - } - return SmtpErrorType.PROTOCOL_ERROR; - } - - return SmtpErrorType.UNKNOWN_ERROR; - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/index.ts b/ts/mail/delivery/smtpclient/index.ts deleted file mode 100644 index 6cb3255..0000000 --- a/ts/mail/delivery/smtpclient/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * SMTP Client Module Exports - * Modular SMTP client implementation for robust email delivery - */ - -// Main client class and factory -export * from './smtp-client.js'; -export * from './create-client.js'; - -// Core handlers -export * from './connection-manager.js'; -export * from './command-handler.js'; -export * from './auth-handler.js'; -export * from './tls-handler.js'; -export * from './error-handler.js'; - -// Interfaces and types -export * from './interfaces.js'; -export * from './constants.js'; - -// Utilities -export * from './utils/validation.js'; -export * from './utils/logging.js'; -export * from './utils/helpers.js'; \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/interfaces.ts b/ts/mail/delivery/smtpclient/interfaces.ts deleted file mode 100644 index 978eceb..0000000 --- a/ts/mail/delivery/smtpclient/interfaces.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * SMTP Client Interfaces and Types - * All interface definitions for the modular SMTP client - */ - -import type * as tls from 'node:tls'; -import type * as net from 'node:net'; -import type { Email } from '../../core/classes.email.js'; - -/** - * SMTP client connection options - */ -export interface ISmtpClientOptions { - /** Hostname of the SMTP server */ - host: string; - - /** Port to connect to */ - port: number; - - /** Whether to use TLS for the connection */ - secure?: boolean; - - /** Connection timeout in milliseconds */ - connectionTimeout?: number; - - /** Socket timeout in milliseconds */ - socketTimeout?: number; - - /** Domain name for EHLO command */ - domain?: string; - - /** Authentication options */ - auth?: ISmtpAuthOptions; - - /** TLS options */ - tls?: tls.ConnectionOptions; - - /** Maximum number of connections in pool */ - pool?: boolean; - maxConnections?: number; - maxMessages?: number; - - /** Enable debug logging */ - debug?: boolean; - - /** Proxy settings */ - proxy?: string; -} - -/** - * Authentication options for SMTP - */ -export interface ISmtpAuthOptions { - /** Username */ - user?: string; - - /** Password */ - pass?: string; - - /** OAuth2 settings */ - oauth2?: IOAuth2Options; - - /** Authentication method preference */ - method?: 'PLAIN' | 'LOGIN' | 'OAUTH2' | 'AUTO'; -} - -/** - * OAuth2 authentication options - */ -export interface IOAuth2Options { - /** OAuth2 user identifier */ - user: string; - - /** OAuth2 client ID */ - clientId: string; - - /** OAuth2 client secret */ - clientSecret: string; - - /** OAuth2 refresh token */ - refreshToken: string; - - /** OAuth2 access token */ - accessToken?: string; - - /** Token expiry time */ - expires?: number; -} - -/** - * Result of an email send operation - */ -export interface ISmtpSendResult { - /** Whether the send was successful */ - success: boolean; - - /** Message ID from server */ - messageId?: string; - - /** List of accepted recipients */ - acceptedRecipients: string[]; - - /** List of rejected recipients */ - rejectedRecipients: string[]; - - /** Error information if failed */ - error?: Error; - - /** Server response */ - response?: string; - - /** Envelope information */ - envelope?: ISmtpEnvelope; -} - -/** - * SMTP envelope information - */ -export interface ISmtpEnvelope { - /** Sender address */ - from: string; - - /** Recipient addresses */ - to: string[]; -} - -/** - * Connection pool status - */ -export interface IConnectionPoolStatus { - /** Total connections in pool */ - total: number; - - /** Active connections */ - active: number; - - /** Idle connections */ - idle: number; - - /** Pending connection requests */ - pending: number; -} - -/** - * SMTP command response - */ -export interface ISmtpResponse { - /** Response code */ - code: number; - - /** Response message */ - message: string; - - /** Enhanced status code */ - enhancedCode?: string; - - /** Raw response */ - raw: string; -} - -/** - * Connection state - */ -export enum ConnectionState { - DISCONNECTED = 'disconnected', - CONNECTING = 'connecting', - CONNECTED = 'connected', - AUTHENTICATED = 'authenticated', - READY = 'ready', - BUSY = 'busy', - CLOSING = 'closing', - ERROR = 'error' -} - -/** - * SMTP capabilities - */ -export interface ISmtpCapabilities { - /** Supported extensions */ - extensions: Set; - - /** Maximum message size */ - maxSize?: number; - - /** Supported authentication methods */ - authMethods: Set; - - /** Support for pipelining */ - pipelining: boolean; - - /** Support for STARTTLS */ - starttls: boolean; - - /** Support for 8BITMIME */ - eightBitMime: boolean; -} - -/** - * Internal connection interface - */ -export interface ISmtpConnection { - /** Socket connection */ - socket: net.Socket | tls.TLSSocket; - - /** Connection state */ - state: ConnectionState; - - /** Server capabilities */ - capabilities?: ISmtpCapabilities; - - /** Connection options */ - options: ISmtpClientOptions; - - /** Whether connection is secure */ - secure: boolean; - - /** Connection creation time */ - createdAt: Date; - - /** Last activity time */ - lastActivity: Date; - - /** Number of messages sent */ - messageCount: number; -} - -/** - * Error context for detailed error reporting - */ -export interface ISmtpErrorContext { - /** Command that caused the error */ - command?: string; - - /** Server response */ - response?: ISmtpResponse; - - /** Connection state */ - connectionState?: ConnectionState; - - /** Additional context data */ - data?: Record; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/smtp-client.ts b/ts/mail/delivery/smtpclient/smtp-client.ts deleted file mode 100644 index e2d20e1..0000000 --- a/ts/mail/delivery/smtpclient/smtp-client.ts +++ /dev/null @@ -1,357 +0,0 @@ -/** - * SMTP Client Core Implementation - * Main client class with delegation to handlers - */ - -import { EventEmitter } from 'node:events'; -import type { Email } from '../../core/classes.email.js'; -import type { - ISmtpClientOptions, - ISmtpSendResult, - ISmtpConnection, - IConnectionPoolStatus, - ConnectionState -} from './interfaces.js'; -import { CONNECTION_STATES, SmtpErrorType } from './constants.js'; -import type { ConnectionManager } from './connection-manager.js'; -import type { CommandHandler } from './command-handler.js'; -import type { AuthHandler } from './auth-handler.js'; -import type { TlsHandler } from './tls-handler.js'; -import type { SmtpErrorHandler } from './error-handler.js'; -import { validateSender, validateRecipients } from './utils/validation.js'; -import { logEmailSend, logPerformance, logDebug } from './utils/logging.js'; - -interface ISmtpClientDependencies { - options: ISmtpClientOptions; - connectionManager: ConnectionManager; - commandHandler: CommandHandler; - authHandler: AuthHandler; - tlsHandler: TlsHandler; - errorHandler: SmtpErrorHandler; -} - -export class SmtpClient extends EventEmitter { - private options: ISmtpClientOptions; - private connectionManager: ConnectionManager; - private commandHandler: CommandHandler; - private authHandler: AuthHandler; - private tlsHandler: TlsHandler; - private errorHandler: SmtpErrorHandler; - private isShuttingDown: boolean = false; - - constructor(dependencies: ISmtpClientDependencies) { - super(); - - this.options = dependencies.options; - this.connectionManager = dependencies.connectionManager; - this.commandHandler = dependencies.commandHandler; - this.authHandler = dependencies.authHandler; - this.tlsHandler = dependencies.tlsHandler; - this.errorHandler = dependencies.errorHandler; - - this.setupEventForwarding(); - } - - /** - * Send an email - */ - public async sendMail(email: Email): Promise { - const startTime = Date.now(); - - // Extract clean email addresses without display names for SMTP operations - const fromAddress = email.getFromAddress(); - const recipients = email.getToAddresses(); - const ccRecipients = email.getCcAddresses(); - const bccRecipients = email.getBccAddresses(); - - // Combine all recipients for SMTP operations - const allRecipients = [...recipients, ...ccRecipients, ...bccRecipients]; - - // Validate email addresses - if (!validateSender(fromAddress)) { - throw new Error(`Invalid sender address: ${fromAddress}`); - } - - const recipientErrors = validateRecipients(allRecipients); - if (recipientErrors.length > 0) { - throw new Error(`Invalid recipients: ${recipientErrors.join(', ')}`); - } - - logEmailSend('start', allRecipients, this.options); - - let connection: ISmtpConnection | null = null; - const result: ISmtpSendResult = { - success: false, - acceptedRecipients: [], - rejectedRecipients: [], - envelope: { - from: fromAddress, - to: allRecipients - } - }; - - try { - // Get connection - connection = await this.connectionManager.getConnection(); - connection.state = CONNECTION_STATES.BUSY as ConnectionState; - - // Wait for greeting if new connection - if (!connection.capabilities) { - await this.commandHandler.waitForGreeting(connection); - } - - // Perform EHLO - await this.commandHandler.sendEhlo(connection, this.options.domain); - - // Upgrade to TLS if needed - if (this.tlsHandler.shouldUseTLS(connection)) { - await this.tlsHandler.upgradeToTLS(connection); - // Re-send EHLO after TLS upgrade - await this.commandHandler.sendEhlo(connection, this.options.domain); - } - - // Authenticate if needed - if (this.options.auth) { - await this.authHandler.authenticate(connection); - } - - // Send MAIL FROM - const mailFromResponse = await this.commandHandler.sendMailFrom(connection, fromAddress); - if (mailFromResponse.code >= 400) { - throw new Error(`MAIL FROM failed: ${mailFromResponse.message}`); - } - - // Send RCPT TO for each recipient (includes TO, CC, and BCC) - for (const recipient of allRecipients) { - try { - const rcptResponse = await this.commandHandler.sendRcptTo(connection, recipient); - if (rcptResponse.code >= 400) { - result.rejectedRecipients.push(recipient); - logDebug(`Recipient rejected: ${recipient}`, this.options, { response: rcptResponse }); - } else { - result.acceptedRecipients.push(recipient); - } - } catch (error) { - result.rejectedRecipients.push(recipient); - logDebug(`Recipient error: ${recipient}`, this.options, { error }); - } - } - - // Check if we have any accepted recipients - if (result.acceptedRecipients.length === 0) { - throw new Error('All recipients were rejected'); - } - - // Send DATA command - const dataResponse = await this.commandHandler.sendData(connection); - if (dataResponse.code !== 354) { - throw new Error(`DATA command failed: ${dataResponse.message}`); - } - - // Send email content - const emailData = await this.formatEmailData(email); - const sendResponse = await this.commandHandler.sendDataContent(connection, emailData); - - if (sendResponse.code >= 400) { - throw new Error(`Email data rejected: ${sendResponse.message}`); - } - - // Success - result.success = true; - result.messageId = this.extractMessageId(sendResponse.message); - result.response = sendResponse.message; - - connection.messageCount++; - logEmailSend('success', recipients, this.options, { - messageId: result.messageId, - duration: Date.now() - startTime - }); - - } catch (error) { - result.success = false; - result.error = error instanceof Error ? error : new Error(String(error)); - - // Classify error and determine if we should retry - const errorType = this.errorHandler.classifyError(result.error); - result.error = this.errorHandler.createError( - result.error.message, - errorType, - { command: 'SEND_MAIL' }, - result.error - ); - - logEmailSend('failure', recipients, this.options, { - error: result.error, - duration: Date.now() - startTime - }); - - } finally { - // Release connection - if (connection) { - connection.state = CONNECTION_STATES.READY as ConnectionState; - this.connectionManager.updateActivity(connection); - this.connectionManager.releaseConnection(connection); - } - - logPerformance('sendMail', Date.now() - startTime, this.options); - } - - return result; - } - - /** - * Test connection to SMTP server - */ - public async verify(): Promise { - let connection: ISmtpConnection | null = null; - - try { - connection = await this.connectionManager.createConnection(); - await this.commandHandler.waitForGreeting(connection); - await this.commandHandler.sendEhlo(connection, this.options.domain); - - if (this.tlsHandler.shouldUseTLS(connection)) { - await this.tlsHandler.upgradeToTLS(connection); - await this.commandHandler.sendEhlo(connection, this.options.domain); - } - - if (this.options.auth) { - await this.authHandler.authenticate(connection); - } - - await this.commandHandler.sendQuit(connection); - return true; - - } catch (error) { - logDebug('Connection verification failed', this.options, { error }); - return false; - - } finally { - if (connection) { - this.connectionManager.closeConnection(connection); - } - } - } - - /** - * Check if client is connected - */ - public isConnected(): boolean { - const status = this.connectionManager.getPoolStatus(); - return status.total > 0; - } - - /** - * Get connection pool status - */ - public getPoolStatus(): IConnectionPoolStatus { - return this.connectionManager.getPoolStatus(); - } - - /** - * Update client options - */ - public updateOptions(newOptions: Partial): void { - this.options = { ...this.options, ...newOptions }; - logDebug('Client options updated', this.options); - } - - /** - * Close all connections and shutdown client - */ - public async close(): Promise { - if (this.isShuttingDown) { - return; - } - - this.isShuttingDown = true; - logDebug('Shutting down SMTP client', this.options); - - try { - this.connectionManager.closeAllConnections(); - this.emit('close'); - } catch (error) { - logDebug('Error during client shutdown', this.options, { error }); - } - } - - private async formatEmailData(email: Email): Promise { - // Convert Email object to raw SMTP data - const headers: string[] = []; - - // Required headers - headers.push(`From: ${email.from}`); - headers.push(`To: ${Array.isArray(email.to) ? email.to.join(', ') : email.to}`); - headers.push(`Subject: ${email.subject || ''}`); - headers.push(`Date: ${new Date().toUTCString()}`); - headers.push(`Message-ID: <${Date.now()}.${Math.random().toString(36)}@${this.options.host}>`); - - // Optional headers - if (email.cc) { - const cc = Array.isArray(email.cc) ? email.cc.join(', ') : email.cc; - headers.push(`Cc: ${cc}`); - } - - if (email.bcc) { - const bcc = Array.isArray(email.bcc) ? email.bcc.join(', ') : email.bcc; - headers.push(`Bcc: ${bcc}`); - } - - // Content headers - if (email.html && email.text) { - // Multipart message - const boundary = `boundary_${Date.now()}_${Math.random().toString(36)}`; - headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`); - headers.push('MIME-Version: 1.0'); - - const body = [ - `--${boundary}`, - 'Content-Type: text/plain; charset=utf-8', - 'Content-Transfer-Encoding: quoted-printable', - '', - email.text, - '', - `--${boundary}`, - 'Content-Type: text/html; charset=utf-8', - 'Content-Transfer-Encoding: quoted-printable', - '', - email.html, - '', - `--${boundary}--` - ].join('\r\n'); - - return headers.join('\r\n') + '\r\n\r\n' + body; - } else if (email.html) { - headers.push('Content-Type: text/html; charset=utf-8'); - headers.push('MIME-Version: 1.0'); - return headers.join('\r\n') + '\r\n\r\n' + email.html; - } else { - headers.push('Content-Type: text/plain; charset=utf-8'); - headers.push('MIME-Version: 1.0'); - return headers.join('\r\n') + '\r\n\r\n' + (email.text || ''); - } - } - - private extractMessageId(response: string): string | undefined { - // Try to extract message ID from server response - const match = response.match(/queued as ([^\s]+)/i) || - response.match(/id=([^\s]+)/i) || - response.match(/Message-ID: <([^>]+)>/i); - return match ? match[1] : undefined; - } - - private setupEventForwarding(): void { - // Forward events from connection manager - this.connectionManager.on('connection', (connection) => { - this.emit('connection', connection); - }); - - this.connectionManager.on('disconnect', (connection) => { - this.emit('disconnect', connection); - }); - - this.connectionManager.on('error', (error) => { - this.emit('error', error); - }); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/tls-handler.ts b/ts/mail/delivery/smtpclient/tls-handler.ts deleted file mode 100644 index 4aecfe0..0000000 --- a/ts/mail/delivery/smtpclient/tls-handler.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * SMTP Client TLS Handler - * TLS and STARTTLS client functionality - */ - -import * as tls from 'node:tls'; -import * as net from 'node:net'; -import { DEFAULTS } from './constants.js'; -import type { - ISmtpConnection, - ISmtpClientOptions, - ConnectionState -} from './interfaces.js'; -import { CONNECTION_STATES } from './constants.js'; -import { logTLS, logDebug } from './utils/logging.js'; -import { isSuccessCode } from './utils/helpers.js'; -import type { CommandHandler } from './command-handler.js'; - -export class TlsHandler { - private options: ISmtpClientOptions; - private commandHandler: CommandHandler; - - constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) { - this.options = options; - this.commandHandler = commandHandler; - } - - /** - * Upgrade connection to TLS using STARTTLS - */ - public async upgradeToTLS(connection: ISmtpConnection): Promise { - if (connection.secure) { - logDebug('Connection already secure', this.options); - return; - } - - // Check if STARTTLS is supported - if (!connection.capabilities?.starttls) { - throw new Error('Server does not support STARTTLS'); - } - - logTLS('starttls_start', this.options); - - try { - // Send STARTTLS command - const response = await this.commandHandler.sendStartTls(connection); - - if (!isSuccessCode(response.code)) { - throw new Error(`STARTTLS command failed: ${response.message}`); - } - - // Upgrade the socket to TLS - await this.performTLSUpgrade(connection); - - // Clear capabilities as they may have changed after TLS - connection.capabilities = undefined; - connection.secure = true; - - logTLS('starttls_success', this.options); - - } catch (error) { - logTLS('starttls_failure', this.options, { error }); - throw error; - } - } - - /** - * Create a direct TLS connection - */ - public async createTLSConnection(host: string, port: number): Promise { - return new Promise((resolve, reject) => { - const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT; - - const tlsOptions: tls.ConnectionOptions = { - host, - port, - ...this.options.tls, - // Default TLS options for email - secureProtocol: 'TLS_method', - ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA', - rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false - }; - - logTLS('tls_connected', this.options, { host, port }); - - const socket = tls.connect(tlsOptions); - - const timeoutHandler = setTimeout(() => { - socket.destroy(); - reject(new Error(`TLS connection timeout after ${timeout}ms`)); - }, timeout); - - socket.once('secureConnect', () => { - clearTimeout(timeoutHandler); - - if (!socket.authorized && this.options.tls?.rejectUnauthorized !== false) { - socket.destroy(); - reject(new Error(`TLS certificate verification failed: ${socket.authorizationError}`)); - return; - } - - logDebug('TLS connection established', this.options, { - authorized: socket.authorized, - protocol: socket.getProtocol(), - cipher: socket.getCipher() - }); - - resolve(socket); - }); - - socket.once('error', (error) => { - clearTimeout(timeoutHandler); - reject(error); - }); - }); - } - - /** - * Validate TLS certificate - */ - public validateCertificate(socket: tls.TLSSocket): boolean { - if (!socket.authorized) { - logDebug('TLS certificate not authorized', this.options, { - error: socket.authorizationError - }); - - // Allow self-signed certificates if explicitly configured - if (this.options.tls?.rejectUnauthorized === false) { - logDebug('Accepting unauthorized certificate (rejectUnauthorized: false)', this.options); - return true; - } - - return false; - } - - const cert = socket.getPeerCertificate(); - if (!cert) { - logDebug('No peer certificate available', this.options); - return false; - } - - // Additional certificate validation - const now = new Date(); - if (cert.valid_from && new Date(cert.valid_from) > now) { - logDebug('Certificate not yet valid', this.options, { validFrom: cert.valid_from }); - return false; - } - - if (cert.valid_to && new Date(cert.valid_to) < now) { - logDebug('Certificate expired', this.options, { validTo: cert.valid_to }); - return false; - } - - logDebug('TLS certificate validated', this.options, { - subject: cert.subject, - issuer: cert.issuer, - validFrom: cert.valid_from, - validTo: cert.valid_to - }); - - return true; - } - - /** - * Get TLS connection information - */ - public getTLSInfo(socket: tls.TLSSocket): any { - if (!(socket instanceof tls.TLSSocket)) { - return null; - } - - return { - authorized: socket.authorized, - authorizationError: socket.authorizationError, - protocol: socket.getProtocol(), - cipher: socket.getCipher(), - peerCertificate: socket.getPeerCertificate(), - alpnProtocol: socket.alpnProtocol - }; - } - - /** - * Check if TLS upgrade is required or recommended - */ - public shouldUseTLS(connection: ISmtpConnection): boolean { - // Already secure - if (connection.secure) { - return false; - } - - // Direct TLS connection configured - if (this.options.secure) { - return false; // Already handled in connection establishment - } - - // STARTTLS available and not explicitly disabled - if (connection.capabilities?.starttls) { - return this.options.tls !== null && this.options.tls !== undefined; // Use TLS if configured - } - - return false; - } - - private async performTLSUpgrade(connection: ISmtpConnection): Promise { - return new Promise((resolve, reject) => { - const plainSocket = connection.socket as net.Socket; - const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT; - - const tlsOptions: tls.ConnectionOptions = { - socket: plainSocket, - host: this.options.host, - ...this.options.tls, - // Default TLS options for STARTTLS - secureProtocol: 'TLS_method', - ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA', - rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false - }; - - const timeoutHandler = setTimeout(() => { - reject(new Error(`TLS upgrade timeout after ${timeout}ms`)); - }, timeout); - - // Create TLS socket from existing connection - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.once('secureConnect', () => { - clearTimeout(timeoutHandler); - - // Validate certificate if required - if (!this.validateCertificate(tlsSocket)) { - tlsSocket.destroy(); - reject(new Error('TLS certificate validation failed')); - return; - } - - // Replace the socket in the connection - connection.socket = tlsSocket; - connection.secure = true; - - logDebug('STARTTLS upgrade completed', this.options, { - protocol: tlsSocket.getProtocol(), - cipher: tlsSocket.getCipher() - }); - - resolve(); - }); - - tlsSocket.once('error', (error) => { - clearTimeout(timeoutHandler); - reject(error); - }); - }); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/utils/helpers.ts b/ts/mail/delivery/smtpclient/utils/helpers.ts deleted file mode 100644 index e534f9b..0000000 --- a/ts/mail/delivery/smtpclient/utils/helpers.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * SMTP Client Helper Functions - * Protocol helper functions and utilities - */ - -import { SMTP_CODES, REGEX_PATTERNS, LINE_ENDINGS } from '../constants.js'; -import type { ISmtpResponse, ISmtpCapabilities } from '../interfaces.js'; - -/** - * Parse SMTP server response - */ -export function parseSmtpResponse(data: string): ISmtpResponse { - const lines = data.trim().split(/\r?\n/); - const firstLine = lines[0]; - const match = firstLine.match(REGEX_PATTERNS.RESPONSE_CODE); - - if (!match) { - return { - code: 500, - message: 'Invalid server response', - raw: data - }; - } - - const code = parseInt(match[1], 10); - const separator = match[2]; - const message = lines.map(line => line.substring(4)).join(' '); - - // Check for enhanced status code - const enhancedMatch = message.match(REGEX_PATTERNS.ENHANCED_STATUS); - const enhancedCode = enhancedMatch ? enhancedMatch[1] : undefined; - - return { - code, - message: enhancedCode ? message.substring(enhancedCode.length + 1) : message, - enhancedCode, - raw: data - }; -} - -/** - * Parse EHLO response and extract capabilities - */ -export function parseEhloResponse(response: string): ISmtpCapabilities { - const lines = response.trim().split(/\r?\n/); - const capabilities: ISmtpCapabilities = { - extensions: new Set(), - authMethods: new Set(), - pipelining: false, - starttls: false, - eightBitMime: false - }; - - for (const line of lines.slice(1)) { // Skip first line (greeting) - const extensionLine = line.substring(4); // Remove "250-" or "250 " - const parts = extensionLine.split(/\s+/); - const extension = parts[0].toUpperCase(); - - capabilities.extensions.add(extension); - - switch (extension) { - case 'PIPELINING': - capabilities.pipelining = true; - break; - case 'STARTTLS': - capabilities.starttls = true; - break; - case '8BITMIME': - capabilities.eightBitMime = true; - break; - case 'SIZE': - if (parts[1]) { - capabilities.maxSize = parseInt(parts[1], 10); - } - break; - case 'AUTH': - // Parse authentication methods - for (let i = 1; i < parts.length; i++) { - capabilities.authMethods.add(parts[i].toUpperCase()); - } - break; - } - } - - return capabilities; -} - -/** - * Format SMTP command with proper line ending - */ -export function formatCommand(command: string, ...args: string[]): string { - const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command; - return fullCommand + LINE_ENDINGS.CRLF; -} - -/** - * Encode authentication string for AUTH PLAIN - */ -export function encodeAuthPlain(username: string, password: string): string { - const authString = `\0${username}\0${password}`; - return Buffer.from(authString, 'utf8').toString('base64'); -} - -/** - * Encode authentication string for AUTH LOGIN - */ -export function encodeAuthLogin(value: string): string { - return Buffer.from(value, 'utf8').toString('base64'); -} - -/** - * Generate OAuth2 authentication string - */ -export function generateOAuth2String(username: string, accessToken: string): string { - const authString = `user=${username}\x01auth=Bearer ${accessToken}\x01\x01`; - return Buffer.from(authString, 'utf8').toString('base64'); -} - -/** - * Check if response code indicates success - */ -export function isSuccessCode(code: number): boolean { - return code >= 200 && code < 300; -} - -/** - * Check if response code indicates temporary failure - */ -export function isTemporaryFailure(code: number): boolean { - return code >= 400 && code < 500; -} - -/** - * Check if response code indicates permanent failure - */ -export function isPermanentFailure(code: number): boolean { - return code >= 500; -} - -/** - * Escape email address for SMTP commands - */ -export function escapeEmailAddress(email: string): string { - return `<${email.trim()}>`; -} - -/** - * Extract email address from angle brackets - */ -export function extractEmailAddress(email: string): string { - const match = email.match(/^<(.+)>$/); - return match ? match[1] : email.trim(); -} - -/** - * Generate unique connection ID - */ -export function generateConnectionId(): string { - return `smtp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; -} - -/** - * Format timeout duration for human readability - */ -export function formatTimeout(milliseconds: number): string { - if (milliseconds < 1000) { - return `${milliseconds}ms`; - } else if (milliseconds < 60000) { - return `${Math.round(milliseconds / 1000)}s`; - } else { - return `${Math.round(milliseconds / 60000)}m`; - } -} - -/** - * Validate and normalize email data size - */ -export function validateEmailSize(emailData: string, maxSize?: number): boolean { - const size = Buffer.byteLength(emailData, 'utf8'); - return !maxSize || size <= maxSize; -} - -/** - * Clean sensitive data from logs - */ -export function sanitizeForLogging(data: any): any { - if (typeof data !== 'object' || data === null) { - return data; - } - - const sanitized = { ...data }; - const sensitiveFields = ['password', 'pass', 'accessToken', 'refreshToken', 'clientSecret']; - - for (const field of sensitiveFields) { - if (field in sanitized) { - sanitized[field] = '[REDACTED]'; - } - } - - return sanitized; -} - -/** - * Calculate exponential backoff delay - */ -export function calculateBackoffDelay(attempt: number, baseDelay: number = 1000): number { - return Math.min(baseDelay * Math.pow(2, attempt - 1), 30000); // Max 30 seconds -} - -/** - * Parse enhanced status code - */ -export function parseEnhancedStatusCode(code: string): { class: number; subject: number; detail: number } | null { - const match = code.match(/^(\d)\.(\d)\.(\d)$/); - if (!match) { - return null; - } - - return { - class: parseInt(match[1], 10), - subject: parseInt(match[2], 10), - detail: parseInt(match[3], 10) - }; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/utils/logging.ts b/ts/mail/delivery/smtpclient/utils/logging.ts deleted file mode 100644 index d0bb2f1..0000000 --- a/ts/mail/delivery/smtpclient/utils/logging.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * SMTP Client Logging Utilities - * Client-side logging utilities for SMTP operations - */ - -import { logger } from '../../../../logger.js'; -import type { ISmtpResponse, ISmtpClientOptions } from '../interfaces.js'; - -export interface ISmtpClientLogData { - component: string; - host?: string; - port?: number; - secure?: boolean; - command?: string; - response?: ISmtpResponse; - error?: Error; - connectionId?: string; - messageId?: string; - duration?: number; - [key: string]: any; -} - -/** - * Log SMTP client connection events - */ -export function logConnection( - event: 'connecting' | 'connected' | 'disconnected' | 'error', - options: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - event, - host: options.host, - port: options.port, - secure: options.secure, - ...data - }; - - switch (event) { - case 'connecting': - logger.info('SMTP client connecting', logData); - break; - case 'connected': - logger.info('SMTP client connected', logData); - break; - case 'disconnected': - logger.info('SMTP client disconnected', logData); - break; - case 'error': - logger.error('SMTP client connection error', logData); - break; - } -} - -/** - * Log SMTP command execution - */ -export function logCommand( - command: string, - response?: ISmtpResponse, - options?: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - command, - response, - host: options?.host, - port: options?.port, - ...data - }; - - if (response && response.code >= 400) { - logger.warn('SMTP command failed', logData); - } else { - logger.debug('SMTP command executed', logData); - } -} - -/** - * Log authentication events - */ -export function logAuthentication( - event: 'start' | 'success' | 'failure', - method: string, - options: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - event: `auth_${event}`, - authMethod: method, - host: options.host, - port: options.port, - ...data - }; - - switch (event) { - case 'start': - logger.debug('SMTP authentication started', logData); - break; - case 'success': - logger.info('SMTP authentication successful', logData); - break; - case 'failure': - logger.error('SMTP authentication failed', logData); - break; - } -} - -/** - * Log TLS/STARTTLS events - */ -export function logTLS( - event: 'starttls_start' | 'starttls_success' | 'starttls_failure' | 'tls_connected', - options: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - event, - host: options.host, - port: options.port, - ...data - }; - - if (event.includes('failure')) { - logger.error('SMTP TLS operation failed', logData); - } else { - logger.info('SMTP TLS operation', logData); - } -} - -/** - * Log email sending events - */ -export function logEmailSend( - event: 'start' | 'success' | 'failure', - recipients: string[], - options: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - event: `send_${event}`, - recipientCount: recipients.length, - recipients: recipients.slice(0, 5), // Only log first 5 recipients for privacy - host: options.host, - port: options.port, - ...data - }; - - switch (event) { - case 'start': - logger.info('SMTP email send started', logData); - break; - case 'success': - logger.info('SMTP email send successful', logData); - break; - case 'failure': - logger.error('SMTP email send failed', logData); - break; - } -} - -/** - * Log performance metrics - */ -export function logPerformance( - operation: string, - duration: number, - options: ISmtpClientOptions, - data?: Partial -): void { - const logData: ISmtpClientLogData = { - component: 'smtp-client', - operation, - duration, - host: options.host, - port: options.port, - ...data - }; - - if (duration > 10000) { // Log slow operations (>10s) - logger.warn('SMTP slow operation detected', logData); - } else { - logger.debug('SMTP operation performance', logData); - } -} - -/** - * Log debug information (only when debug is enabled) - */ -export function logDebug( - message: string, - options: ISmtpClientOptions, - data?: Partial -): void { - if (!options.debug) { - return; - } - - const logData: ISmtpClientLogData = { - component: 'smtp-client-debug', - host: options.host, - port: options.port, - ...data - }; - - logger.debug(`[SMTP Client Debug] ${message}`, logData); -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpclient/utils/validation.ts b/ts/mail/delivery/smtpclient/utils/validation.ts deleted file mode 100644 index 3c4d745..0000000 --- a/ts/mail/delivery/smtpclient/utils/validation.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * SMTP Client Validation Utilities - * Input validation functions for SMTP client operations - */ - -import { REGEX_PATTERNS } from '../constants.js'; -import type { ISmtpClientOptions, ISmtpAuthOptions } from '../interfaces.js'; - -/** - * Validate email address format - * Supports RFC-compliant addresses including empty return paths for bounces - */ -export function validateEmailAddress(email: string): boolean { - if (typeof email !== 'string') { - return false; - } - - const trimmed = email.trim(); - - // Handle empty return path for bounce messages (RFC 5321) - if (trimmed === '' || trimmed === '<>') { - return true; - } - - // Handle display name formats - const angleMatch = trimmed.match(/<([^>]+)>/); - if (angleMatch) { - return REGEX_PATTERNS.EMAIL_ADDRESS.test(angleMatch[1]); - } - - // Regular email validation - return REGEX_PATTERNS.EMAIL_ADDRESS.test(trimmed); -} - -/** - * Validate SMTP client options - */ -export function validateClientOptions(options: ISmtpClientOptions): string[] { - const errors: string[] = []; - - // Required fields - if (!options.host || typeof options.host !== 'string') { - errors.push('Host is required and must be a string'); - } - - if (!options.port || typeof options.port !== 'number' || options.port < 1 || options.port > 65535) { - errors.push('Port must be a number between 1 and 65535'); - } - - // Optional field validation - if (options.connectionTimeout !== undefined) { - if (typeof options.connectionTimeout !== 'number' || options.connectionTimeout < 1000) { - errors.push('Connection timeout must be a number >= 1000ms'); - } - } - - if (options.socketTimeout !== undefined) { - if (typeof options.socketTimeout !== 'number' || options.socketTimeout < 1000) { - errors.push('Socket timeout must be a number >= 1000ms'); - } - } - - if (options.maxConnections !== undefined) { - if (typeof options.maxConnections !== 'number' || options.maxConnections < 1) { - errors.push('Max connections must be a positive number'); - } - } - - if (options.maxMessages !== undefined) { - if (typeof options.maxMessages !== 'number' || options.maxMessages < 1) { - errors.push('Max messages must be a positive number'); - } - } - - // Validate authentication options - if (options.auth) { - errors.push(...validateAuthOptions(options.auth)); - } - - return errors; -} - -/** - * Validate authentication options - */ -export function validateAuthOptions(auth: ISmtpAuthOptions): string[] { - const errors: string[] = []; - - if (auth.method && !['PLAIN', 'LOGIN', 'OAUTH2', 'AUTO'].includes(auth.method)) { - errors.push('Invalid authentication method'); - } - - // For basic auth, require user and pass - if ((auth.user || auth.pass) && (!auth.user || !auth.pass)) { - errors.push('Both user and pass are required for basic authentication'); - } - - // For OAuth2, validate required fields - if (auth.oauth2) { - const oauth = auth.oauth2; - if (!oauth.user || !oauth.clientId || !oauth.clientSecret || !oauth.refreshToken) { - errors.push('OAuth2 requires user, clientId, clientSecret, and refreshToken'); - } - - if (oauth.user && !validateEmailAddress(oauth.user)) { - errors.push('OAuth2 user must be a valid email address'); - } - } - - return errors; -} - -/** - * Validate hostname format - */ -export function validateHostname(hostname: string): boolean { - if (!hostname || typeof hostname !== 'string') { - return false; - } - - // Basic hostname validation (allow IP addresses and domain names) - const hostnameRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$|^(?:\d{1,3}\.){3}\d{1,3}$|^\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$/; - return hostnameRegex.test(hostname); -} - -/** - * Validate port number - */ -export function validatePort(port: number): boolean { - return typeof port === 'number' && port >= 1 && port <= 65535; -} - -/** - * Sanitize and validate domain name for EHLO - */ -export function validateAndSanitizeDomain(domain: string): string { - if (!domain || typeof domain !== 'string') { - return 'localhost'; - } - - const sanitized = domain.trim().toLowerCase(); - if (validateHostname(sanitized)) { - return sanitized; - } - - return 'localhost'; -} - -/** - * Validate recipient list - */ -export function validateRecipients(recipients: string | string[]): string[] { - const errors: string[] = []; - const recipientList = Array.isArray(recipients) ? recipients : [recipients]; - - for (const recipient of recipientList) { - if (!validateEmailAddress(recipient)) { - errors.push(`Invalid email address: ${recipient}`); - } - } - - return errors; -} - -/** - * Validate sender address - */ -export function validateSender(sender: string): boolean { - return validateEmailAddress(sender); -} \ No newline at end of file diff --git a/ts/mail/routing/classes.unified.email.server.ts b/ts/mail/routing/classes.unified.email.server.ts index c3a6410..2c4b0bc 100644 --- a/ts/mail/routing/classes.unified.email.server.ts +++ b/ts/mail/routing/classes.unified.email.server.ts @@ -17,8 +17,7 @@ import { Email } from '../core/classes.email.js'; import { DomainRegistry } from './classes.domain.registry.js'; import { DnsManager } from './classes.dns.manager.js'; import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js'; -import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js'; -import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js'; +import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js'; import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js'; import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js'; import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js'; @@ -167,7 +166,6 @@ export class UnifiedEmailServer extends EventEmitter { public deliverySystem: MultiModeDeliverySystem; private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers private dkimKeys: Map = new Map(); // domain -> private key - private smtpClients: Map = new Map(); // host:port -> client constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) { super(); @@ -242,17 +240,8 @@ export class UnifiedEmailServer extends EventEmitter { bounceHandler: { processSmtpFailure: this.processSmtpFailure.bind(this) }, - onDeliverySuccess: async (item, _result) => { - // Record delivery success event for reputation monitoring - const email = item.processingResult as Email; - const senderDomain = email.from.split('@')[1]; - - if (senderDomain) { - this.recordReputationEvent(senderDomain, { - type: 'delivered', - count: email.to.length - }); - } + onDeliverySuccess: async (_item, _result) => { + // Delivery success recorded via delivery system } }; @@ -281,34 +270,50 @@ export class UnifiedEmailServer extends EventEmitter { } /** - * Get or create an SMTP client for the given host and port - * Uses connection pooling for efficiency + * Send an outbound email via the Rust SMTP client. + * Uses connection pooling in the Rust binary for efficiency. */ - public getSmtpClient(host: string, port: number = 25): SmtpClient { - const clientKey = `${host}:${port}`; - - // Check if we already have a client for this destination - let client = this.smtpClients.get(clientKey); - - if (!client) { - // Create a new pooled SMTP client - client = createPooledSmtpClient({ - host, - port, - secure: port === 465, - connectionTimeout: this.options.outbound?.connectionTimeout || 30000, - socketTimeout: this.options.outbound?.socketTimeout || 120000, - maxConnections: this.options.outbound?.maxConnections || 10, - maxMessages: 1000, // Messages per connection before reconnect - pool: true, - debug: false - }); - - this.smtpClients.set(clientKey, client); - logger.log('info', `Created new SMTP client pool for ${clientKey}`); + public async sendOutboundEmail(host: string, port: number, email: Email, options?: { + auth?: { user: string; pass: string }; + dkimDomain?: string; + dkimSelector?: string; + }): Promise { + // Build DKIM config if domain has keys + let dkim: { domain: string; selector: string; privateKey: string } | undefined; + if (options?.dkimDomain) { + try { + const { privateKey } = await this.dkimCreator.readDKIMKeys(options.dkimDomain); + dkim = { domain: options.dkimDomain, selector: options.dkimSelector || 'default', privateKey }; + } catch (err) { + logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`); + } } - - return client; + + // Serialize the Email to the outbound format + const outboundEmail: IOutboundEmail = { + from: email.from, + to: email.to, + cc: email.cc || [], + bcc: email.bcc || [], + subject: email.subject || '', + text: email.text || '', + html: email.html || undefined, + headers: email.headers as Record || {}, + }; + + return this.rustBridge.sendOutboundEmail({ + host, + port, + secure: port === 465, + domain: this.options.hostname, + auth: options?.auth, + email: outboundEmail, + dkim, + connectionTimeoutSecs: Math.floor((this.options.outbound?.connectionTimeout || 30000) / 1000), + socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000), + poolKey: `${host}:${port}`, + maxPoolConnections: this.options.outbound?.maxConnections || 10, + }); } /** @@ -486,16 +491,12 @@ export class UnifiedEmailServer extends EventEmitter { logger.log('info', 'Email delivery queue shut down'); } - // Close all SMTP client connections - for (const [clientKey, client] of this.smtpClients) { - try { - await client.close(); - logger.log('info', `Closed SMTP client pool for ${clientKey}`); - } catch (error) { - logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`); - } + // Close all Rust SMTP client connection pools + try { + await this.rustBridge.closeSmtpPool(); + } catch { + // Bridge may already be stopped } - this.smtpClients.clear(); logger.log('info', 'UnifiedEmailServer stopped successfully'); this.emit('stopped'); @@ -826,13 +827,12 @@ export class UnifiedEmailServer extends EventEmitter { email.headers['X-Forwarded-To'] = email.to.join(', '); email.headers['X-Forwarded-Date'] = new Date().toISOString(); - // Get SMTP client - const client = this.getSmtpClient(host, port); - try { - // Send email - await client.sendMail(email); - + // Send email via Rust SMTP client + await this.sendOutboundEmail(host, port, email, { + auth: auth as { user: string; pass: string } | undefined, + }); + logger.log('info', `Successfully forwarded email to ${host}:${port}`); SecurityLogger.getInstance().logEvent({ @@ -1297,15 +1297,6 @@ export class UnifiedEmailServer extends EventEmitter { // Queue the email for delivery await this.deliveryQueue.enqueue(email, mode, route); - // Record 'sent' event for domain reputation monitoring - const senderDomain = email.from.split('@')[1]; - if (senderDomain) { - this.recordReputationEvent(senderDomain, { - type: 'sent', - count: email.to.length - }); - } - logger.log('info', `Email queued with ID: ${id}`); return id; } catch (error) { @@ -1370,15 +1361,6 @@ export class UnifiedEmailServer extends EventEmitter { // Notify any registered listeners about the bounce this.emit('bounceProcessed', bounceRecord); - // Record bounce event for domain reputation tracking - if (bounceRecord.domain) { - this.recordReputationEvent(bounceRecord.domain, { - type: 'bounce', - hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, - receivingDomain: bounceRecord.recipient.split('@')[1] - }); - } - // Log security event SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, @@ -1450,15 +1432,6 @@ export class UnifiedEmailServer extends EventEmitter { // Notify any registered listeners about the bounce this.emit('bounceProcessed', bounceRecord); - // Record bounce event for domain reputation tracking - if (bounceRecord.domain) { - this.recordReputationEvent(bounceRecord.domain, { - type: 'bounce', - hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, - receivingDomain: bounceRecord.recipient.split('@')[1] - }); - } - // Log security event SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, @@ -1566,40 +1539,6 @@ export class UnifiedEmailServer extends EventEmitter { logger.log('info', `Removed ${email} from suppression list`); } - /** - * Record an email event for domain reputation tracking. - * Currently a no-op — the sender reputation monitor is not yet implemented. - * @param domain Domain sending the email - * @param event Event details - */ - public recordReputationEvent(domain: string, event: { - type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click'; - count?: number; - hardBounce?: boolean; - receivingDomain?: string; - }): void { - logger.log('debug', `Reputation event for ${domain}: ${event.type}`); - } - - /** - * Check if DKIM key exists for a domain - * @param domain Domain to check - */ - public hasDkimKey(domain: string): boolean { - return this.dkimKeys.has(domain); - } - - /** - * Record successful email delivery - * @param domain Sending domain - */ - public recordDelivery(domain: string): void { - this.recordReputationEvent(domain, { - type: 'delivered', - count: 1 - }); - } - /** * Record email bounce * @param domain Sending domain @@ -1608,7 +1547,6 @@ export class UnifiedEmailServer extends EventEmitter { * @param reason Bounce reason */ public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void { - // Record bounce in bounce manager const bounceRecord = { id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, recipient: `user@${receivingDomain}`, @@ -1622,17 +1560,8 @@ export class UnifiedEmailServer extends EventEmitter { statusCode: bounceType === 'hard' ? '550' : '450', processed: false }; - - // Process the bounce + this.bounceManager.processBounce(bounceRecord); - - // Record reputation event - this.recordReputationEvent(domain, { - type: 'bounce', - count: 1, - hardBounce: bounceType === 'hard', - receivingDomain - }); } /** diff --git a/ts/plugins.ts b/ts/plugins.ts index c7d6cf1..f39bb2b 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -21,61 +21,21 @@ export { util, } -// @serve.zone scope -import * as servezoneInterfaces from '@serve.zone/interfaces'; - -export { - servezoneInterfaces -} - -// @api.global scope -import * as typedrequest from '@api.global/typedrequest'; -import * as typedserver from '@api.global/typedserver'; -import * as typedsocket from '@api.global/typedsocket'; - -export { - typedrequest, - typedserver, - typedsocket, -} - // @push.rocks scope -import * as projectinfo from '@push.rocks/projectinfo'; -import * as qenv from '@push.rocks/qenv'; -import * as smartacme from '@push.rocks/smartacme'; -import * as smartdata from '@push.rocks/smartdata'; -import * as smartdns from '@push.rocks/smartdns'; import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs'; -import * as smartguard from '@push.rocks/smartguard'; -import * as smartjwt from '@push.rocks/smartjwt'; import * as smartlog from '@push.rocks/smartlog'; import * as smartmail from '@push.rocks/smartmail'; -import * as smartmetrics from '@push.rocks/smartmetrics'; -import * as smartnetwork from '@push.rocks/smartnetwork'; import * as smartpath from '@push.rocks/smartpath'; -import * as smartproxy from '@push.rocks/smartproxy'; -import * as smartpromise from '@push.rocks/smartpromise'; -import * as smartrequest from '@push.rocks/smartrequest'; -import * as smartrule from '@push.rocks/smartrule'; import * as smartrust from '@push.rocks/smartrust'; -import * as smartrx from '@push.rocks/smartrx'; -import * as smartunique from '@push.rocks/smartunique'; export const smartfs = new SmartFs(new SmartFsProviderNode()); -export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, SmartFs, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrust, smartrx, smartunique }; +export { smartfile, SmartFs, smartlog, smartmail, smartpath, smartrust }; // Define SmartLog types for use in error handling export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug'; -// apiclient.xyz scope -import * as cloudflare from '@apiclient.xyz/cloudflare'; - -export { - cloudflare, -} - // tsclass scope import * as tsclass from '@tsclass/tsclass'; diff --git a/ts/security/classes.rustsecuritybridge.ts b/ts/security/classes.rustsecuritybridge.ts index e393282..d9fb3fc 100644 --- a/ts/security/classes.rustsecuritybridge.ts +++ b/ts/security/classes.rustsecuritybridge.ts @@ -67,6 +67,71 @@ interface IContentScanResult { scannedElements: string[]; } +// --- SMTP Client types --- + +interface IOutboundEmail { + from: string; + to: string[]; + cc?: string[]; + bcc?: string[]; + subject?: string; + text?: string; + html?: string; + headers?: Record; +} + +interface ISmtpSendResult { + accepted: string[]; + rejected: string[]; + messageId?: string; + response: string; + envelope: { from: string; to: string[] }; +} + +interface ISmtpSendOptions { + host: string; + port: number; + secure?: boolean; + domain?: string; + auth?: { user: string; pass: string; method?: string }; + email: IOutboundEmail; + dkim?: { domain: string; selector: string; privateKey: string }; + connectionTimeoutSecs?: number; + socketTimeoutSecs?: number; + poolKey?: string; + maxPoolConnections?: number; +} + +interface ISmtpSendRawOptions { + host: string; + port: number; + secure?: boolean; + domain?: string; + auth?: { user: string; pass: string; method?: string }; + envelopeFrom: string; + envelopeTo: string[]; + rawMessageBase64: string; + poolKey?: string; +} + +interface ISmtpVerifyOptions { + host: string; + port: number; + secure?: boolean; + domain?: string; + auth?: { user: string; pass: string; method?: string }; +} + +interface ISmtpVerifyResult { + reachable: boolean; + greeting?: string; + capabilities?: string[]; +} + +interface ISmtpPoolStatus { + pools: Record; +} + interface IVersionInfo { bin: string; core: string; @@ -212,6 +277,26 @@ type TMailerCommands = { params: IRateLimitConfig; result: { configured: boolean }; }; + sendEmail: { + params: ISmtpSendOptions; + result: ISmtpSendResult; + }; + sendRawEmail: { + params: ISmtpSendRawOptions; + result: ISmtpSendResult; + }; + verifySmtpConnection: { + params: ISmtpVerifyOptions; + result: ISmtpVerifyResult; + }; + closeSmtpPool: { + params: { poolKey?: string }; + result: { closed: boolean }; + }; + getSmtpPoolStatus: { + params: Record; + result: ISmtpPoolStatus; + }; }; // --------------------------------------------------------------------------- @@ -660,6 +745,40 @@ export class RustSecurityBridge extends EventEmitter { return this.bridge.sendCommand('verifyEmail', opts); } + // ----------------------------------------------------------------------- + // SMTP Client — outbound email delivery via Rust + // ----------------------------------------------------------------------- + + /** Send a structured email via the Rust SMTP client. */ + public async sendOutboundEmail(opts: ISmtpSendOptions): Promise { + this.ensureRunning(); + return this.bridge.sendCommand('sendEmail', opts); + } + + /** Send a pre-formatted raw email via the Rust SMTP client. */ + public async sendRawEmail(opts: ISmtpSendRawOptions): Promise { + this.ensureRunning(); + return this.bridge.sendCommand('sendRawEmail', opts); + } + + /** Verify connectivity to an SMTP server. */ + public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise { + this.ensureRunning(); + return this.bridge.sendCommand('verifySmtpConnection', opts); + } + + /** Close a specific connection pool (or all pools if no key is given). */ + public async closeSmtpPool(poolKey?: string): Promise { + this.ensureRunning(); + await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any)); + } + + /** Get status of all SMTP client connection pools. */ + public async getSmtpPoolStatus(): Promise { + this.ensureRunning(); + return this.bridge.sendCommand('getSmtpPoolStatus', {} as any); + } + // ----------------------------------------------------------------------- // SMTP Server lifecycle // ----------------------------------------------------------------------- @@ -763,4 +882,11 @@ export type { IEmailData, IEmailReceivedEvent, IAuthRequestEvent, + IOutboundEmail, + ISmtpSendResult, + ISmtpSendOptions, + ISmtpSendRawOptions, + ISmtpVerifyOptions, + ISmtpVerifyResult, + ISmtpPoolStatus, }; diff --git a/ts/security/index.ts b/ts/security/index.ts index 5c0de62..830e2d8 100644 --- a/ts/security/index.ts +++ b/ts/security/index.ts @@ -32,4 +32,9 @@ export { type IBounceDetection, type IRustReputationResult, type IVersionInfo, + type IOutboundEmail, + type ISmtpSendResult, + type ISmtpSendOptions, + type ISmtpVerifyResult, + type ISmtpPoolStatus, } from './classes.rustsecuritybridge.js'; \ No newline at end of file