Index: .github/workflows/rust-clippy.yml ================================================================== --- .github/workflows/rust-clippy.yml +++ .github/workflows/rust-clippy.yml @@ -2,10 +2,13 @@ on: push: branches: [ master, release ] pull_request: +permissions: + contents: read + # sccache enable for rust/C builds env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" @@ -12,12 +15,10 @@ jobs: rust-ci-run: name: Run rust-clippy analyzing and tests runs-on: ubuntu-latest if: github.event_name == 'push' || (github.head_ref != 'master' && github.head_ref != 'release') - permissions: - contents: read steps: # SETUP - uses: actions/checkout@v7 with: persist-credentials: false Index: Cargo.lock ================================================================== --- Cargo.lock +++ Cargo.lock @@ -73,13 +73,13 @@ "pin-project-lite", ] [[package]] name = "async-compat" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +checksum = "4c97d7ff3c25d6c10d64170c12acaf5d4245e76dece3779c1d92b153a64f11df" dependencies = [ "futures-core", "futures-io", "once_cell", "pin-project-lite", @@ -227,37 +227,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" dependencies = [ "aws-lc-sys", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" dependencies = [ "cc", "cmake", "dunce", "fs_extra", "pkg-config", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" @@ -271,13 +265,13 @@ "byteorder", ] [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blocking" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -314,25 +308,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -349,47 +343,47 @@ "rand_core", ] [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -404,13 +398,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", ] @@ -423,13 +417,13 @@ "crossbeam-utils", ] [[package]] name = "config" -version = "0.15.25" +version = "0.15.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" +checksum = "38e9fbc53afcec54441422f1c596cd054655b32e70e1cbac956651b7b175a404" dependencies = [ "pathdiff", "serde_core", "toml", "winnow", @@ -484,11 +478,11 @@ dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "darling_macro" version = "0.24.1" @@ -495,11 +489,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "derive_more" version = "2.1.1" @@ -527,11 +521,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "dunce" version = "1.0.5" @@ -538,19 +532,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "encodify" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95d6e27686e1d6991a3077f4dbec9d33d18903466d234165ad86cd08c3ebc79" +dependencies = [ + "memchr", + "simdutf8", +] [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -586,13 +590,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -647,11 +651,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "futures-sink" version = "0.3.34" @@ -713,18 +717,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashify" -version = "0.2.9" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26" +checksum = "66f5a65bf7824ada2de4a2cf9fa0981dabffc5392bb8550767c3c3d71ed52e4d" dependencies = [ - "indexmap", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "heck" version = "0.5.0" @@ -748,13 +751,13 @@ "windows-link", ] [[package]] name = "html-escape" -version = "0.2.15" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5" +checksum = "491194d601b4fa9fac3c9571e014070a0ad1bc3392d1bc10419695196a8b3cc3" [[package]] name = "http" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -793,17 +796,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", - "futures-channel", "futures-core", "http", "http-body", "httparse", "itoa", @@ -813,13 +815,13 @@ "want", ] [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", "hyper-util", "rustls", @@ -828,20 +830,21 @@ "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", @@ -851,13 +854,13 @@ "tracing", ] [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" dependencies = [ "displaydoc", "potential_utf", "utf8_iter", "yoke", @@ -865,13 +868,13 @@ "zerovec", ] [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", @@ -878,13 +881,13 @@ "zerovec", ] [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", @@ -892,19 +895,19 @@ "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -913,19 +916,19 @@ "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", @@ -1048,30 +1051,30 @@ "libc", ] [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1089,21 +1092,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "mail-parser" -version = "0.11.8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a9080c1fb8190e232df37a10aa1b3d6b08be084e537069913f025e0ce86c5" +checksum = "908e05ffd2f281e4494cdb77552e6df2dc1476da22af811685b37773d415b2df" dependencies = [ + "encodify", "hashify", + "memchr", + "simdutf8", ] [[package]] name = "mailin" version = "0.6.5" @@ -1160,13 +1166,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] @@ -1193,13 +1199,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" [[package]] name = "owo-colors" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1278,13 +1284,13 @@ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", @@ -1298,13 +1304,13 @@ "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.4.3", "lru-slab", @@ -1321,13 +1327,13 @@ "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", @@ -1350,13 +1356,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", "rand_core", ] @@ -1409,11 +1415,11 @@ name = "reqwest" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.23.1", + "base64", "bytes", "futures-core", "futures-util", "http", "http-body", @@ -1475,13 +1481,13 @@ "semver", ] [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", @@ -1488,13 +1494,13 @@ "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.44" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", @@ -1503,13 +1509,13 @@ "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework", @@ -1534,13 +1540,13 @@ "zeroize", ] [[package]] name = "rustls-platform-verifier" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation", "core-foundation-sys", "jni", "log", @@ -1555,19 +1561,19 @@ "windows-sys 0.61.2", ] [[package]] name = "rustls-platform-verifier-android" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1588,13 +1594,13 @@ "winapi-util", ] [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" dependencies = [ "windows-sys 0.61.2", ] [[package]] @@ -1657,18 +1663,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", "serde", "serde_core", @@ -1675,37 +1681,37 @@ "zmij", ] [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] [[package]] name = "serde_with" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" dependencies = [ "serde_core", "serde_with_macros", ] [[package]] name = "serde_with_macros" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" dependencies = [ "darling", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "shellwords" version = "1.1.0" @@ -1760,13 +1766,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aca054fd9f8c2ebe8557a2433f307e038c0716124efd045daa0388afa5172189" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "smol" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1780,14 +1786,27 @@ "async-net", "async-process", "blocking", "futures-lite", ] + +[[package]] +name = "smol-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcaedb62e0475a6898988138995ec7b1e5d116167a72bb12c7b59d0649fbbc2" +dependencies = [ + "async-executor", + "async-io", + "async-lock", + "event-listener", + "futures-lite", +] [[package]] name = "smtp2tg" -version = "0.6.8" +version = "0.6.9" dependencies = [ "async-compat", "clap", "config", "hostname", @@ -1794,10 +1813,11 @@ "html-escape", "mail-parser", "mailin-embedded", "regex", "smol", + "smol-macros", "stacked_errors", "tgbot", ] [[package]] @@ -1851,13 +1871,13 @@ "unicode-ident", ] [[package]] name = "syn" -version = "3.0.5" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] @@ -1871,17 +1891,17 @@ "futures-core", ] [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "ternop" version = "1.0.1" @@ -1888,13 +1908,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4ae32d0a4605a89c28534371b056919c12e7a070ee07505af75130ff030111" [[package]] name = "tgbot" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f48a1d2ad66e77ebd16e857ea3925cfb5e19a34fe2c39a77f3c8c367694c80a" +checksum = "c0b411ddc08a9a4cb4844a32a7fc286efa1dbf5055a575eb6167d5ba1153d60f" dependencies = [ "async-stream", "bytes", "derive_more", "futures-util", @@ -1912,64 +1932,55 @@ "zmij", ] [[package]] name = "thin-vec" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" +checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e" [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.1" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", @@ -1977,23 +1988,23 @@ "windows-sys 0.61.2", ] [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", "futures-sink", "libc", @@ -2001,39 +2012,47 @@ "tokio", ] [[package]] name = "toml" -version = "1.1.5+spec-1.1.0" +version = "1.1.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" dependencies = [ + "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + [[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" @@ -2102,19 +2121,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2154,13 +2173,13 @@ "winapi-util", ] [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] [[package]] @@ -2169,13 +2188,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -2182,46 +2201,47 @@ "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.78" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] @@ -2237,13 +2257,13 @@ "web-sys", ] [[package]] name = "web-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", ] @@ -2257,13 +2277,13 @@ "wasm-bindgen", ] [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" dependencies = [ "rustls-pki-types", ] [[package]] @@ -2378,28 +2398,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerofrom" @@ -2410,25 +2430,25 @@ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zerotrie" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2439,30 +2459,30 @@ "zerofrom", ] [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.9", ] [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,23 +1,24 @@ [package] name = "smtp2tg" -version = "0.6.8" +version = "0.6.9" authors = [ "arcade@b1t.name" ] edition = "2024" license = "0BSD" repository = "http://fs.b1t.name/smtp2tg" [dependencies] async-compat = "0.2.5" config = { version = "0.15", default-features = false, features = [ "toml" ] } hostname = "0.4.1" -html-escape = "0.2.13" +html-escape = "0.3" clap = { version = "4.6", features = [ "derive" ] } -mail-parser = "0.11" +mail-parser = "1" mailin-embedded = "0.8" regex = "1.11.1" smol = "2.0.2" +smol-macros = "0.1" stacked_errors = "0.8" tgbot = "0.48" # All tests are moved to tests/ [lib] @@ -24,5 +25,9 @@ test = false [[bin]] test = false name = "smtp2tg" + +[profile.release] +panic = "abort" +opt-level = "z" Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -1,98 +1,14 @@ //! SMTP-to-Telegram gateway main library. //! -//! This module provides the core functionality for receiving emails via SMTP +//! This lib provides the core functionality for receiving emails via SMTP //! and forwarding them to Telegram chats. //! //! As we are not actually exporting this lib there would be no local Error's //! for now, everything will be just .stack()?'ed and propagated like in real -//! bin The lib here is just to separate all tests from main code into tests/ +//! bin. The lib here is just to separate all tests from main code into tests/ pub mod mail; mod telegram; pub mod utils; -use crate::mail::MailServer; - -use std::{ - io::Cursor, - os::unix::fs::PermissionsExt, - path::Path, -}; - -use clap::Parser; -use smol::{ - fs::metadata, -}; -use stacked_errors::{ - Result, - StackableErr, - bail, -}; - -/// SMTP-to-Telegram gateway -#[derive(Parser, Debug)] -#[command(name = "smtp2tg")] -#[command(about = format!("SMTP-to-Telegram gateway v{}, (C) 2024 - 2026", env!("CARGO_PKG_VERSION")), long_about = None)] -struct Args { - /// Set configuration file location - #[arg(short, long, default_value = "smtp2tg.toml")] - config: String, -} - -/// Main asynchronous entry point for the application. Runs the gateway using -/// the configuration selected on the command line. -/// -/// The configuration file must use owner-only permissions. Once configured, -/// the SMTP server runs until it stops or encounters an error. -/// -/// # Errors -/// Returns an error if the configuration file is missing, inaccessible, -/// insecure, malformed, or contains invalid required settings. -/// -/// # Panics -/// Panics if configuring or serving the SMTP server fails. -pub async fn async_main () -> Result<()> { - let args = Args::parse(); - let config_file = Path::new(&args.config); - if !config_file.try_exists().stack()? { - bail!("Configuration file not found: {config_file:?}\n\ - Hint: Ensure the file exists and the path is correct."); - }; - { - let meta = metadata(config_file).await.stack()?; - if (!0o100600 & meta.permissions().mode()) > 0 { - bail!("Configuration file permissions are insecure {config_file:?}\n\ - Current permissions: {:o}\n\ - Required: 0600 (owner read/write only).\n\ - Fix with: chmod 600 {config_file:?}", - meta.permissions().mode()); - } } - let settings: config::Config = config::Config::builder() - .set_default("api_gateway", "https://api.telegram.org").stack()? - .set_default("fields", vec!["date", "from", "subject"]).stack()? - .set_default("hostname", "smtp.2.tg").stack()? - .set_default("listen_on", "0.0.0.0:1025").stack()? - .set_default("domains", vec!["localhost", - hostname::get().context("Failed to get current hostname")? - .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()? - .add_source(config::File::from(config_file)) - .build() - .with_context(|| format!( - "Failed to parse configuration file: {config_file:?}\n\ - Check syntax against smtp2tg.toml.example.\n\ - Common issues: missing quotes, trailing commas in inline tables, or invalid types." - ))?; - - let listen_on = settings.get_string("listen_on").stack()?; - let server_name = settings.get_string("hostname").stack()?; - let core = MailServer::new(settings)?; - let mut server = mailin_embedded::Server::new(core); - - // TODO: remove unwraps when mailin-embedded bumps with better error handling - server.with_name(server_name) - .with_ssl(mailin_embedded::SslConfig::None).unwrap() - .with_addr(listen_on).unwrap(); - server.serve().unwrap(); - - Ok(()) -} +use std::io::Cursor; Index: src/mail.rs ================================================================== --- src/mail.rs +++ src/mail.rs @@ -67,11 +67,12 @@ /// /// # Arguments /// * `settings` - Parsed application configuration. /// /// # Errors - /// Returns an error if required configuration values are missing or invalid. + /// Returns an error if required configuration values are missing or invalid, + /// Telegram API client creation fails, or the address regex cannot be built. pub fn new (settings: config::Config) -> Result { let api_key = settings.get_string("api_key") .context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?; let mut recipients = HashMap::new(); for (name, value) in settings.get_table("recipients") @@ -105,11 +106,11 @@ .collect::>().join("|"); let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({re_domains}))?$")) .case_insensitive(true).build().stack()?; Ok(MailServer { - data: vec!(), + data: vec![], headers: None, tg, fields, address, }) @@ -129,11 +130,23 @@ Ok(self.tg.get(name).unwrap_or(&self.tg.default)) } else { bail!("Email address {name:?} is not from an allowed domain."); } } - /// Attempt to deliver one message + /// Forwards the buffered email to each distinct destination chat, using the + /// default chat for allowed addresses without a configured recipient. + /// + /// Inlines the first plain-text body if the escaped message is below 4096 + /// bytes. Sends remaining text parts and decoded attachments as documents; + /// HTML body parts are omitted. Leaves the buffered data and envelope intact. + /// + /// # Errors + /// Returns an error for a missing envelope, no recipients, a disallowed + /// address, mail parsing or part extraction failures, or rejected closing + /// `pre`/`code` tags in text passed to validation. Propagates media-group + /// construction and Telegram request errors, including diagnostic sends. + /// Delivery to earlier chats is not rolled back if a later send fails. async fn relay_mail (&self) -> Result<()> { if let Some(headers) = &self.headers { let mail = mail_parser::MessageParser::new().parse(&self.data) .context("Failed to parse mail.")?; @@ -170,96 +183,61 @@ reply.push(format!("Date: {date}")); } reply.push("
".into());
 			let reply = reply.join("\n");
 
-			let html_parts = mail.html_body_count();
-			let text_parts = mail.text_body_count();
-			let attachments = mail.attachment_count();
+			let text_parts = mail.text_body().len();
+			let html_parts = mail.html_body().len();
 			if html_parts != text_parts {
 				self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
 			}
-			//let mut html_num = 0;
 			let mut text_num = 0;
-			let mut file_num = 0;
-			// let's display first html or text part as body
-			let mut body: String = "".into();
-			/*
-			 * actually I don't wanna parse that html stuff
-			if html_parts > 0 {
-				let text = mail.body_html(0).stack()?;
-				if text.len() < 4096 - header_size {
-					body = text;
-					html_num = 1;
-				}
-			};
-			*/
-			if body.is_empty() && text_parts > 0 {
+			// Display the first text part as the body if it fits.
+			let mut body = String::new();
+			if text_parts > 0 {
 				let text = mail.body_text(0)
 					.context("Failed to extract text from message")?
 					.replace("\r\n", "\n");
 				let text = validate(&text).stack()?;
 				// 6:
 				// - (headers)
 				// - (mail text)
 				// - 6: 
if text.len() < 4096 - ( reply.len() + 6 ) { - body = text.to_string(); + body = text.into_owned(); text_num = 1; } }; let msg = format!("{}{}", reply, body); - // and let's collect all other attachment parts - let mut files_to_send = vec![]; - /* - * let's just skip html parts for now, they just duplicate text? - while html_num < html_parts { - files_to_send.push(mail.html_part(html_num).stack()?); - html_num += 1; - } - */ - while text_num < text_parts { - files_to_send.push(mail.text_part(text_num.try_into().stack()?) - .context("Failed to get text part from message.")?); - text_num += 1; - } - while file_num < attachments { - files_to_send.push(mail.attachment(file_num.try_into().stack()?) - .context("Failed to get file part from message.")?); - file_num += 1; - } + // Send remaining text-body parts followed by attachments. + let files_to_send: Vec<_> = mail.text_body().skip(text_num) + .chain(mail.attachments()).collect(); for chat in rcpt { if !files_to_send.is_empty() { let mut files = vec![]; - // let mut first_one = true; for chunk in &files_to_send { - let data: Vec = chunk.contents().to_vec(); + let data: Vec = chunk.decoded().to_vec(); let mut filename: Option = None; - for header in chunk.headers() { - if header.name() == "Content-Type" { - match header.value() { - mail_parser::HeaderValue::ContentType(contenttype) => { - if let Some(fname) = contenttype.attribute("name") { - filename = Some(fname.to_owned()); - } - }, - _ => { - self.tg.debug("Attachment has bad ContentType header.").await?; - }, - }; - }; - }; - let filename = if let Some(fname) = filename { - fname - } else { - "Attachment.txt".into() + for header in chunk.headers().iter() + .filter(|header| header.raw_name().eq_ignore_ascii_case("Content-Type")) + { + match header.value() { + mail_parser::HeaderValue::ContentType(contenttype) => { + if let Some(fname) = contenttype.attribute("name") { + filename = Some(fname.to_owned()); + } + }, + _ => { + self.tg.debug("Attachment has bad ContentType header.").await?; + }, + }; }; files.push(Attachment { data: Cursor::new(data), - name: filename, + name: filename.unwrap_or_else(|| "Attachment.txt".into()), }); } self.tg.sendgroup(chat, files, &msg).await?; } else { self.tg.send(chat, &msg).await?; Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -1,13 +1,97 @@ //! Simple SMTP-to-Telegram gateway. Can parse email and send them as telegram //! messages to specified chats, generally you specify which email address is //! available in configuration, everything else is sent to default address. -use async_compat::Compat; -use stacked_errors:: Result; - -// main function stub that executes main code from lib -fn main () -> Result<()> { - smol::block_on(Compat::new(async { - smtp2tg::async_main().await - })) -} +use smol_macros::{ + LocalExecutor, + main, +}; + +use std::{ + os::unix::fs::PermissionsExt, + path::Path, +}; + +use clap::Parser; +use smol::fs::metadata; +use stacked_errors::{ + Result, + StackableErr, + bail, +}; + +use smtp2tg::mail::MailServer; + +/// SMTP-to-Telegram gateway +#[derive(Parser, Debug)] +#[command(name = "smtp2tg")] +#[command(about = format!("SMTP-to-Telegram gateway v{}, (C) 2024 - 2026", env!("CARGO_PKG_VERSION")), long_about = None)] +struct Args { + /// Set configuration file location + #[arg(short, long, default_value = "smtp2tg.toml")] + config: String, +} + +main! { +/// Main asynchronous entry point for the application. Runs the gateway using +/// the configuration selected on the command line. +/// +/// The configuration file must use owner-only permissions. Once configured, +/// the SMTP server runs until it stops or encounters an error. +/// +/// # Errors +/// Returns an error if the configuration file is missing, inaccessible, +/// insecure, malformed, or contains invalid required settings. +/// Also returns an error if the system hostname cannot be read or is not +/// valid UTF-8, or if mail server initialisation fails. +/// +/// # Panics +/// Panics if configuring or serving the SMTP server fails. + async fn main (ex: &LocalExecutor<'_>) -> Result<()> { + let args = Args::parse(); + let settings = { + // Load configuration defaults and overrides after checking file permissions. + let config_file = Path::new(&args.config); + if !config_file.try_exists().stack()? { + bail!("Configuration file not found: {config_file:?}\n\ + Hint: Ensure the file exists and the path is correct."); + }; + { + let meta = metadata(config_file).await.stack()?; + if (!0o100600 & meta.permissions().mode()) > 0 { + bail!("Configuration file permissions are insecure {config_file:?}\n\ + Current permissions: {:o}\n\ + Required: 0600 (owner read/write only).\n\ + Fix with: chmod 600 {config_file:?}", + meta.permissions().mode()); + } } + config::Config::builder() + .set_default("api_gateway", "https://api.telegram.org").stack()? + .set_default("fields", vec!["date", "from", "subject"]).stack()? + .set_default("hostname", "smtp.2.tg").stack()? + .set_default("listen_on", "0.0.0.0:1025").stack()? + .set_default("domains", vec!["localhost", + hostname::get().context("Failed to get current hostname")? + .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()? + .add_source(config::File::from(config_file)) + .build() + .with_context(|| format!( + "Failed to parse configuration file: {config_file:?}\n\ + Check syntax against smtp2tg.toml.example.\n\ + Common issues: missing quotes, trailing commas in inline tables, or invalid types." + ))? + }; + + let listen_on = settings.get_string("listen_on").stack()?; + let server_name = settings.get_string("hostname").stack()?; + let core = MailServer::new(settings)?; + let mut server = mailin_embedded::Server::new(core); + + // TODO: remove unwraps when mailin-embedded bumps with better error handling + server.with_name(server_name) + .with_ssl(mailin_embedded::SslConfig::None).unwrap() + .with_addr(listen_on).unwrap(); + server.serve().unwrap(); + + Ok(()) +} } ADDED tests/cli.rs Index: tests/cli.rs ================================================================== --- /dev/null +++ tests/cli.rs @@ -0,0 +1,209 @@ +//! The entry point is private to the binary, so test its public CLI and SMTP +//! behavior with isolated configuration directories and bounded subprocesses. +use std::{ + fs::{self, OpenOptions}, + io::{BufRead, BufReader, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + os::unix::fs::{OpenOptionsExt, PermissionsExt}, + path::PathBuf, + process::{Child, Command, Output, Stdio}, + sync::atomic::{AtomicUsize, Ordering}, + thread, + time::{Duration, Instant}, +}; +const DEADLINE: Duration = Duration::from_secs(5); +struct TestDir(PathBuf); +impl TestDir { + fn new () -> Self { + static NEXT: AtomicUsize = AtomicUsize::new(0); + loop { + let path = std::env::temp_dir().join(format!("smtp2tg-cli-{}-{}", + std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + match fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("Cannot create test directory: {error}"), + } + } + } + fn config (&self, name: &str, text: &str, mode: u32) { + let path = self.0.join(name); + let mut file = OpenOptions::new().create_new(true).write(true).mode(0o600) + .open(&path).unwrap(); + file.write_all(text.as_bytes()).unwrap(); + fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap(); + } + fn spawn (&self, args: &[&str]) -> Process { + Process(Some(Command::new(env!("CARGO_BIN_EXE_smtp2tg")) + .current_dir(&self.0).args(args) + .stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) + .spawn().unwrap())) + } + fn run (&self, args: &[&str]) -> Output { + self.spawn(args).output() + } +} +impl Drop for TestDir { + fn drop (&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} +struct Process(Option); +impl Process { + fn output (mut self) -> Output { + let deadline = Instant::now() + DEADLINE; + while self.0.as_mut().unwrap().try_wait().unwrap().is_none() { + assert!(Instant::now() < deadline, "CLI did not exit within {DEADLINE:?}"); + thread::sleep(Duration::from_millis(10)); + } + self.0.take().unwrap().wait_with_output().unwrap() + } + fn connect (&mut self, address: SocketAddr) -> TcpStream { + let deadline = Instant::now() + DEADLINE; + loop { + assert!(self.0.as_mut().unwrap().try_wait().unwrap().is_none(), + "SMTP server exited before accepting connections"); + if let Ok(stream) = TcpStream::connect_timeout(&address, Duration::from_millis(100)) { + stream.set_read_timeout(Some(DEADLINE)).unwrap(); + stream.set_write_timeout(Some(DEADLINE)).unwrap(); + return stream; + } + assert!(Instant::now() < deadline, "SMTP server did not start within {DEADLINE:?}"); + thread::sleep(Duration::from_millis(10)); + } + } +} +impl Drop for Process { + fn drop (&mut self) { + if let Some(child) = &mut self.0 { + let _ = child.kill(); + let _ = child.wait(); + } + } +} +fn assert_error (output: Output, expected: &str) { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!output.status.success(), "Expected failure, stderr: {stderr}"); + assert!(stderr.contains(expected), "Missing {expected:?} in stderr: {stderr}"); +} +#[test] +fn help_succeeds_without_a_configuration_file () { + let dir = TestDir::new(); + for flag in ["-h", "--help"] { + let output = dir.run(&[flag]); + assert!(output.status.success()); + let help = String::from_utf8(output.stdout).unwrap(); + for expected in ["SMTP-to-Telegram gateway", env!("CARGO_PKG_VERSION"), + "-c, --config", "[default: smtp2tg.toml]"] { + assert!(help.contains(expected), "Missing {expected:?} in help: {help}"); + } + } +} +#[test] +fn missing_default_and_explicit_configuration_paths_are_reported () { + let dir = TestDir::new(); + for (args, path) in [ + (vec![], "smtp2tg.toml"), + (vec!["-c", "missing file.toml"], "missing file.toml"), + (vec!["--config", "missing file.toml"], "missing file.toml"), + ] { + let output = dir.run(&args); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains(path), "Missing path in diagnostic: {stderr}"); + assert_error(output, "Configuration file not found"); + } +} +#[test] +fn invalid_arguments_fail_before_configuration_loading () { + let dir = TestDir::new(); + for args in [vec!["-c"], vec!["--config"], vec!["--unknown-option"]] { + let output = dir.run(&args); + assert_eq!(output.status.code(), Some(2), "arguments: {args:?}"); + assert!(!String::from_utf8_lossy(&output.stderr).contains("Configuration file not found")); + } +} +#[test] +fn insecure_permissions_are_rejected_before_parsing () { + // Each forbidden permission is independent: group read/write/execute, + // other read/write/execute, and owner execute. + for mode in [0o640, 0o620, 0o610, 0o604, 0o602, 0o601, 0o700] { + let dir = TestDir::new(); + dir.config("smtp2tg.toml", "this is not valid TOML", mode); + let output = dir.run(&[]); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Required: 0600"), "mode {mode:o}: {stderr}"); + assert!(stderr.contains("chmod 600"), "mode {mode:o}: {stderr}"); + assert_error(output, "Configuration file permissions are insecure"); + } +} +#[test] +fn secure_malformed_config_reports_parse_context () { + let dir = TestDir::new(); + // A spaces-containing path must be passed intact, and the selected file + // must take precedence over an insecure default configuration. + dir.config("smtp2tg.toml", "ignored", 0o644); + dir.config("selected config.toml", "api_key = [", 0o600); + for flag in ["-c", "--config"] { + let output = dir.run(&[flag, "selected config.toml"]); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("selected config.toml")); + assert!(stderr.contains("smtp2tg.toml.example")); + assert_error(output, "Failed to parse configuration file"); + } +} +#[test] +fn required_settings_remain_required_after_applying_defaults () { + for (config, expected) in [ + ("default = 0\n[recipients]\n", "api_key"), + ("api_key = 'test-api-key'\n[recipients]\n", "default"), + ("api_key = 'test-api-key'\ndefault = 0\n", "recipients"), + ] { + let dir = TestDir::new(); + dir.config("smtp2tg.toml", config, 0o600); + let output = dir.run(&[]); + assert_error(output, expected); + } +} +fn smtp_reply (stream: &mut BufReader) -> String { + let mut response = String::new(); + loop { + let mut line = String::new(); + assert!(stream.read_line(&mut line).unwrap() > 0, "SMTP connection closed: {response}"); + let more = line.as_bytes().get(3) == Some(&b'-'); + response.push_str(&line); + if !more { + return response; + } + } +} +#[test] +fn entry_point_serves_smtp_with_defaults_and_configuration_overrides () { + for (overrides, hostname, allowed, denied) in [ + ("", "smtp.2.tg", "localhost", "outside.invalid"), + ("hostname = 'custom.smtp'\ndomains = ['example.com']\nfields = []\n", + "custom.smtp", "example.com", "localhost"), + ] { + let dir = TestDir::new(); + let reservation = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = reservation.local_addr().unwrap(); + dir.config("smtp2tg.toml", &format!( + "api_key = 'test-api-key'\ndefault = 0\nlisten_on = '{address}'\n{overrides}[recipients]\n"), 0o600); + drop(reservation); + let mut process = dir.spawn(&[]); + let mut stream = BufReader::new(process.connect(address)); + let banner = smtp_reply(&mut stream); + assert!(banner.starts_with("220 "), "{banner}"); + assert!(banner.contains(hostname), "{banner}"); + for (command, code) in [ + ("EHLO test.local\r\n".into(), "250"), + ("MAIL FROM:\r\n".into(), "250"), + (format!("RCPT TO:\r\n"), "250"), + (format!("RCPT TO:\r\n"), "550"), + ("QUIT\r\n".into(), "221"), + ] { + stream.get_mut().write_all(command.as_bytes()).unwrap(); + let reply = smtp_reply(&mut stream); + assert!(reply.starts_with(code), "command {command:?}: {reply}"); + } + } +} ADDED tests/mail_relay.rs Index: tests/mail_relay.rs ================================================================== --- /dev/null +++ tests/mail_relay.rs @@ -0,0 +1,206 @@ +//! Exercise the production mail handler with a recording transport. Including +//! the module here provides a test seam for its private Telegram dependency +//! without changing production visibility or making HTTP requests. +#[path = "../src/mail.rs"] +mod mail; +use smtp2tg::utils; +use std::io::Cursor; +use mailin_embedded::{Handler, response::{INTERNAL_ERROR, OK}}; +use stacked_errors::{Result, StackableErr}; +#[derive(Debug, PartialEq)] +enum Delivery { + Debug(String), + Text(String), + Files(Vec<(String, Vec)>, String), +} +mod telegram { + use super::{Delivery, utils::Attachment}; + use std::{cell::RefCell, collections::HashMap}; + use stacked_errors::{Result, StackableErr}; + use tgbot::types::ChatPeerId; + thread_local! { + // Handler::data_end drives its future on the calling test thread. + static DELIVERIES: RefCell> = const { RefCell::new(Vec::new()) }; + } + pub fn take_deliveries () -> Vec { + DELIVERIES.with(|items| std::mem::take(&mut *items.borrow_mut())) + } + #[derive(Debug)] + pub struct TelegramTransport { + pub default: ChatPeerId, + recipients: HashMap, + } + impl TelegramTransport { + pub fn new (_api_key: String, recipients: HashMap, settings: &config::Config) -> Result { + Ok(Self { + default: settings.get_int("default").stack()?.into(), + recipients: recipients.into_iter().map(|(name, id)| (name, id.into())).collect(), + }) + } + pub fn get (&self, name: &str) -> Result<&ChatPeerId> { + self.recipients.get(&name.to_lowercase()).context("Unknown test recipient") + } + pub async fn debug (&self, msg: &str) -> Result<()> { + DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Debug(msg.into()))); + Ok(()) + } + pub async fn send (&self, _to: &ChatPeerId, msg: &str) -> Result<()> { + DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Text(msg.into()))); + Ok(()) + } + pub async fn sendgroup (&self, _to: &ChatPeerId, media: Vec, msg: &str) -> Result<()> { + let files = media.into_iter().map(|file| (file.name, file.data.into_inner())).collect(); + DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Files(files, msg.into()))); + Ok(()) + } + } +} +fn server () -> Result { + telegram::take_deliveries(); + let settings = config::Config::builder() + .add_source(config::File::from_str(r#" + api_key = "test-api-key" + api_gateway = "https://api.telegram.org" + default = 0 + fields = [] + domains = ["example.com"] + [recipients] + "recipient@example.com" = 1 + "#, config::FileFormat::Toml)) + .build().stack()?; + mail::MailServer::new(settings) +} +fn relay (message: &str) -> Result> { + let mut server = server()?; + assert_eq!(server.data_start("example.com", "sender@example.com", true, + &["recipient@example.com".into()]), OK); + server.data(message.as_bytes()).stack()?; + assert_eq!(server.data_end(), OK); + Ok(telegram::take_deliveries()) +} +fn caption (body: &str) -> String { + format!("
\n
{body}
") +} +fn multipart (kind: &str, parts: &[&str]) -> String { + format!("MIME-Version: 1.0\r\nContent-Type: multipart/{kind}; boundary=parts\r\n\r\n--parts\r\n{}\r\n--parts--\r\n", + parts.join("\r\n--parts\r\n")) +} +const TEXT: &str = "Content-Type: text/plain; charset=utf-8\r\n\r\nPlain body"; +const HTML: &str = "Content-Type: text/html; charset=utf-8\r\n\r\n

HTML body

"; +#[test] +fn short_text_is_inlined_and_normalized () -> Result<()> { + let deliveries = relay("Content-Type: text/plain\r\n\r\nFirst & \r\nThird")?; + assert_eq!(deliveries, vec![ + Delivery::Text(caption("First & <second>\nThird")), + ]); + Ok(()) +} +#[test] +fn alternative_html_is_not_sent_as_a_duplicate_attachment () -> Result<()> { + let deliveries = relay(&multipart("alternative", &[TEXT, HTML]))?; + assert_eq!(deliveries, vec![Delivery::Text(caption("Plain body"))]); + Ok(()) +} +#[test] +fn html_only_message_uses_the_parsers_text_conversion () -> Result<()> { + assert_eq!(relay(HTML)?, vec![ + Delivery::Text(caption("HTML body\n")), + ]); + Ok(()) +} +#[test] +fn remaining_text_parts_are_sent_once_in_order () -> Result<()> { + let second = "Content-Type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nSecond=20body"; + let third = "Content-Type: text/plain\r\n\r\nThird body"; + assert_eq!(relay(&multipart("mixed", &[TEXT, second, third]))?, vec![ + Delivery::Files(vec![ + ("Attachment.txt".into(), b"Second body".to_vec()), + ("Attachment.txt".into(), b"Third body".to_vec()), + ], caption("Plain body")), + ]); + Ok(()) +} +#[test] +fn text_at_the_message_limit_is_sent_as_a_decoded_attachment () -> Result<()> { + // The existing contract requires the complete escaped message to be <4096 + // bytes. Exercise both sides so the first text iterator item cannot be lost. + let capacity = 4096 - caption("").len(); + for size in [capacity - 1, capacity] { + let body = "x".repeat(size); + let message = format!("Content-Type: text/plain\r\n\r\n{body}"); + let deliveries = relay(&message)?; + assert_eq!(deliveries.len(), 1); + let expected = if size < capacity { + Delivery::Text(caption(&body)) + } else { + Delivery::Files(vec![("Attachment.txt".into(), body.into_bytes())], caption("")) + }; + assert_eq!(deliveries[0], expected, "body size: {size}"); + } + Ok(()) +} +#[test] +fn attachment_payloads_are_transfer_decoded_and_keep_content_type_names () -> Result<()> { + let binary = "Content-Type: application/octet-stream; name=\"payload.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w=="; + let quoted = "Content-Type: application/octet-stream; name=\"notes.txt\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\ncaf=C3=A9=0Asecond=3Dline"; + assert_eq!(relay(&multipart("mixed", &[TEXT, binary, quoted]))?, vec![ + Delivery::Files(vec![ + ("payload.bin".into(), vec![0, 1, 2, 255]), + ("notes.txt".into(), "café\nsecond=line".as_bytes().to_vec()), + ], caption("Plain body")), + ]); + Ok(()) +} +#[test] +fn unnamed_attachment_uses_the_fallback_filename () -> Result<()> { + let attachment = "Content-Type: application/octet-stream\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\naGVsbG8="; + assert_eq!(relay(&multipart("mixed", &[attachment]))?, vec![ + Delivery::Files(vec![("Attachment.txt".into(), b"hello".to_vec())], caption("")), + ]); + Ok(()) +} +#[test] +fn attachment_filename_is_preserved_for_case_insensitive_mime_headers () -> Result<()> { + // MIME field names are case insensitive. The old parser's Header::name() + // returned canonical names; raw_name() now preserves their original case. + for header in ["Content-Type", "content-type", "CONTENT-TYPE", "cOnTeNt-TyPe"] { + let attachment = format!("{header}: application/octet-stream; name=\"report.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w=="); + assert_eq!(relay(&multipart("mixed", &[&attachment]))?, vec![ + Delivery::Files(vec![("report.bin".into(), vec![0, 1, 2, 255])], caption("")), + ], "MIME header: {header}"); + } + Ok(()) +} +#[test] +fn nested_alternatives_and_files_use_separate_iterators () -> Result<()> { + let alternative = multipart("alternative", &[TEXT, HTML]).replace("parts", "inner"); + let extra = "Content-Type: text/plain\r\n\r\nExtra body"; + let attachment = "Content-Type: application/octet-stream; name=\"file.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w=="; + assert_eq!(relay(&multipart("mixed", &[&alternative, extra, attachment]))?, vec![ + Delivery::Files(vec![ + ("Attachment.txt".into(), b"Extra body".to_vec()), + ("file.bin".into(), vec![0, 1, 2, 255]), + ], caption("Plain body")), + ]); + Ok(()) +} +#[test] +fn empty_text_body_does_not_create_a_spurious_attachment () -> Result<()> { + assert_eq!(relay("Content-Type: text/plain\r\n\r\n")?, vec![ + Delivery::Text(caption("")), + ]); + Ok(()) +} +#[test] +fn forbidden_closing_tag_stops_delivery () -> Result<()> { + let mut server = server()?; + server.data_start("example.com", "sender@example.com", false, + &["recipient@example.com".into()]); + server.data(b"Content-Type: text/plain\r\n\r\nUnsafe body").stack()?; + assert_eq!(server.data_end(), INTERNAL_ERROR); + let deliveries = telegram::take_deliveries(); + assert_eq!(deliveries.len(), 1); + assert!(matches!(&deliveries[0], Delivery::Debug(message) + if message.starts_with("Sending emails failed:"))); + Ok(()) +}