diff --git a/.repository-projection.json b/.repository-projection.json index d0cb113c9..a5b0d6357 100644 --- a/.repository-projection.json +++ b/.repository-projection.json @@ -3,9 +3,9 @@ "projection": "deixic-code", "projectionSchemaVersion": 1, "sourceRepository": "dx-corp/mono", - "sourceSha": "aa976f3d57658df7f0804227bb70e99e317ab2c1", + "sourceSha": "a69c1b529a97b14a03400ba6da3c7638e731bed7", "destinationRepository": "dx-corp/code", - "priorProjectedBase": "d2b2b4dbd91eb9443b760e8f0480a4413ff81b46", + "priorProjectedBase": "b559b7489e7be5106525ee575859f5ddef1f21d8", "definitionDigest": "cb9d429542ebb0a2de9b42a7aad60d9d8696a648ceba47c30f05c0b285ca0db7", "toolDigest": "f8cb071b0f27267120ccf45a00d0982f45113bd23535bef6a1555b4933f99f13", "contentDigest": "4676d676f45ea745c21a6ebb2d20aedcf3d83bcf978ca61d2f303c5ca4a09c82", diff --git a/vendor/codemode-provenance.json b/vendor/codemode-provenance.json new file mode 100644 index 000000000..ee793ca80 --- /dev/null +++ b/vendor/codemode-provenance.json @@ -0,0 +1,99 @@ +[ + { + "archive_files_sha256": { + ".cargo_vcs_info.json": "f03df2fac8642e82b6a3722802afa836c46d19ad317b669aad3311a12f20f0a7", + "Cargo.lock": "e0ad8a233cb652d36eb1e9c65845b9b2b24da80cb62f169c96babe893862453c", + "Cargo.toml": "2a876598dfdae3b57cd230fbacc08e964aaf896e85482f43857e439b705a2c01", + "Cargo.toml.orig": "65bfd4a89def9cb53c31fe6c40df23500db1e0ba45bcd117eb0f409cc612775c", + "README.md": "6ed3f903edad2a16c871baf0e2ce13d80a317e4147b9553e5bf186955a4231f5", + "src/allocator.rs": "301baa03e63c247b710e3359bfed959195e0649214d0f650b6901d7bc83f88c0", + "src/allocator/rust.rs": "15243e4e4eff57237553f814cf8f9f373466ae42fe6252223a460dc34c4d9dd8", + "src/class.rs": "9558392d052586b9984c71b285d046f16149bad0583d3c454ec7b9ec2a5fda56", + "src/class/cell.rs": "ae3204e96cd201451e4050e418b7df3f2e2f6fc75a5fe3550ec8203ebeb4d21b", + "src/class/ffi.rs": "9b3bb35e502a091c4510964721781d1256916a008a6cedacb014d9ba1fc4d8ed", + "src/class/impl_.rs": "608875a4b43b277d3bed87d98173b03b548b008979d89ffc3ffb3be659280ea5", + "src/class/trace.rs": "a1594bc9e4b0e581c13ffae9a5af715da750ab4a96d73976532e540c51556031", + "src/context.rs": "f7b069a5741975467caaf8f2a46ae4aa620ed3bd2ecf0814b343cbf5064d6798", + "src/context/async.rs": "0e061f70ad266dc4512c4fe688b6f26ffaa6ef77b8f3ce037d38a014da138728", + "src/context/async/future.rs": "093b2ed761eabd0d5104c96c3cd95bcc477474092b947f60e49fe6feb5523193", + "src/context/base.rs": "c5049fd11c92e56f602c4baf4c3b7a666b0b5623c5c03c546335984bc1a94fb5", + "src/context/builder.rs": "200cb0a5955828e74698d9955da2299ea89bc9864dde5df65f28b0cfc45e68f4", + "src/context/ctx.rs": "a9c71df2fb0cec3e053db18cf20ff65598193b4dcc66434e54666ebd09749003", + "src/context/multi_with_impl.rs": "4a5b9b90bad3a424a7501fdd7433b3c8a862620bfb776bfce3f3ed1d99f6f04a", + "src/context/owner.rs": "2a71c53639ebdf39fc0869ff23877c541581fe632e742a57cbb683fabdf37e81", + "src/js_lifetime.rs": "06f4925dd9a9ac2876993bb66123342d9fca596f61a71861dfbbcc78a7066fb9", + "src/lib.rs": "edef197427c18c7a60e66ec77dae893461e4da15236b7da67af4f65f7e7bf580", + "src/loader.rs": "02c1174843e6f27da8c3c76d6119a751804d9f83e2ff08ab050fca8fe5888b61", + "src/loader/builtin_loader.rs": "88dc50a90cc61f604865e29f4e9b4e5127615127d8a55eb87a5c789f48e499e1", + "src/loader/builtin_resolver.rs": "0368254d46e18bdaf5a3209be49497b674fd9a45718ba07b0d6137f4f016f249", + "src/loader/bundle.rs": "ff9bb1174d51db50f949486ded577cf07ae373312f4ee999b8d0e88a1de5519b", + "src/loader/compile.rs": "0190669255458e1669a98f48a18e4ff3a56f0af70b8fa00aea259cdf04ea32d2", + "src/loader/file_resolver.rs": "6ade41db474a91c8b92e3d6f9904fb2ad380066bdee0e0693fedfe57359ae860", + "src/loader/module_loader.rs": "f77d2b996592894c5456d4cfd0d865df1e4e3e6ad944dd9c61e8a7178f37d0aa", + "src/loader/native_loader.rs": "b877e5cce97b36d7e58a68449d39010ce0b5ace62bc4b7059d6998e2eba09c81", + "src/loader/script_loader.rs": "6980215683b3c2fc31ffec0f9bc42e4af1569a1abda16524f31e9ddc9ea24d76", + "src/loader/util.rs": "78c45bd8d20504f1de80597ee6305beae03ff7cb2f73ded9115dfe4a4fbfab4b", + "src/markers.rs": "0d3c665cc024313df4db41483e114fcbb36d6602bb880eeb9d4f7de37ec444bf", + "src/persistent.rs": "facd167ef7ddb838d56c1dd099677d9d9d36a22120b0dabfbfa05417f3be8b78", + "src/result.rs": "65377537f86dfabe7a77c0bdab6143aebf3cb4decebb81be4dcd547290b7ba0e", + "src/runtime.rs": "0a51347ad3b91fd18ca4c3fa6993f3487ea9158577698fae96344271c2622799", + "src/runtime/async.rs": "9a68ba07e8b90bba323fa643db4fcadbe3e4fb87d1a3e082bc97cc0a97e45dbc", + "src/runtime/base.rs": "1e5c5e85453096e647867d3978a03298614fa609c243f90270a339c000ae399c", + "src/runtime/exotic.rs": "767d55139ae206a452e779c8770f2d1f4d0de2f75e3be63026169cf959ff9628", + "src/runtime/opaque.rs": "b21fca9233686abadb65fa488c7e5d9043020e38cf728a83f88d112d7714c08c", + "src/runtime/raw.rs": "e7f70ab2752c2c91fcd36474d326fc24b3e4fd7c346dcc52a01673abb3eb36eb", + "src/runtime/schedular.rs": "f19b1f1d2acb5d672c93ea58512cdcc60b3f7ec1e7ede766b680eedcb8034566", + "src/runtime/schedular/atomic_waker.rs": "204e112674416114f52bd7c676a1c032f2f42dec9877ff22a929456ddf9fa74e", + "src/runtime/schedular/queue.rs": "9d0708700e039e84d5d06d50dae68c0ed60fee47377363f21084b165f7c2da46", + "src/runtime/schedular/task.rs": "2518021fef7268d21531b33cc7f5a644be64daccf6a8d0df720be06934b10206", + "src/runtime/schedular/vtable.rs": "eae72a12b25d5aa52ae4d440fe23df34e794c9438d69fbfb988513ccfe277a6d", + "src/runtime/schedular/waker.rs": "ad9586704aaf21851c9bb9e7d4d48f86e66ca15a949ce8d3027e5298a7e49096", + "src/runtime/spawner.rs": "a0dfa8a5ec6e21d6ab9ee5da4dd8798bef6664634f760c3a2576c6140851a258", + "src/runtime/userdata.rs": "52d3d5111a24c922aabff03de4d07c8e52843e007d718893c693e87a6f533937", + "src/safe_ref.rs": "f94ca7c42f20257348d92db7900e0b42e6119064d725d47312e9d57843dbc1b3", + "src/util.rs": "bd97d25dbcb303dfe820f3b1d221e063d5dbbb6e51488347d5bc9c6573012eed", + "src/value.rs": "01ac5d735a36298de881afe731c249ba50f240adda3dd6689c59620b7bb581c1", + "src/value/array.rs": "17760377055733f77abfbe2d056b92bf63c01aa95819f47481431a40cfff5d8c", + "src/value/array_buffer.rs": "62a4d82adf005bcb40d50e299466c5d0a96f987b14ef67ac61ad34743355c85c", + "src/value/atom.rs": "d83ba99326c893dbb531d9ec11c30e045f469ebe5a8d321be458654cd4dcd32c", + "src/value/atom/predefined.rs": "44d54dd94951d59af6617711b5419747810741854f4d9e5c26984fe99be71e37", + "src/value/bigint.rs": "f405d82351c4073433bcb938d195db231e66984d21289170b699c96d44b42587", + "src/value/convert.rs": "909d201564711823cedb77a2c7b511cc1edc17bbb52600475f0dcc25294c04ab", + "src/value/convert/atom.rs": "4dd356f2cbd1f8bf53e55c7d35d23b8541500f74f08324681cf211064b0b5fc1", + "src/value/convert/coerce.rs": "f761b53f1b30a395c5803ee85513acb82e8a28b9dd4710f8129ad4d6f50cbbbc", + "src/value/convert/from.rs": "dfb07cd95646821420e39648b6289716252c3f52e245d4c4db91e2776b0f4f50", + "src/value/convert/into.rs": "a8742102fdba459a4c0709baabe91c3aa8685e6892ef5e444696db91d54f8b96", + "src/value/exception.rs": "272b07e443bfa7d92e4b885abcbc92629a439ae15dd20780863432a3d1d614f7", + "src/value/function.rs": "186099d97cd2a3701c45bea43b38a290c63289590363fcc83207f60a62569ec3", + "src/value/function/args.rs": "41d23a2b2a319cad3f9e43dc9cf70b15ea0cd83324f4347ef914b17abde76082", + "src/value/function/ffi.rs": "c0c29010ca451433406322761da94b0c862bf01c49b0f748a3121e71824e332e", + "src/value/function/into_func.rs": "0971639e94816462a56be381ba106284e1ceb27ed5f3f27c69e6811713a0a9bc", + "src/value/function/params.rs": "a495be67ef9c29292fbe7e7d04a6b675f636ffdfe4551f4e99b65c7e0b6bb44e", + "src/value/function/types.rs": "9e19dfcdaeb19bedbc93714d6145b8805acdb51b51abb245f6da93308c5e2ae8", + "src/value/iterable.rs": "cd2891d6dddf014663167371c2eed3588a44f8466434d77260c7de70f0ef4531", + "src/value/module.rs": "ca512e6669f55c705648885e53982f4c7f8798aef5af35bb97a8a57939f60f6d", + "src/value/object.rs": "1b5ab8f7106d4276ac9823d91519fcd28fe36f215eb82e6fb39a2f5db842704a", + "src/value/object/property.rs": "7001ca4dd609720b4d7e872ef88e3a225eb3fff2504c8f48d3cf33c0a04393f5", + "src/value/promise.rs": "eee35c3723e917e86dab01bdd51d7f88a9f6e3813008c54fb5542d39e8e5fa6f", + "src/value/proxy.rs": "6cb8942b371ca2f8c616463761aabb6b69035f460891aaf3fb3aeabf7a63a302", + "src/value/proxy/handler.rs": "7c30f54a60bdc24a0899d661317f68c5524523b3ae1e6b4ab1f1843273308ccf", + "src/value/string.rs": "ef82ed01c948e028ffaea2d37bc29c42677ee2c203244f9980526a060592202b", + "src/value/symbol.rs": "c6e67505bfd7286d3a36b34a1b7672b21616acc2f619802ff7d55e5451ec9e5e", + "src/value/typed_array.rs": "8c665a13bc8027993409063000f2a040a363515b7a8d8dc017241f36f6e887fb" + }, + "archive_sha256": "7c6dbfedfbf458dc119c21ccd032c7527d185210b11a1ae796cd04106e6db280", + "crate": "rquickjs-core", + "omitted_archive_files": [ + "Cargo.lock" + ], + "patch": "rquickjs-core-0.13.0.patch", + "patch_sha256": "fb7b55eb61d1fd5dba155e0c921ffefe7eb2a008d138f4bd2db575999bd05787", + "path": "rquickjs-core-0.13.0", + "version": "0.13.0", + "workspaces": [ + "rust", + "products/maestro", + "tests/business-objects-integration" + ] + } +] diff --git a/vendor/email-encoding-0.4.2.patch b/vendor/email-encoding-0.4.2.patch new file mode 100644 index 000000000..76316c48f --- /dev/null +++ b/vendor/email-encoding-0.4.2.patch @@ -0,0 +1,151 @@ +diff --git a/src/headers/rfc2231.rs b/src/headers/rfc2231.rs +--- a/src/headers/rfc2231.rs ++++ b/src/headers/rfc2231.rs +@@ -7,6 +7,9 @@ + use super::{MAX_LINE_LEN, hex_encoding, utils, writer::EmailWriter}; + + /// Encode a string via RFC 2231. ++/// ++/// Returns an error if the writer fails or a continuation cannot consume ++/// any more of the value within the line limit. Output may be partial on error. + /// + /// # Examples + /// +@@ -74,8 +77,16 @@ + if utils::str_is_ascii_printable(value) { + // Can be written normally (Parameter Value Continuations) + +- let quoted_plain_combined_len = key.len() + "=\"".len() + value.len() + "\"\r\n".len(); +- if w.line_len() + quoted_plain_combined_len <= MAX_LINE_LEN { ++ let quoted_plain_combined_len = key ++ .len() ++ .checked_add(value.len()) ++ .and_then(|len| len.checked_add("=\"".len() + "\"\r\n".len())) ++ .ok_or(fmt::Error)?; ++ if w.line_len() ++ .checked_add(quoted_plain_combined_len) ++ .ok_or(fmt::Error)? ++ <= MAX_LINE_LEN ++ { + // Fits line + + w.write_str(key)?; +@@ -95,10 +106,16 @@ + loop { + write!(w, " {}*{}=\"", key, i)?; + +- let remaining_len = MAX_LINE_LEN - w.line_len() - "\"\r\n".len(); ++ let remaining_len = MAX_LINE_LEN ++ .checked_sub(w.line_len()) ++ .and_then(|len| len.checked_sub("\"\r\n".len())) ++ .ok_or(fmt::Error)?; + + let value_ = + utils::truncate_to_char_boundary(value, remaining_len.min(value.len())); ++ if value_.is_empty() && !value.is_empty() { ++ return Err(fmt::Error); ++ } + value = &value[value_.len()..]; + + utils::write_escaped(value_, w)?; +@@ -131,6 +148,7 @@ + w.write_str("utf-8''")?; + } + ++ let value_len = value.len(); + let mut chars = value.chars(); + while w.line_len() < MAX_LINE_LEN - "=xx=xx=xx=xx;\r\n".len() { + match chars.next() { +@@ -148,6 +166,9 @@ + // End of value + break; + } ++ if value.len() == value_len { ++ return Err(fmt::Error); ++ } + + // End of line + w.write_char(';')?; +@@ -167,6 +188,82 @@ + use pretty_assertions::assert_eq; + + use super::*; ++ ++ // Count output without retaining it, and stop a broken encoder safely. ++ struct BudgetWriter { ++ remaining: usize, ++ exhausted: bool, ++ } ++ ++ impl Write for BudgetWriter { ++ fn write_str(&mut self, value: &str) -> fmt::Result { ++ if value.len() > self.remaining { ++ self.exhausted = true; ++ return Err(fmt::Error); ++ } ++ self.remaining -= value.len(); ++ Ok(()) ++ } ++ } ++ ++ #[test] ++ fn long_key_unicode_fails_before_exhausting_writer() { ++ for key_len in 49..=62 { ++ let key = "a".repeat(key_len); ++ for value in ["é", "📕", "aé"] { ++ let mut sink = BudgetWriter { ++ remaining: 16_384, ++ exhausted: false, ++ }; ++ let result = { ++ let mut w = EmailWriter::new(&mut sink, 0, 0, false); ++ encode(&key, value, &mut w) ++ }; ++ assert!(result.is_err(), "key length {key_len}, value {value}"); ++ assert!(!sink.exhausted, "key length {key_len}, value {value}"); ++ } ++ } ++ } ++ ++ #[test] ++ fn long_key_plain_fails_before_exhausting_writer() { ++ let key = "a".repeat(62); ++ // The first million segments consume 1,111,110 bytes. The next ++ // segment number leaves no space for the last byte of the value. ++ let value = "a".repeat(1_111_111); ++ let mut sink = BudgetWriter { ++ remaining: 128 * 1024 * 1024, ++ exhausted: false, ++ }; ++ let result = { ++ let mut w = EmailWriter::new(&mut sink, 0, 0, false); ++ encode(&key, &value, &mut w) ++ }; ++ assert!(result.is_err()); ++ assert!(!sink.exhausted); ++ } ++ ++ #[test] ++ fn line_length_overflow_returns_error() { ++ let mut output = String::new(); ++ let result = { ++ let mut w = EmailWriter::new(&mut output, usize::MAX, 0, false); ++ encode("filename", "duck.txt", &mut w) ++ }; ++ assert!(result.is_err()); ++ assert!(output.is_empty()); ++ } ++ ++ #[test] ++ fn longest_unicode_key_with_room_encodes_value() { ++ let key = "a".repeat(48); ++ let mut output = String::new(); ++ { ++ let mut w = EmailWriter::new(&mut output, 0, 0, false); ++ encode(&key, "📕", &mut w).unwrap(); ++ } ++ assert_eq!(output, alloc::format!("\r\n {key}*0*=utf-8''%F0%9F%93%95")); ++ } + + #[test] + fn empty() { diff --git a/vendor/email-encoding-0.4.2/.cargo_vcs_info.json b/vendor/email-encoding-0.4.2/.cargo_vcs_info.json new file mode 100644 index 000000000..ab037c964 --- /dev/null +++ b/vendor/email-encoding-0.4.2/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "ae49096e1279e5d7207b70f7f2d690a44403b5bf" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/email-encoding-0.4.2/.github/workflows/ci.yml b/vendor/email-encoding-0.4.2/.github/workflows/ci.yml new file mode 100644 index 000000000..a7bcea920 --- /dev/null +++ b/vendor/email-encoding-0.4.2/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: [push, pull_request] + +jobs: + fmt: + name: rustfmt / stable + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Rust rustfmt + run: cargo fmt --all -- --check + + clippy: + name: clippy / stable + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Run clippy + run: cargo clippy -- -D warnings + + test: + name: test / ${{ matrix.name }} + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - name: stable + rust: stable + - name: beta + rust: beta + - name: nightly + rust: nightly + - name: 1.85.0 + rust: 1.85.0 + + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Run tests + run: cargo test + + - name: Run no_std tests + run: cargo test --no-default-features + + coverage: + name: coverage / stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-tarpaulin + run: | + LINK="https://github.com/xd009642/tarpaulin/releases/download/0.35.2/cargo-tarpaulin-x86_64-unknown-linux-gnu.tar.gz" + curl -L --output tarpaulin.tar.gz "$LINK" + tar -xzvf tarpaulin.tar.gz + chmod +x cargo-tarpaulin + + - name: Run cargo-tarpaulin + run: ./cargo-tarpaulin tarpaulin --out Xml + + - name: Upload to codecov.io + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Archive code coverage results + uses: actions/upload-artifact@v7 + with: + name: code-coverage-report + path: cobertura.xml diff --git a/vendor/email-encoding-0.4.2/.gitignore b/vendor/email-encoding-0.4.2/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/vendor/email-encoding-0.4.2/.gitignore @@ -0,0 +1 @@ +/target diff --git a/vendor/email-encoding-0.4.2/Cargo.toml b/vendor/email-encoding-0.4.2/Cargo.toml new file mode 100644 index 000000000..2ba8b35d6 --- /dev/null +++ b/vendor/email-encoding-0.4.2/Cargo.toml @@ -0,0 +1,58 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.85" +name = "email-encoding" +version = "0.4.2" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Low level email encoding RFCs implementations" +readme = "README.md" +keywords = [ + "email", + "rfc2047", + "rfc2231", +] +categories = [ + "email", + "encoding", + "no-std", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/lettre/email-encoding" + +[features] +default = ["std"] +std = [ + "base64/std", + "memchr/std", +] + +[lib] +name = "email_encoding" +path = "src/lib.rs" + +[dependencies.base64] +version = "0.23" +default-features = false + +[dependencies.memchr] +version = "2.1" +default-features = false + +[dev-dependencies.pretty_assertions] +version = "1" diff --git a/vendor/email-encoding-0.4.2/Cargo.toml.orig b/vendor/email-encoding-0.4.2/Cargo.toml.orig new file mode 100644 index 000000000..f61e3564d --- /dev/null +++ b/vendor/email-encoding-0.4.2/Cargo.toml.orig @@ -0,0 +1,21 @@ +[package] +name = "email-encoding" +version = "0.4.2" +description = "Low level email encoding RFCs implementations" +repository = "https://github.com/lettre/email-encoding" +edition = "2024" +rust-version = "1.85" +license = "MIT OR Apache-2.0" +categories = ["email", "encoding", "no-std"] +keywords = ["email", "rfc2047", "rfc2231"] + +[dependencies] +base64 = { version = "0.23", default-features = false } +memchr = { version = "2.1", default-features = false } + +[dev-dependencies] +pretty_assertions = "1" + +[features] +default = ["std"] +std = ["base64/std", "memchr/std"] diff --git a/vendor/email-encoding-0.4.2/LICENSE-APACHE b/vendor/email-encoding-0.4.2/LICENSE-APACHE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/email-encoding-0.4.2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/email-encoding-0.4.2/LICENSE-MIT b/vendor/email-encoding-0.4.2/LICENSE-MIT new file mode 100644 index 000000000..82f0e3516 --- /dev/null +++ b/vendor/email-encoding-0.4.2/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2024 Paolo Barbolini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/email-encoding-0.4.2/README.md b/vendor/email-encoding-0.4.2/README.md new file mode 100644 index 000000000..9397b8f5d --- /dev/null +++ b/vendor/email-encoding-0.4.2/README.md @@ -0,0 +1,20 @@ +# email-encoding + +[![crates.io](https://img.shields.io/crates/v/email-encoding.svg)](https://crates.io/crates/email-encoding) +[![Documentation](https://docs.rs/email-encoding/badge.svg)](https://docs.rs/email-encoding) +[![dependency status](https://deps.rs/crate/email-encoding/0.4.2/status.svg)](https://deps.rs/crate/email-encoding/0.4.2) +[![Rustc Version 1.85.0+](https://img.shields.io/badge/rustc-1.85.0+-lightgray.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/) +[![CI](https://github.com/lettre/email-encoding/actions/workflows/ci.yml/badge.svg)](https://github.com/lettre/email-encoding/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/lettre/email-encoding/branch/main/graph/badge.svg)](https://codecov.io/gh/lettre/email-encoding) + +Low-level `#[no_std]` crate implementing various RFCs for encoding emails. +Used internally by [lettre]. + +Implemented RFCs: + +* [RFC 2047] +* [RFC 2231] + +[lettre]: https://crates.io/crates/lettre +[RFC 2047]: https://datatracker.ietf.org/doc/html/rfc2047 +[RFC 2231]: https://datatracker.ietf.org/doc/html/rfc2231 diff --git a/vendor/email-encoding-0.4.2/src/body/base64.rs b/vendor/email-encoding-0.4.2/src/body/base64.rs new file mode 100644 index 000000000..71ec897e0 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/body/base64.rs @@ -0,0 +1,199 @@ +//! Base64 email body encoder. + +use core::{ + fmt::{self, Write}, + str, +}; + +use ::base64::Engine; + +const LINE_LEN: usize = 76; +const CRLF: &str = "\r\n"; + +/// Base64 encode the provided bytes. +/// +/// Splits the provided `b` into 57 bytes chunks and +/// base64 encodes them, writing the resulting 76 characters +/// CRLF sequence into `w`. +/// +/// The last line may be less than 76 characters in length +/// and will not end in CRLF. +/// +/// # Examples +/// +/// ```rust +/// # fn main() -> core::fmt::Result { +/// let input = "Hello! +/// You've got mail! +/// This one is base64 encoded. +/// +/// Enjoy your bytes 📬📬📬"; +/// +/// let mut output = String::new(); +/// email_encoding::body::base64::encode(input.as_bytes(), &mut output)?; +/// assert_eq!( +/// output, +/// concat!( +/// "SGVsbG8hCllvdSd2ZSBnb3QgbWFpbCEKVGhpcyBvbmUgaXMgYmFzZTY0IGVuY29kZWQuCgpFbmpv\r\n", +/// "eSB5b3VyIGJ5dGVzIPCfk6zwn5Os8J+TrA==" +/// ) +/// ); +/// # Ok(()) +/// # } +/// ``` +pub fn encode(b: &[u8], w: &mut dyn Write) -> fmt::Result { + let mut buf = [0; LINE_LEN]; + + let mut chunks = b.chunks(LINE_LEN / 4 * 3).peekable(); + while let Some(chunk) = chunks.next() { + let len = ::base64::engine::general_purpose::STANDARD + .encode_slice(chunk, &mut buf) + .expect("base64 output `buf` is not big enough"); + + w.write_str(str::from_utf8(&buf[..len]).expect("base64 produced an invalid encode"))?; + if chunks.peek().is_some() { + w.write_str(CRLF)?; + } + } + + Ok(()) +} + +/// Predict how many bytes [`encode`] is going to write given a `input_len` input length. +/// +/// # Panics +/// +/// Panics if any of the internal calculations overflow. +/// +/// # Examples +/// +/// ```rust +/// # use email_encoding::body::base64::encoded_len; +/// assert_eq!(encoded_len(0), 0); +/// assert_eq!(encoded_len(16), 24); +/// assert_eq!(encoded_len(300), 410); +/// ``` +pub const fn encoded_len(input_len: usize) -> usize { + // FIXME: use `Option::expect` with MSRV >= 1.83 + macro_rules! checked { + ($val:expr) => { + match $val { + Some(val) => val, + None => panic!("overflow"), + } + }; + } + + let mut base64_len = checked!((input_len / 3).checked_mul(4)); + if input_len % 3 != 0 { + base64_len = checked!(base64_len.checked_add(4)); + } + let mut crlf_len = base64_len / LINE_LEN * CRLF.len(); + if crlf_len >= CRLF.len() && base64_len % LINE_LEN == 0 { + crlf_len -= CRLF.len(); + } + checked!(base64_len.checked_add(crlf_len)) +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + + use pretty_assertions::assert_eq; + + use super::{encode, encoded_len}; + + #[test] + fn empty() { + let input = b""; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!(output, ""); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + fn oneline() { + let input = b"012"; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!(output, "MDEy"); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + fn oneline_padded() { + let input = b"0123"; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!(output, "MDEyMw=="); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + fn multiline() { + let input = + b"012345678998765432100123456789987654321001234567899876543210012345678998765432100"; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!( + output, + concat!( + "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n", + "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAw" + ) + ); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + fn multiline_padded() { + let input = + b"01234567899876543210012345678998765432100123456789987654321001234567899876543210"; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!( + output, + concat!( + "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n", + "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTA=" + ) + ); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + fn multiline_exact() { + let input = + b"012345678998765432100123456789987654321001234567899876543210012345678998765432100123456789987654321001234567899876543210012345678998765432100123456789987654321001234567899"; + let mut output = String::new(); + + encode(input, &mut output).unwrap(); + + assert_eq!( + output, + concat!( + "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n", + "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2\r\n", + "NTQzMjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5" + ) + ); + assert_eq!(output.len(), encoded_len(input.len())); + } + + #[test] + #[should_panic(expected = "overflow")] + fn overflow() { + encoded_len(usize::MAX); + } +} diff --git a/vendor/email-encoding-0.4.2/src/body/chooser.rs b/vendor/email-encoding-0.4.2/src/body/chooser.rs new file mode 100644 index 000000000..bfd9a2af2 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/body/chooser.rs @@ -0,0 +1,259 @@ +use core::mem; + +use super::{Encoding, StrOrBytes}; + +enum InputKind { + Ascii, + Utf8, + Binary, +} + +impl StrOrBytes<'_> { + fn kind(&self) -> InputKind { + if self.is_ascii() { + InputKind::Ascii + } else { + match self { + Self::Str(_) => InputKind::Utf8, + Self::Bytes(_) => InputKind::Binary, + } + } + } +} + +impl Encoding { + /// Choose the most efficient `Encoding` for `input` + /// + /// Look into `input` and decide what encoding format could best + /// be used to represent it. + /// + /// If the SMTP server supports the `SMTPUTF8` extension + /// `supports_utf8` _may_ me set to `true`, otherwise `false` + /// is the safest option. + /// + /// Possible return values based on `supports_utf8` + /// + /// | `Encoding` | `false` | `true` | + /// | ------------------ | ------- | ------ | + /// | `7bit` | ✅ | ✅ | + /// | `8bit` | ❌ | ✅ | + /// | `quoted-printable` | ✅ | ✅ | + /// | `base64` | ✅ | ✅ | + /// + /// # Examples + /// + /// ```rust + /// # use email_encoding::body::Encoding; + /// // Ascii + /// { + /// let input = "Hello, World!"; + /// assert_eq!(Encoding::choose(input, false), Encoding::SevenBit); + /// assert_eq!(Encoding::choose(input, true), Encoding::SevenBit); + /// } + /// + /// // Mostly ascii + utf-8 + /// { + /// let input = "Hello, World! 📬"; + /// assert_eq!(Encoding::choose(input, false), Encoding::QuotedPrintable); + /// assert_eq!(Encoding::choose(input, true), Encoding::EightBit); + /// } + /// + /// // Mostly utf-8 + /// { + /// let input = "Hello! 📬📬📬📬📬📬📬📬📬📬"; + /// assert_eq!(Encoding::choose(input, false), Encoding::Base64); + /// assert_eq!(Encoding::choose(input, true), Encoding::EightBit); + /// } + /// + /// // Non utf-8 bytes + /// { + /// let input = &[255, 35, 123, 190]; + /// assert_eq!(Encoding::choose(input, false), Encoding::Base64); + /// assert_eq!(Encoding::choose(input, true), Encoding::Base64); + /// } + /// ``` + pub fn choose<'a>(input: impl Into>, supports_utf8: bool) -> Self { + let input = input.into(); + Self::choose_impl(input, supports_utf8) + } + + fn choose_impl(input: StrOrBytes<'_>, supports_utf8: bool) -> Self { + let line_too_long = line_too_long(&input); + + match (input.kind(), line_too_long, supports_utf8) { + (InputKind::Ascii, false, _) => { + // Input is ascii and fits the maximum line length + Self::SevenBit + } + (InputKind::Ascii, true, _) => { + // Input is ascii but doesn't fix the maximum line length + quoted_printable_or_base64(&input) + } + (InputKind::Utf8, false, true) => { + // Input is utf-8, line fits, the server supports it + Self::EightBit + } + (InputKind::Utf8, true, true) => { + // Input is utf-8, line doesn't fit, the server supports it + quoted_printable_or_base64(&input) + } + (InputKind::Utf8, _, false) => { + // Input is utf-8, the server doesn't support it + quoted_printable_or_base64(&input) + } + (InputKind::Binary, _, _) => { + // Input is binary + Self::Base64 + } + } + } +} + +fn line_too_long(b: &[u8]) -> bool { + let mut last = 0; + memchr::memchr_iter(b'\n', b).any(|i| { + let last_ = mem::replace(&mut last, i); + (i - last_) >= 76 + }) || (b.len() - last) >= 76 +} + +fn quoted_printable_or_base64(b: &[u8]) -> Encoding { + if quoted_printable_efficient(b) { + Encoding::QuotedPrintable + } else { + Encoding::Base64 + } +} + +fn quoted_printable_efficient(b: &[u8]) -> bool { + let requiring_escaping = b + .iter() + .filter(|&b| !matches!(b, b'\t' | b' '..=b'~')) + .count(); + requiring_escaping <= (b.len() / 3) // 33.33% or less +} + +#[cfg(test)] +mod tests { + use super::{Encoding, line_too_long}; + + #[test] + fn ascii_short_str() { + let input = "0123"; + + assert_eq!(Encoding::choose(input, false), Encoding::SevenBit); + } + + #[test] + fn ascii_long_str() { + let input = concat!( + "0123\n", + "01234567899876543210012345678998765432100123456789987654321001234567899876543210\n", + "4567" + ); + + assert_eq!(Encoding::choose(input, false), Encoding::QuotedPrintable); + } + + #[test] + fn ascii_short_binary() { + let input = b"0123"; + + assert_eq!(Encoding::choose(input, false), Encoding::SevenBit); + } + + #[test] + fn ascii_long_binary() { + let input = concat!( + "0123\n", + "01234567899876543210012345678998765432100123456789987654321001234567899876543210\n", + "4567" + ) + .as_bytes(); + + assert_eq!(Encoding::choose(input, false), Encoding::QuotedPrintable); + } + + #[test] + fn utf8_short_str_supported() { + let input = "0123 📬"; + + assert_eq!(Encoding::choose(input, true), Encoding::EightBit); + } + + #[test] + fn utf8_short_str_unsupported_efficient() { + let input = "01234567899876543210 📬"; + + assert_eq!(Encoding::choose(input, false), Encoding::QuotedPrintable); + } + + #[test] + fn utf8_short_str_unsupported_inefficient() { + let input = "0123 📬"; + + assert_eq!(Encoding::choose(input, false), Encoding::Base64); + } + + #[test] + fn utf8_long_str_efficient() { + let input = + "01234567899876543210012345678998765432100123456789987654321001234567899876543210"; + + assert_eq!(Encoding::choose(input, true), Encoding::QuotedPrintable); + } + + #[test] + fn utf8_long_str_inefficient() { + let input = "0123 📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬📬"; + + assert_eq!(Encoding::choose(input, true), Encoding::Base64); + } + + #[test] + fn binary() { + let input = &[255, 234, b'A', b'C', 210]; + + assert_eq!(Encoding::choose(input, false), Encoding::Base64); + } + + #[test] + fn not_too_long_oneline() { + let input = b"0123"; + + assert!(!line_too_long(input)); + } + + #[test] + fn not_too_long_multiline() { + let input = concat!( + "0123\n", + "4567\n", + "00000000000000000000000000000000000000000\n", + "89" + ) + .as_bytes(); + + assert!(!line_too_long(input)); + } + + #[test] + fn too_long_oneline() { + let input = + b"01234567899876543210012345678998765432100123456789987654321001234567899876543210"; + + assert!(line_too_long(input)); + } + + #[test] + fn too_long_multiline() { + let input = concat!( + "0123\n", + "01234567899876543210012345678998765432100123456789987654321001234567899876543210\n", + "4567" + ) + .as_bytes(); + + assert!(line_too_long(input)); + } +} diff --git a/vendor/email-encoding-0.4.2/src/body/mod.rs b/vendor/email-encoding-0.4.2/src/body/mod.rs new file mode 100644 index 000000000..a8fd77a53 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/body/mod.rs @@ -0,0 +1,57 @@ +//! Email body encoding algorithms. + +use core::ops::Deref; + +pub mod base64; +mod chooser; + +/// A possible email `Content-Transfer-Encoding` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Encoding { + /// 7bit (US-ASCII) + SevenBit, + /// 8bit (UTF-8) + EightBit, + /// [Quoted Printable](https://docs.rs/quoted_printable/0.4.5/quoted_printable/fn.encode_to_str.html) + QuotedPrintable, + /// [Base64](self::base64::encode) + Base64, +} + +/// A borrowed `str` or `[u8]` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum StrOrBytes<'a> { + /// `str` variant + Str(&'a str), + /// `[u8]` variant + Bytes(&'a [u8]), +} + +impl<'a> From<&'a str> for StrOrBytes<'a> { + fn from(s: &'a str) -> Self { + Self::Str(s) + } +} + +impl<'a> From<&'a [u8]> for StrOrBytes<'a> { + fn from(s: &'a [u8]) -> Self { + Self::Bytes(s) + } +} + +impl<'a, const N: usize> From<&'a [u8; N]> for StrOrBytes<'a> { + fn from(s: &'a [u8; N]) -> Self { + Self::Bytes(s) + } +} + +impl Deref for StrOrBytes<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + Self::Str(s) => s.as_bytes(), + Self::Bytes(b) => b, + } + } +} diff --git a/vendor/email-encoding-0.4.2/src/headers/hex.rs b/vendor/email-encoding-0.4.2/src/headers/hex.rs new file mode 100644 index 000000000..1871cf463 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/hex.rs @@ -0,0 +1,9 @@ +const HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF"; + +pub(super) const fn encode_byte(byte: u8) -> [u8; 2] { + [lower_nibble_to_hex(byte >> 4), lower_nibble_to_hex(byte)] +} + +const fn lower_nibble_to_hex(half_byte: u8) -> u8 { + HEX_CHARS[(half_byte & 0x0F) as usize] +} diff --git a/vendor/email-encoding-0.4.2/src/headers/hex_encoding.rs b/vendor/email-encoding-0.4.2/src/headers/hex_encoding.rs new file mode 100644 index 000000000..e603e2c89 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/hex_encoding.rs @@ -0,0 +1,42 @@ +use core::fmt::{self, Write}; + +use super::{hex, utils, writer::EmailWriter}; + +pub(super) fn percent_encode_char(w: &mut EmailWriter<'_>, to_append: char) -> fmt::Result { + encode_char(w, '%', to_append) +} + +fn encode_char(w: &mut EmailWriter<'_>, prefix: char, to_append: char) -> fmt::Result { + if utils::char_is_ascii_alphanumeric_plus(to_append) { + w.write_char(to_append)?; + } else { + let mut dst = [0; 4]; + let written = to_append.encode_utf8(&mut dst).len(); + + encode_byte(w, prefix, dst[0])?; + + // Manually unrolled loop over `dst` + if written >= 2 { + encode_byte(w, prefix, dst[1])?; + + if written >= 3 { + encode_byte(w, prefix, dst[2])?; + + if written >= 4 { + encode_byte(w, prefix, dst[3])?; + } + } + } + } + + Ok(()) +} + +fn encode_byte(w: &mut EmailWriter<'_>, prefix: char, to_append: u8) -> fmt::Result { + let chars = hex::encode_byte(to_append); + w.write_char(prefix)?; + w.write_char(char::from(chars[0]))?; + w.write_char(char::from(chars[1]))?; + + Ok(()) +} diff --git a/vendor/email-encoding-0.4.2/src/headers/mod.rs b/vendor/email-encoding-0.4.2/src/headers/mod.rs new file mode 100644 index 000000000..59d5d0b11 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/mod.rs @@ -0,0 +1,11 @@ +//! Email header encoding algorithms. + +mod hex; +mod hex_encoding; +pub mod quoted_string; +pub mod rfc2047; +pub mod rfc2231; +mod utils; +pub mod writer; + +pub(super) const MAX_LINE_LEN: usize = 76; diff --git a/vendor/email-encoding-0.4.2/src/headers/quoted_string.rs b/vendor/email-encoding-0.4.2/src/headers/quoted_string.rs new file mode 100644 index 000000000..0fd37b411 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/quoted_string.rs @@ -0,0 +1,228 @@ +//! Quoted String encoder. + +use core::fmt::{self, Write}; + +use super::{rfc2047, utils, writer::EmailWriter}; + +/// Encode a string that may need to be quoted. +/// +/// # Examples +/// +/// ```rust +/// # use email_encoding::headers::writer::EmailWriter; +/// # fn main() -> core::fmt::Result { +/// { +/// let input = "John"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::quoted_string::encode(input, &mut writer)?; +/// } +/// assert_eq!(output, "John"); +/// } +/// +/// { +/// let input = "John Smith"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::quoted_string::encode(input, &mut writer)?; +/// } +/// assert_eq!(output, "\"John Smith\""); +/// } +/// +/// { +/// let input = "Rogue \" User"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::quoted_string::encode(input, &mut writer)?; +/// } +/// assert_eq!(output, "\"Rogue \\\" User\""); +/// } +/// +/// { +/// let input = "Adrián"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::quoted_string::encode(input, &mut writer)?; +/// } +/// assert_eq!(output, "=?utf-8?b?QWRyacOhbg==?="); +/// } +/// # Ok(()) +/// # } +/// ``` +pub fn encode(value: &str, w: &mut EmailWriter<'_>) -> fmt::Result { + #[derive(Debug)] + enum Strategy { + Plain, + Quoted, + QuotedEscaped, + Rfc2047, + } + + let mut strategy = Strategy::Plain; + + let mut bytes = value.as_bytes(); + + // Plain -> Quoted + while !bytes.is_empty() { + let byte = bytes[0]; + + if !byte.is_ascii_alphanumeric() && !matches!(byte, b'-' | b'_' | b'.') { + strategy = Strategy::Quoted; + break; + } + + bytes = &bytes[1..]; + } + + // Quoted -> QuotedEscaped + while !bytes.is_empty() { + let byte = bytes[0]; + + if !byte.is_ascii_alphanumeric() && !matches!(byte, b' ' | b'-' | b'_' | b'.') { + strategy = Strategy::QuotedEscaped; + break; + } + + bytes = &bytes[1..]; + } + + // QuotedEscaped -> Rfc2047 + while !bytes.is_empty() { + let byte = bytes[0]; + + if !byte.is_ascii_alphanumeric() + && !matches!(byte, b'\\' | b'"' | b' ' | b'-' | b'_' | b'.') + { + strategy = Strategy::Rfc2047; + break; + } + + bytes = &bytes[1..]; + } + + match strategy { + Strategy::Plain => { + w.write_str(value)?; + } + Strategy::Quoted => { + w.write_char('"')?; + w.folding().write_str(value)?; + w.write_char('"')?; + } + Strategy::QuotedEscaped => { + w.write_char('"')?; + utils::write_escaped(value, &mut w.folding())?; + w.write_char('"')?; + } + Strategy::Rfc2047 => { + rfc2047::encode(value, w)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn plain() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("1234567890abcd", &mut w).unwrap(); + } + + assert_eq!(s, "1234567890abcd"); + } + + #[test] + fn quoted() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("1234567890 abcd", &mut w).unwrap(); + } + + assert_eq!(s, "\"1234567890 abcd\""); + } + + #[test] + fn quoted_long() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("1234567890 abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "\"1234567890\r\n", + " abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd\"" + ) + ); + } + + #[test] + fn quoted_escaped() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("12345\\67890 ab\"cd", &mut w).unwrap(); + } + + assert_eq!(s, "\"12345\\\\67890 ab\\\"cd\""); + } + + // TODO: get it working for the quoted escaped strategy + // #[test] + // fn quoted_escaped_long() { + // let mut s = String::new(); + // let line_len = s.len(); + // + // { + // let mut w = EmailWriter::new(&mut s, line_len, 0, false, false); + // encode("12345\\67890 ab\"cdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", &mut w).unwrap(); + // } + // + // assert_eq!(s, concat!( + // "\"12345\\\\67890\r\n", + // " ab\\\"cdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd\"" + // )); + // } + + #[test] + fn rfc2047() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("12345\\67890 perché ab\"cd", &mut w).unwrap(); + } + + assert_eq!(s, "=?utf-8?b?MTIzNDVcNjc4OTAgcGVyY2jDqSBhYiJjZA==?="); + } +} diff --git a/vendor/email-encoding-0.4.2/src/headers/rfc2047.rs b/vendor/email-encoding-0.4.2/src/headers/rfc2047.rs new file mode 100644 index 000000000..6feabc683 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/rfc2047.rs @@ -0,0 +1,170 @@ +//! [RFC 2047] encoder. +//! +//! [RFC 2047]: https://datatracker.ietf.org/doc/html/rfc2047 + +use core::fmt::{self, Write}; + +use super::{MAX_LINE_LEN, utils, writer::EmailWriter}; + +const ENCODING_START_PREFIX: &str = "=?utf-8?b?"; +const ENCODING_END_SUFFIX: &str = "?="; + +/// Encode a string via RFC 2047. +/// +/// # Examples +/// +/// ```rust +/// # use email_encoding::headers::writer::EmailWriter; +/// # fn main() -> core::fmt::Result { +/// let input = "Adrián"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::rfc2047::encode(input, &mut writer)?; +/// } +/// assert_eq!(output, "=?utf-8?b?QWRyacOhbg==?="); +/// # Ok(()) +/// # } +/// ``` +pub fn encode(mut s: &str, w: &mut EmailWriter<'_>) -> fmt::Result { + let mut wrote = false; + + while !s.is_empty() { + let remaining_line_len = MAX_LINE_LEN.saturating_sub( + ENCODING_START_PREFIX.len() + ENCODING_END_SUFFIX.len() + w.line_len() + "\r\n".len(), + ); + let unencoded_remaining_line_len = remaining_line_len / 4 * 3; + + let mut word = + utils::truncate_to_char_boundary(s, unencoded_remaining_line_len.min(s.len())); + if word.is_empty() { + if wrote || w.has_spaces() { + // No space remaining on this line, go to a new one + w.new_line()?; + if !w.has_spaces() { + // The last write before this call to `encode` most + // likely wasn't rfc2047 so we must write a "soft" + // space to let the decoder know we're still within the + // same header + w.space(); + } + continue; + } + + // No space remaining, but going to a new line will require us + // to introduce a new space, which will mess up things even more. + word = &s[..s.chars().next().expect("`s` is empty").len_utf8()]; + } + + // Write the prefix + w.write_str(ENCODING_START_PREFIX)?; + + // Encode `word` + let encoder = base64::display::Base64Display::new( + word.as_bytes(), + &base64::engine::general_purpose::STANDARD, + ); + write!(w, "{}", encoder)?; + + // Write the suffix + w.write_str(ENCODING_END_SUFFIX)?; + + s = &s[word.len()..]; + wrote = true; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn empty() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("", &mut w).unwrap(); + } + + assert_eq!(s, ""); + } + + #[test] + fn basic() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("abcd", &mut w).unwrap(); + } + + assert_eq!(s, "=?utf-8?b?YWJjZA==?="); + } + + #[test] + fn basic_nopad() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode("abcdef", &mut w).unwrap(); + } + + assert_eq!(s, "=?utf-8?b?YWJjZGVm?="); + } + + #[test] + fn long() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode(&"lettre".repeat(20), &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "=?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0?=\r\n", + " =?utf-8?b?dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?=\r\n", + " =?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?=" + ) + ); + } + + #[test] + fn long_encoded() { + let mut s = String::new(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, false); + encode(&"hétérogénéité".repeat(16), &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "=?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n", + " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n", + " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n", + " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n", + " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n", + " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n", + " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOp?=", + ) + ); + } +} diff --git a/vendor/email-encoding-0.4.2/src/headers/rfc2231.rs b/vendor/email-encoding-0.4.2/src/headers/rfc2231.rs new file mode 100644 index 000000000..8031f7a50 --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/rfc2231.rs @@ -0,0 +1,479 @@ +//! [RFC 2231] encoder. +//! +//! [RFC 2231]: https://datatracker.ietf.org/doc/html/rfc2231 + +use core::fmt::{self, Write}; + +use super::{MAX_LINE_LEN, hex_encoding, utils, writer::EmailWriter}; + +/// Encode a string via RFC 2231. +/// +/// Returns an error if the writer fails or a continuation cannot consume +/// any more of the value within the line limit. Output may be partial on error. +/// +/// # Examples +/// +/// ```rust +/// # use email_encoding::headers::writer::EmailWriter; +/// # fn main() -> core::fmt::Result { +/// { +/// let input = "invoice.pdf"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?; +/// } +/// assert_eq!(output, "filename=\"invoice.pdf\""); +/// } +/// +/// { +/// let input = "invoice_2022_06_04_letshaveaverylongfilenamewhynotemailcanhandleit.pdf"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?; +/// } +/// assert_eq!( +/// output, +/// concat!( +/// "\r\n", +/// " filename*0=\"invoice_2022_06_04_letshaveaverylongfilenamewhynotemailcanha\";\r\n", +/// " filename*1=\"ndleit.pdf\"" +/// ) +/// ); +/// } +/// +/// { +/// let input = "faktúra.pdf"; +/// +/// let mut output = String::new(); +/// { +/// let mut writer = EmailWriter::new(&mut output, 0, 0, false); +/// email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?; +/// } +/// assert_eq!( +/// output, +/// concat!( +/// "\r\n", +/// " filename*0*=utf-8''fakt%C3%BAra.pdf" +/// ) +/// ); +/// } +/// # Ok(()) +/// # } +/// ``` +pub fn encode(key: &str, mut value: &str, w: &mut EmailWriter<'_>) -> fmt::Result { + assert!( + utils::str_is_ascii_alphanumeric(key), + "`key` must only be composed of ascii alphanumeric chars" + ); + assert!( + key.len() + "*12*=utf-8'';".len() < MAX_LINE_LEN, + "`key` must not be too long to cause the encoder to overflow the max line length" + ); + + if utils::str_is_ascii_printable(value) { + // Can be written normally (Parameter Value Continuations) + + let quoted_plain_combined_len = key + .len() + .checked_add(value.len()) + .and_then(|len| len.checked_add("=\"".len() + "\"\r\n".len())) + .ok_or(fmt::Error)?; + if w.line_len() + .checked_add(quoted_plain_combined_len) + .ok_or(fmt::Error)? + <= MAX_LINE_LEN + { + // Fits line + + w.write_str(key)?; + + w.write_char('=')?; + + w.write_char('"')?; + utils::write_escaped(value, w)?; + w.write_char('"')?; + } else { + // Doesn't fit line + + w.new_line()?; + w.forget_spaces(); + + let mut i = 0_usize; + loop { + write!(w, " {}*{}=\"", key, i)?; + + let remaining_len = MAX_LINE_LEN + .checked_sub(w.line_len()) + .and_then(|len| len.checked_sub("\"\r\n".len())) + .ok_or(fmt::Error)?; + + let value_ = + utils::truncate_to_char_boundary(value, remaining_len.min(value.len())); + if value_.is_empty() && !value.is_empty() { + return Err(fmt::Error); + } + value = &value[value_.len()..]; + + utils::write_escaped(value_, w)?; + + w.write_char('"')?; + + if value.is_empty() { + // End of value + break; + } + + // End of line + w.write_char(';')?; + w.new_line()?; + + i += 1; + } + } + } else { + // Needs encoding (Parameter Value Character Set and Language Information) + + w.new_line()?; + w.forget_spaces(); + + let mut i = 0_usize; + loop { + write!(w, " {}*{}*=", key, i)?; + + if i == 0 { + w.write_str("utf-8''")?; + } + + let value_len = value.len(); + let mut chars = value.chars(); + while w.line_len() < MAX_LINE_LEN - "=xx=xx=xx=xx;\r\n".len() { + match chars.next() { + Some(c) => { + hex_encoding::percent_encode_char(w, c)?; + value = chars.as_str(); + } + None => { + break; + } + } + } + + if value.is_empty() { + // End of value + break; + } + if value.len() == value_len { + return Err(fmt::Error); + } + + // End of line + w.write_char(';')?; + w.new_line()?; + + i += 1; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloc::{borrow::ToOwned, string::String}; + + use pretty_assertions::assert_eq; + + use super::*; + + // Count output without retaining it, and stop a broken encoder safely. + struct BudgetWriter { + remaining: usize, + exhausted: bool, + } + + impl Write for BudgetWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.len() > self.remaining { + self.exhausted = true; + return Err(fmt::Error); + } + self.remaining -= value.len(); + Ok(()) + } + } + + #[test] + fn long_key_unicode_fails_before_exhausting_writer() { + for key_len in 49..=62 { + let key = "a".repeat(key_len); + for value in ["é", "📕", "aé"] { + let mut sink = BudgetWriter { + remaining: 16_384, + exhausted: false, + }; + let result = { + let mut w = EmailWriter::new(&mut sink, 0, 0, false); + encode(&key, value, &mut w) + }; + assert!(result.is_err(), "key length {key_len}, value {value}"); + assert!(!sink.exhausted, "key length {key_len}, value {value}"); + } + } + } + + #[test] + fn long_key_plain_fails_before_exhausting_writer() { + let key = "a".repeat(62); + // The first million segments consume 1,111,110 bytes. The next + // segment number leaves no space for the last byte of the value. + let value = "a".repeat(1_111_111); + let mut sink = BudgetWriter { + remaining: 128 * 1024 * 1024, + exhausted: false, + }; + let result = { + let mut w = EmailWriter::new(&mut sink, 0, 0, false); + encode(&key, &value, &mut w) + }; + assert!(result.is_err()); + assert!(!sink.exhausted); + } + + #[test] + fn line_length_overflow_returns_error() { + let mut output = String::new(); + let result = { + let mut w = EmailWriter::new(&mut output, usize::MAX, 0, false); + encode("filename", "duck.txt", &mut w) + }; + assert!(result.is_err()); + assert!(output.is_empty()); + } + + #[test] + fn longest_unicode_key_with_room_encodes_value() { + let key = "a".repeat(48); + let mut output = String::new(); + { + let mut w = EmailWriter::new(&mut output, 0, 0, false); + encode(&key, "📕", &mut w).unwrap(); + } + assert_eq!(output, alloc::format!("\r\n {key}*0*=utf-8''%F0%9F%93%95")); + } + + #[test] + fn empty() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = 1; + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode("filename", "", &mut w).unwrap(); + } + + assert_eq!(s, "Content-Disposition: attachment; filename=\"\""); + } + + #[test] + fn parameter() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = 1; + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode("filename", "duck.txt", &mut w).unwrap(); + } + + assert_eq!(s, "Content-Disposition: attachment; filename=\"duck.txt\""); + } + + #[test] + fn parameter_to_escape() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = 1; + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode("filename", "du\"ck\\.txt", &mut w).unwrap(); + } + + assert_eq!( + s, + "Content-Disposition: attachment; filename=\"du\\\"ck\\\\.txt\"" + ); + } + + #[test] + fn parameter_long() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode( + "filename", + "a-fairly-long-filename-just-to-see-what-happens-when-we-encode-it-will-the-client-be-able-to-handle-it.txt", + &mut w, + ) + .unwrap(); + } + + assert_eq!( + s, + concat!( + "Content-Disposition: attachment;\r\n", + " filename*0=\"a-fairly-long-filename-just-to-see-what-happens-when-we-enco\";\r\n", + " filename*1=\"de-it-will-the-client-be-able-to-handle-it.txt\"" + ) + ); + } + + #[test] + fn parameter_special() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode("filename", "caffè.txt", &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "Content-Disposition: attachment;\r\n", + " filename*0*=utf-8''caff%C3%A8.txt" + ) + ); + } + + #[test] + fn parameter_special_long() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode( + "filename", + "testing-to-see-what-happens-when-📕📕📕📕📕📕📕📕📕📕📕-are-placed-on-the-boundary.txt", + &mut w, + ) + .unwrap(); + } + + assert_eq!( + s, + concat!( + "Content-Disposition: attachment;\r\n", + " filename*0*=utf-8''testing-to-see-what-happens-when-%F0%9F%93%95;\r\n", + " filename*1*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n", + " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n", + " filename*3*=%F0%9F%93%95%F0%9F%93%95-are-placed-on-the-bound;\r\n", + " filename*4*=ary.txt" + ) + ); + } + + #[test] + fn parameter_special_long_part2() { + let mut s = "Content-Disposition: attachment;".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + encode( + "filename", + "testing-to-see-what-happens-when-books-are-placed-in-the-second-part-📕📕📕📕📕📕📕📕📕📕📕.txt", + &mut w, + ) + .unwrap(); + } + + assert_eq!( + s, + concat!( + "Content-Disposition: attachment;\r\n", + " filename*0*=utf-8''testing-to-see-what-happens-when-books-ar;\r\n", + " filename*1*=e-placed-in-the-second-part-%F0%9F%93%95%F0%9F%93%95;\r\n", + " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n", + " filename*3*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n", + " filename*4*=%F0%9F%93%95.txt" + ) + ); + } + + #[test] + fn parameter_dont_split_on_hex_boundary() { + let base_header = "Content-Disposition: attachment;".to_owned(); + let line_len = base_header.len(); + + for start_offset in &["", "x", "xx", "xxx"] { + let mut filename = (*start_offset).to_owned(); + + for i in 1..256 { + // 'Ü' results in two hex chars %C3%9C + filename.push('Ü'); + + let mut output = base_header.clone(); + { + let mut w = EmailWriter::new(&mut output, line_len, 0, true); + encode("filename", &filename, &mut w).unwrap(); + } + + // look for all hex encoded chars + let output_len = output.len(); + let mut found_hex_count = 0; + for (percent_sign_idx, _) in output.match_indices('%') { + assert!(percent_sign_idx + 3 <= output_len); + + // verify we get the expected hex sequence for an 'Ü' + let must_be_hex = &output[percent_sign_idx + 1..percent_sign_idx + 3]; + assert!( + must_be_hex == "C3" || must_be_hex == "9C", + "unexpected hex char: {}", + must_be_hex + ); + found_hex_count += 1; + } + // verify the number of hex encoded chars adds up + let number_of_chars_in_hex = 2; + assert_eq!(found_hex_count, i * number_of_chars_in_hex); + + // verify max line length + let mut last_newline_pos = 0; + for (newline_idx, _) in output.match_indices("\r\n") { + let line_length = newline_idx - last_newline_pos; + assert!( + line_length < MAX_LINE_LEN, + "expected line length exceeded: {} > {}", + line_length, + MAX_LINE_LEN + ); + last_newline_pos = newline_idx; + } + // ensure there was at least one newline + assert_ne!(0, last_newline_pos); + } + } + } + + #[test] + #[should_panic(expected = "`key` must only be composed of ascii alphanumeric chars")] + fn non_ascii_key() { + let mut s = String::new(); + let mut w = EmailWriter::new(&mut s, 0, 0, true); + let _ = encode("📬", "", &mut w); + } +} diff --git a/vendor/email-encoding-0.4.2/src/headers/utils.rs b/vendor/email-encoding-0.4.2/src/headers/utils.rs new file mode 100644 index 000000000..3dfaf643e --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/utils.rs @@ -0,0 +1,84 @@ +use core::fmt::{self, Write}; + +pub(super) fn str_is_ascii_alphanumeric(s: &str) -> bool { + s.bytes().all(|c| c.is_ascii_alphanumeric()) +} + +pub(super) const fn char_is_ascii_alphanumeric_plus(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') +} + +pub(super) fn str_is_ascii_printable(s: &str) -> bool { + s.bytes().all(char_is_ascii_printable) +} + +const fn char_is_ascii_printable(c: u8) -> bool { + matches!(c, b' '..=b'~') +} + +pub(super) fn write_escaped(s: &str, w: &mut impl Write) -> fmt::Result { + debug_assert!(s.is_ascii()); + + for b in s.bytes() { + match b { + b'\\' => { + w.write_str("\\\\")?; + } + b'"' => { + w.write_str("\\\"")?; + } + b => { + w.write_char(char::from(b))?; + } + } + } + + Ok(()) +} + +pub(super) fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { + assert!(max <= s.len()); + + while !s.is_char_boundary(max) { + max -= 1; + } + &s[..max] +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn truncate_ascii() { + assert_eq!(truncate_to_char_boundary("12345678", 4), "1234"); + } + + #[test] + fn truncate0_ascii() { + assert_eq!(truncate_to_char_boundary("12345678", 0), ""); + } + + #[test] + fn truncate_utf8() { + assert_eq!(truncate_to_char_boundary("📬📬📬📬📬📬", 8), "📬📬"); + } + + #[test] + fn truncate0_utf8() { + assert_eq!(truncate_to_char_boundary("📬📬📬📬📬📬", 0), ""); + } + + #[test] + fn truncate_boundary_utf8() { + assert_eq!(truncate_to_char_boundary("📬📬📬📬📬📬", 9), "📬📬"); + } + + #[test] + #[should_panic] + fn truncate_out_of_bounds() { + let _ = truncate_to_char_boundary("12345678", 16); + } +} diff --git a/vendor/email-encoding-0.4.2/src/headers/writer.rs b/vendor/email-encoding-0.4.2/src/headers/writer.rs new file mode 100644 index 000000000..aa285b6ab --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/headers/writer.rs @@ -0,0 +1,363 @@ +//! Utilities for writing email headers to a [`Write`]r. +//! +//! [`Write`]: core::fmt::Write + +use core::fmt::{self, Write}; + +use super::MAX_LINE_LEN; + +/// Wrapper around [`Write`] that remembers the length of the +/// last line written to it. +/// +/// [`Write`]: core::fmt::Write +pub struct EmailWriter<'a> { + writer: &'a mut dyn Write, + line_len: usize, + spaces: usize, + can_go_to_new_line_now: bool, +} + +impl<'a> EmailWriter<'a> { + /// Construct a new `EmailWriter`. + /// + /// * `line_len` is the length of the last line in `writer`. + /// * `spaces` the number of spaces that must be written before + /// the next write. + /// * `can_go_to_new_line_now` is whether the current line can + /// be wrapped now or not. + pub fn new( + writer: &'a mut dyn Write, + line_len: usize, + spaces: usize, + can_go_to_new_line_now: bool, + ) -> Self { + Self { + writer, + line_len, + spaces, + can_go_to_new_line_now, + } + } + + /// Go to a new line and reset the `line_len` to `0`. + pub fn new_line(&mut self) -> fmt::Result { + self.writer.write_str("\r\n")?; + self.line_len = 0; + self.can_go_to_new_line_now = false; + + Ok(()) + } + + /// Write a space which _might_ get wrapped to a new line on the next write. + pub fn space(&mut self) { + self.spaces += 1; + } + + /// Forget all buffered spaces + pub(super) fn forget_spaces(&mut self) { + self.spaces = 0; + } + + pub(super) fn has_spaces(&mut self) -> bool { + self.spaces >= 1 + } + + /// Get the length in bytes of the last line written to the inner writer. + pub fn line_len(&self) -> usize { + self.line_len + } + + /// Get the length in bytes of the last line written to the inner writer + /// plus the spaces which might be written to in on the next write call. + pub fn projected_line_len(&self) -> usize { + self.line_len + self.spaces + } + + /// Get a [`Write`]r which automatically line folds text written to it. + /// + /// [`Write`]: core::fmt::Write + pub fn folding<'b>(&'b mut self) -> FoldingEmailWriter<'a, 'b> { + FoldingEmailWriter { writer: self } + } + + fn write_spaces(&mut self) -> fmt::Result { + while self.spaces > 0 { + self.writer.write_char(' ')?; + self.line_len += 1; + self.spaces -= 1; + } + + Ok(()) + } +} + +impl Write for EmailWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.write_spaces()?; + + let s_after = s.trim_end_matches(' '); + self.spaces += s.len() - s_after.len(); + + if !s_after.is_empty() { + self.writer.write_str(s_after)?; + self.line_len += s_after.len(); + self.can_go_to_new_line_now = true; + } + + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + if c == ' ' { + self.spaces += 1; + } else { + self.write_spaces()?; + self.can_go_to_new_line_now = true; + + self.writer.write_char(c)?; + self.line_len += c.len_utf8(); + } + + Ok(()) + } +} + +impl Drop for EmailWriter<'_> { + fn drop(&mut self) { + let _ = self.write_spaces(); + } +} + +/// Wrapper around [`Write`] that remembers the length of the +/// last line and automatically line folds text written to it. +/// +/// [`Write`]: core::fmt::Write +pub struct FoldingEmailWriter<'a, 'b> { + writer: &'b mut EmailWriter<'a>, +} + +impl Write for FoldingEmailWriter<'_, '_> { + fn write_str(&mut self, mut s: &str) -> fmt::Result { + while !s.is_empty() { + if s.starts_with(' ') { + self.writer.space(); + s = &s[1..]; + continue; + } + + let (start, end) = s.find(' ').map_or((s, ""), |i| s.split_at(i)); + + if self.writer.can_go_to_new_line_now + && self.writer.spaces >= 1 + && (self.writer.projected_line_len() + start.len()) > MAX_LINE_LEN + { + self.writer.new_line()?; + } + + self.writer.write_str(start)?; + s = end; + } + + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + if c == ' ' { + self.writer.spaces += 1; + } else { + self.write_str(c.encode_utf8(&mut [0u8; 4]))?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use alloc::borrow::ToOwned; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn wrap_immediate() { + let mut s = + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + for _ in 0..16 { + w.folding().write_str("0123456789").unwrap(); + } + } + + assert_eq!( + s, + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + ); + } + + #[test] + fn wrap_keeping_final_whitespace() { + let mut s = "Subject: AAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 1, true); + w.folding().write_str("12345 ").unwrap(); + w.new_line().unwrap(); + w.folding().write_str("12345").unwrap(); + } + + assert_eq!(s, concat!("Subject: AAAAAAAAAAAAAA 12345\r\n", " 12345")); + } + + #[test] + fn catch_space() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 1, true); + w.folding().write_str("BBB ").unwrap(); + w.folding().write_str("CCCCCCCCCCCCC").unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBB\r\n", + " CCCCCCCCCCCCC" + ) + ); + } + + #[test] + fn catch_spaces() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 1, true); + w.folding().write_str("BBB ").unwrap(); + w.folding().write_str("CCCCCCCCCCCCC").unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBB\r\n", + " CCCCCCCCCCCCC" + ) + ); + } + + #[test] + fn explicit_space() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 1, true); + w.folding().write_str("BBB").unwrap(); + w.space(); + w.folding().write_str("CCCCCCCCCCCCC").unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBB\r\n", + " CCCCCCCCCCCCC" + ) + ); + } + + #[test] + fn explicit_spaces() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 1, true); + w.folding().write_str("BBB").unwrap(); + w.space(); + w.write_char(' ').unwrap(); + w.space(); + w.folding().write_str("CCCCCCCCCCCCC").unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBB\r\n", + " CCCCCCCCCCCCC" + ) + ); + } + + #[test] + fn optional_breakpoint() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.space(); + w.folding().write_str("BBBBBBBBBB").unwrap(); + w.space(); + w.folding().write_str("CCCCCCCCCC").unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n", + " BBBBBBBBBB CCCCCCCCCC", + ) + ); + } + + #[test] + fn double_spaces_issue_949() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.folding().write_str("BBBBBBBBBBBBB ").unwrap(); + crate::headers::rfc2047::encode("sélection", &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBB\r\n", + " =?utf-8?b?c8OpbGVjdGlvbg==?=", + ) + ); + } + + #[test] + fn double_spaces_issue_949_no_space() { + let mut s = "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ".to_owned(); + let line_len = s.len(); + + { + let mut w = EmailWriter::new(&mut s, line_len, 0, true); + w.folding().write_str("BBBBBBBBBBBBBBB").unwrap(); + crate::headers::rfc2047::encode("sélection", &mut w).unwrap(); + } + + assert_eq!( + s, + concat!( + "Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBB=?utf-8?b?cw==?=\r\n", + " =?utf-8?b?w6lsZWN0aW9u?=", + ) + ); + } +} diff --git a/vendor/email-encoding-0.4.2/src/lib.rs b/vendor/email-encoding-0.4.2/src/lib.rs new file mode 100644 index 000000000..bfae86c6d --- /dev/null +++ b/vendor/email-encoding-0.4.2/src/lib.rs @@ -0,0 +1,49 @@ +//! Low-level crate implementing various RFCs for encoding emails. +//! Used internally by [lettre]. +//! +//! [lettre]: https://crates.io/crates/lettre + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![deny( + unreachable_pub, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces, + rust_2018_idioms, + missing_docs, + rustdoc::broken_intra_doc_links, + clippy::string_add, + clippy::string_add_assign, + clippy::clone_on_ref_ptr, + clippy::verbose_file_reads, + clippy::unnecessary_self_imports, + clippy::implicit_clone, + clippy::mem_forget, + clippy::cast_lossless, + clippy::inefficient_to_string, + clippy::inline_always, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::manual_assert, + clippy::unnecessary_join, + clippy::wildcard_imports, + clippy::str_to_string, + clippy::empty_structs_with_brackets, + clippy::zero_sized_map_values, + clippy::manual_let_else, + clippy::semicolon_if_nothing_returned, + clippy::unnecessary_wraps, + clippy::doc_markdown, + clippy::explicit_iter_loop, + clippy::redundant_closure_for_method_calls, +// Rust 1.86: clippy::unnecessary_semicolon, +)] + +#[cfg(test)] +extern crate alloc; + +pub mod body; +pub mod headers; diff --git a/vendor/lettre-0.11.23.patch b/vendor/lettre-0.11.23.patch new file mode 100644 index 000000000..e0b4170cf --- /dev/null +++ b/vendor/lettre-0.11.23.patch @@ -0,0 +1,647 @@ +diff --git a/src/message/header/mod.rs b/src/message/header/mod.rs +--- a/src/message/header/mod.rs ++++ b/src/message/header/mod.rs +@@ -176,8 +176,7 @@ + impl HeaderName { + /// Creates a new header name + pub fn new_from_ascii(ascii: String) -> Result { +- if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' ']) +- { ++ if !ascii.is_empty() && ascii.len() <= 76 && ascii.bytes().all(valid_header_name_byte) { + Ok(Self(Cow::Owned(ascii))) + } else { + Err(InvalidHeaderName) +@@ -193,14 +192,18 @@ + let bytes = ascii.as_bytes(); + let mut i = 0; + while i < bytes.len() { +- assert!(bytes[i] != b' '); +- assert!(bytes[i] != b':'); ++ assert!(valid_header_name_byte(bytes[i])); + + i += 1; + } + + Self(Cow::Borrowed(ascii)) + } ++} ++ ++// RFC 5322 field-name is printable US-ASCII excluding colon. ++const fn valid_header_name_byte(byte: u8) -> bool { ++ byte >= 33 && byte <= 126 && byte != b':' + } + + impl Display for HeaderName { +@@ -386,6 +389,24 @@ + + use super::{HeaderName, HeaderValue, Headers, To}; + use crate::message::Mailboxes; ++ ++ #[test] ++ fn headername_constructors_agree_on_rfc_field_name_bytes() { ++ for byte in 0..=127u8 { ++ let name = format!("X-{}", char::from(byte)); ++ let expected = (33..=126).contains(&byte) && byte != b':'; ++ assert_eq!(HeaderName::new_from_ascii(name.clone()).is_ok(), expected); ++ let name = Box::leak(name.into_boxed_str()); ++ assert_eq!( ++ std::panic::catch_unwind(|| HeaderName::new_from_ascii_str(name)).is_ok(), ++ expected ++ ); ++ } ++ let valid = "X".repeat(76); ++ assert!(HeaderName::new_from_ascii(valid).is_ok()); ++ assert!(HeaderName::new_from_ascii("X".repeat(77)).is_err()); ++ assert!(HeaderName::new_from_ascii("X-Note\r\n\r\n".into()).is_err()); ++ } + + #[test] + fn valid_headername() { +diff --git a/src/transport/smtp/client/async_connection.rs b/src/transport/smtp/client/async_connection.rs +--- a/src/transport/smtp/client/async_connection.rs ++++ b/src/transport/smtp/client/async_connection.rs +@@ -1,6 +1,6 @@ + use std::{fmt::Display, net::IpAddr, time::Duration}; + +-use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; ++use futures_util::io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader}; + + #[cfg(feature = "tokio1")] + use super::async_net::AsyncTokioStream; +@@ -8,8 +8,7 @@ + use super::escape_crlf; + #[allow(deprecated)] + use super::{ +- ClientCodec, MAX_RESPONSE_BYTES, MAX_RESPONSE_LINE_BYTES, TlsParameters, +- async_net::AsyncNetworkStream, ++ ClientCodec, TlsParameters, async_net::AsyncNetworkStream, response_reader::ResponseLine, + }; + use crate::{ + Envelope, +@@ -358,45 +357,7 @@ + + /// Gets the SMTP response + pub async fn read_response(&mut self) -> Result { +- let mut buffer = String::with_capacity(100); +- let mut pre = 0; +- +- while self +- .stream +- .read_line(&mut buffer) +- .await +- .map_err(error::network)? +- > 0 +- { +- if buffer.len() - pre > MAX_RESPONSE_LINE_BYTES { +- return Err(error::response("SMTP response line too long")); +- } +- if buffer.len() > MAX_RESPONSE_BYTES { +- return Err(error::response("SMTP response too large")); +- } +- pre = buffer.len(); +- +- #[cfg(feature = "tracing")] +- tracing::debug!("<< {}", escape_crlf(&buffer)); +- match parse_response(&buffer) { +- Ok((_remaining, response)) => { +- return if response.is_positive() { +- Ok(response) +- } else { +- Err(error::code_from_response(&response)) +- }; +- } +- Err(nom::Err::Failure(e)) => { +- return Err(error::response(e.to_string())); +- } +- Err(nom::Err::Incomplete(_)) => { /* read more */ } +- Err(nom::Err::Error(e)) => { +- return Err(error::response(e.to_string())); +- } +- } +- } +- +- Err(error::response("incomplete response")) ++ read_response(&mut self.stream).await + } + + /// The X509 certificate of the server (DER encoded) +@@ -433,3 +394,107 @@ + self.stream.get_ref().certificate_chain() + } + } ++ ++async fn read_response(stream: &mut R) -> Result { ++ let mut buffer = String::with_capacity(100); ++ ++ while read_response_line(stream, &mut buffer).await? > 0 { ++ #[cfg(feature = "tracing")] ++ tracing::debug!("<< {}", escape_crlf(&buffer)); ++ match parse_response(&buffer) { ++ Ok((_remaining, response)) => { ++ return if response.is_positive() { ++ Ok(response) ++ } else { ++ Err(error::code_from_response(&response)) ++ }; ++ } ++ Err(nom::Err::Failure(e)) => { ++ return Err(error::response(e.to_string())); ++ } ++ Err(nom::Err::Incomplete(_)) => { /* read more */ } ++ Err(nom::Err::Error(e)) => { ++ return Err(error::response(e.to_string())); ++ } ++ } ++ } ++ ++ Err(error::response("incomplete response")) ++} ++ ++async fn read_response_line( ++ stream: &mut R, ++ buffer: &mut String, ++) -> Result { ++ let mut line = ResponseLine::new(buffer.len()); ++ loop { ++ let available = stream.fill_buf().await.map_err(error::network)?; ++ if available.is_empty() { ++ return line.finish(buffer); ++ } ++ let (consumed, complete) = line.extend(available)?; ++ stream.consume_unpin(consumed); ++ if complete { ++ return line.finish(buffer); ++ } ++ } ++} ++ ++#[cfg(all(test, feature = "tokio1"))] ++mod bounded_response_tests { ++ use futures_util::io::Cursor; ++ use std::{cell::Cell, rc::Rc}; ++ ++ use super::super::response_reader::tests::{ ++ Expected, FailingPeer, NoNewlinePeer, assert_expected, cases, ++ }; ++ use super::*; ++ ++ #[tokio1_crate::test(flavor = "current_thread", crate = "tokio1_crate")] ++ async fn preserves_response_contracts_at_byte_boundaries() { ++ for (input, expected) in cases() { ++ // Single-byte chunks deliberately split CRLF and multibyte UTF-8. ++ let mut stream = futures_util::io::BufReader::with_capacity(1, Cursor::new(input)); ++ assert_expected(read_response(&mut stream).await, expected); ++ } ++ } ++ ++ #[tokio1_crate::test(flavor = "current_thread", crate = "tokio1_crate")] ++ async fn rejects_newline_free_peer_before_unbounded_read() { ++ let delivered = Rc::new(Cell::new(0)); ++ let peer = NoNewlinePeer { ++ delivered: delivered.clone(), ++ }; ++ let mut stream = futures_util::io::BufReader::with_capacity(64, peer); ++ assert_expected( ++ read_response(&mut stream).await, ++ Expected::ResponseError("SMTP response line too long"), ++ ); ++ assert!(delivered.get() <= 1024); ++ } ++ ++ #[tokio1_crate::test(flavor = "current_thread", crate = "tokio1_crate")] ++ async fn leaves_next_response_buffered() { ++ let mut stream = ++ futures_util::io::BufReader::new(Cursor::new(b"250 first\r\n251 second\r\n")); ++ assert_expected( ++ read_response(&mut stream).await, ++ Expected::Positive(250, vec!["first".into()]), ++ ); ++ assert_expected( ++ read_response(&mut stream).await, ++ Expected::Positive(251, vec!["second".into()]), ++ ); ++ } ++ #[tokio1_crate::test(flavor = "current_thread", crate = "tokio1_crate")] ++ async fn preserves_network_errors() { ++ let mut stream = futures_util::io::BufReader::new(FailingPeer); ++ let error = read_response(&mut stream).await.unwrap_err(); ++ assert!(!error.is_response()); ++ let source = std::error::Error::source(&error).unwrap(); ++ assert_eq!( ++ source.downcast_ref::().unwrap().kind(), ++ std::io::ErrorKind::ConnectionReset ++ ); ++ } ++} +diff --git a/src/transport/smtp/client/connection.rs b/src/transport/smtp/client/connection.rs +--- a/src/transport/smtp/client/connection.rs ++++ b/src/transport/smtp/client/connection.rs +@@ -7,9 +7,7 @@ + + #[cfg(feature = "tracing")] + use super::escape_crlf; +-use super::{ +- ClientCodec, MAX_RESPONSE_BYTES, MAX_RESPONSE_LINE_BYTES, NetworkStream, TlsParameters, +-}; ++use super::{ClientCodec, NetworkStream, TlsParameters, response_reader::ResponseLine}; + use crate::{ + address::Envelope, + transport::smtp::{ +@@ -283,39 +281,7 @@ + + /// Gets the SMTP response + pub fn read_response(&mut self) -> Result { +- let mut buffer = String::with_capacity(100); +- let mut pre = 0; +- +- while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 { +- if buffer.len() - pre > MAX_RESPONSE_LINE_BYTES { +- return Err(error::response("SMTP response line too long")); +- } +- if buffer.len() > MAX_RESPONSE_BYTES { +- return Err(error::response("SMTP response too large")); +- } +- pre = buffer.len(); +- +- #[cfg(feature = "tracing")] +- tracing::debug!("<< {}", escape_crlf(&buffer)); +- match parse_response(&buffer) { +- Ok((_remaining, response)) => { +- return if response.is_positive() { +- Ok(response) +- } else { +- Err(error::code_from_response(&response)) +- }; +- } +- Err(nom::Err::Failure(e)) => { +- return Err(error::response(e.to_string())); +- } +- Err(nom::Err::Incomplete(_)) => { /* read more */ } +- Err(nom::Err::Error(e)) => { +- return Err(error::response(e.to_string())); +- } +- } +- } +- +- Err(error::response("incomplete response")) ++ read_response(&mut self.stream) + } + + /// The X509 certificate of the server (DER encoded) +@@ -352,3 +318,102 @@ + self.stream.get_ref().certificate_chain() + } + } ++ ++fn read_response(stream: &mut R) -> Result { ++ let mut buffer = String::with_capacity(100); ++ ++ while read_response_line(stream, &mut buffer)? > 0 { ++ #[cfg(feature = "tracing")] ++ tracing::debug!("<< {}", escape_crlf(&buffer)); ++ match parse_response(&buffer) { ++ Ok((_remaining, response)) => { ++ return if response.is_positive() { ++ Ok(response) ++ } else { ++ Err(error::code_from_response(&response)) ++ }; ++ } ++ Err(nom::Err::Failure(e)) => { ++ return Err(error::response(e.to_string())); ++ } ++ Err(nom::Err::Incomplete(_)) => { /* read more */ } ++ Err(nom::Err::Error(e)) => { ++ return Err(error::response(e.to_string())); ++ } ++ } ++ } ++ ++ Err(error::response("incomplete response")) ++} ++ ++fn read_response_line(stream: &mut R, buffer: &mut String) -> Result { ++ let mut line = ResponseLine::new(buffer.len()); ++ loop { ++ let available = stream.fill_buf().map_err(error::network)?; ++ if available.is_empty() { ++ return line.finish(buffer); ++ } ++ let (consumed, complete) = line.extend(available)?; ++ stream.consume(consumed); ++ if complete { ++ return line.finish(buffer); ++ } ++ } ++} ++ ++#[cfg(test)] ++mod bounded_response_tests { ++ use std::{cell::Cell, io::Cursor, rc::Rc}; ++ ++ use super::super::response_reader::tests::{ ++ Expected, FailingPeer, NoNewlinePeer, assert_expected, cases, ++ }; ++ use super::*; ++ ++ #[test] ++ fn preserves_response_contracts_at_byte_boundaries() { ++ for (input, expected) in cases() { ++ // Single-byte chunks deliberately split CRLF and multibyte UTF-8. ++ let mut stream = std::io::BufReader::with_capacity(1, Cursor::new(input)); ++ assert_expected(read_response(&mut stream), expected); ++ } ++ } ++ ++ #[test] ++ fn rejects_newline_free_peer_before_unbounded_read() { ++ let delivered = Rc::new(Cell::new(0)); ++ let peer = NoNewlinePeer { ++ delivered: delivered.clone(), ++ }; ++ let mut stream = std::io::BufReader::with_capacity(64, peer); ++ assert_expected( ++ read_response(&mut stream), ++ Expected::ResponseError("SMTP response line too long"), ++ ); ++ assert!(delivered.get() <= 1024); ++ } ++ ++ #[test] ++ fn leaves_next_response_buffered() { ++ let mut stream = std::io::BufReader::new(Cursor::new(b"250 first\r\n251 second\r\n")); ++ assert_expected( ++ read_response(&mut stream), ++ Expected::Positive(250, vec!["first".into()]), ++ ); ++ assert_expected( ++ read_response(&mut stream), ++ Expected::Positive(251, vec!["second".into()]), ++ ); ++ } ++ #[test] ++ fn preserves_network_errors() { ++ let mut stream = std::io::BufReader::new(FailingPeer); ++ let error = read_response(&mut stream).unwrap_err(); ++ assert!(!error.is_response()); ++ let source = std::error::Error::source(&error).unwrap(); ++ assert_eq!( ++ source.downcast_ref::().unwrap().kind(), ++ std::io::ErrorKind::ConnectionReset ++ ); ++ } ++} +diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs +--- a/src/transport/smtp/client/mod.rs ++++ b/src/transport/smtp/client/mod.rs +@@ -48,6 +48,198 @@ + mod async_net; + mod connection; + mod net; ++mod response_reader { ++ use std::io; ++ ++ use super::{MAX_RESPONSE_BYTES, MAX_RESPONSE_LINE_BYTES}; ++ use crate::transport::smtp::{error, error::Error}; ++ ++ /// Accumulates one line without retaining bytes beyond either response cap. ++ pub(super) struct ResponseLine { ++ bytes: Vec, ++ response_bytes: usize, ++ } ++ ++ impl ResponseLine { ++ pub(super) fn new(response_bytes: usize) -> Self { ++ Self { ++ bytes: Vec::new(), ++ response_bytes, ++ } ++ } ++ ++ pub(super) fn extend(&mut self, available: &[u8]) -> Result<(usize, bool), Error> { ++ let newline = available.iter().position(|&byte| byte == b'\n'); ++ let consumed = newline.map_or(available.len(), |position| position + 1); ++ if consumed > MAX_RESPONSE_LINE_BYTES - self.bytes.len() { ++ return Err(error::response("SMTP response line too long")); ++ } ++ if consumed > MAX_RESPONSE_BYTES - self.response_bytes - self.bytes.len() { ++ return Err(error::response("SMTP response too large")); ++ } ++ self.bytes.extend_from_slice(&available[..consumed]); ++ Ok((consumed, newline.is_some())) ++ } ++ ++ pub(super) fn finish(self, buffer: &mut String) -> Result { ++ // Validate after the line is assembled: reads can split UTF-8 code points. ++ let line = std::str::from_utf8(&self.bytes).map_err(|error| { ++ error::network(io::Error::new(io::ErrorKind::InvalidData, error)) ++ })?; ++ buffer.push_str(line); ++ Ok(self.bytes.len()) ++ } ++ } ++ ++ #[cfg(test)] ++ pub(super) mod tests { ++ use std::{cell::Cell, io, rc::Rc}; ++ ++ use super::*; ++ use crate::transport::smtp::response::Response; ++ ++ pub(crate) enum Expected { ++ Positive(u16, Vec), ++ Transient, ++ Permanent, ++ ResponseError(&'static str), ++ NetworkError, ++ } ++ ++ pub(crate) fn cases() -> Vec<(Vec, Expected)> { ++ let exact_line = format!("250 {}\r\n", "x".repeat(MAX_RESPONSE_LINE_BYTES - 6)); ++ let continuation = format!("250-{}\r\n", "x".repeat(MAX_RESPONSE_LINE_BYTES - 6)); ++ let exact_total = format!("{}{}", continuation.repeat(99), exact_line); ++ vec![ ++ ( ++ b"250 hello\r\n".to_vec(), ++ Expected::Positive(250, vec!["hello".into()]), ++ ), ++ ( ++ "250-héllo\r\n250 世界\r\n".as_bytes().to_vec(), ++ Expected::Positive(250, vec!["héllo".into(), "世界".into()]), ++ ), ++ ( ++ exact_line.into_bytes(), ++ Expected::Positive(250, vec!["x".repeat(994)]), ++ ), ++ ( ++ exact_total.into_bytes(), ++ Expected::Positive(250, vec!["x".repeat(994); 100]), ++ ), ++ ( ++ format!("250 {}\r\n", "x".repeat(995)).into_bytes(), ++ Expected::ResponseError("SMTP response line too long"), ++ ), ++ ( ++ format!("{}250 ok\r\n", continuation.repeat(100)).into_bytes(), ++ Expected::ResponseError("SMTP response too large"), ++ ), ++ ( ++ b"250-partial\r\n".to_vec(), ++ Expected::ResponseError("incomplete response"), ++ ), ++ ( ++ b"250 partial".to_vec(), ++ Expected::ResponseError("incomplete response"), ++ ), ++ (vec![], Expected::ResponseError("incomplete response")), ++ (b"550 rejected\r\n".to_vec(), Expected::Permanent), ++ (b"451 later\r\n".to_vec(), Expected::Transient), ++ (b"invalid\r\n".to_vec(), Expected::ResponseError("")), ++ (b"250 \xff\r\n".to_vec(), Expected::NetworkError), ++ (b"250 \xc3".to_vec(), Expected::NetworkError), ++ ] ++ } ++ ++ pub(crate) fn assert_expected(result: Result, expected: Expected) { ++ match expected { ++ Expected::Positive(code, lines) => { ++ let response = result.unwrap(); ++ assert!(response.has_code(code)); ++ assert_eq!(response.message().collect::>(), lines); ++ } ++ Expected::Transient => assert!(result.unwrap_err().is_transient()), ++ Expected::Permanent => assert!(result.unwrap_err().is_permanent()), ++ Expected::ResponseError(message) => { ++ let error = result.unwrap_err(); ++ assert!(error.is_response(), "{error:?}"); ++ assert!(error.to_string().contains(message), "{error:?}"); ++ } ++ Expected::NetworkError => { ++ let error = result.unwrap_err(); ++ assert!(!error.is_response(), "{error:?}"); ++ let source = std::error::Error::source(&error).unwrap(); ++ assert_eq!( ++ source.downcast_ref::().unwrap().kind(), ++ io::ErrorKind::InvalidData ++ ); ++ } ++ } ++ } ++ ++ // Fixed read budget makes the regression fail promptly on the original reader, ++ // instead of consuming unbounded memory or waiting forever for a newline. ++ pub(crate) struct NoNewlinePeer { ++ pub(crate) delivered: Rc>, ++ } ++ ++ impl io::Read for NoNewlinePeer { ++ fn read(&mut self, output: &mut [u8]) -> io::Result { ++ let count = output.len().min(64); ++ assert!( ++ self.delivered.get() + count <= 1088, ++ "reader passed its bounded byte budget" ++ ); ++ output[..count].fill(b'x'); ++ self.delivered.set(self.delivered.get() + count); ++ Ok(count) ++ } ++ } ++ ++ #[cfg(any(feature = "tokio1", feature = "async-std1"))] ++ impl futures_io::AsyncRead for NoNewlinePeer { ++ fn poll_read( ++ mut self: std::pin::Pin<&mut Self>, ++ _: &mut std::task::Context<'_>, ++ output: &mut [u8], ++ ) -> std::task::Poll> { ++ std::task::Poll::Ready(io::Read::read(&mut *self, output)) ++ } ++ } ++ ++ pub(crate) struct FailingPeer; ++ ++ impl io::Read for FailingPeer { ++ fn read(&mut self, _: &mut [u8]) -> io::Result { ++ Err(io::Error::new(io::ErrorKind::ConnectionReset, "test reset")) ++ } ++ } ++ ++ #[cfg(any(feature = "tokio1", feature = "async-std1"))] ++ impl futures_io::AsyncRead for FailingPeer { ++ fn poll_read( ++ mut self: std::pin::Pin<&mut Self>, ++ _: &mut std::task::Context<'_>, ++ output: &mut [u8], ++ ) -> std::task::Poll> { ++ std::task::Poll::Ready(io::Read::read(&mut *self, output)) ++ } ++ } ++ ++ #[test] ++ fn rejected_bytes_are_never_retained() { ++ let mut line = ResponseLine::new(0); ++ line.extend(&vec![b'x'; MAX_RESPONSE_LINE_BYTES]).unwrap(); ++ assert!(line.extend(b"x").is_err()); ++ assert_eq!(line.bytes.len(), MAX_RESPONSE_LINE_BYTES); ++ let mut line = ResponseLine::new(MAX_RESPONSE_BYTES - 1); ++ line.extend(b"x").unwrap(); ++ assert!(line.extend(b"x").is_err()); ++ assert_eq!(line.bytes.len(), 1); ++ } ++ } ++} + mod tls; + + /// Total bytes cap on an SMTP response (Postfix `smtp_response_limit`). +diff --git a/src/transport/smtp/extension.rs b/src/transport/smtp/extension.rs +--- a/src/transport/smtp/extension.rs ++++ b/src/transport/smtp/extension.rs +@@ -141,7 +141,10 @@ + } + + let mut split = line.split_whitespace(); +- match split.next().unwrap() { ++ let Some(keyword) = split.next() else { ++ return Err(error::response("Could not read server capability")); ++ }; ++ match keyword { + "8BITMIME" => { + features.insert(Extension::EightBitMime); + } +@@ -294,6 +297,36 @@ + use crate::transport::smtp::response::{Category, Code, Detail, Severity}; + + #[test] ++ fn whitespace_only_ehlo_capability_returns_response_error() { ++ for whitespace in [" ", "\t", " \t "] { ++ let wire = format!("250-localhost\r\n250 {whitespace}\r\n"); ++ let response: Response = wire.parse().unwrap(); ++ let error = ServerInfo::from_response(&response).unwrap_err(); ++ assert!(error.is_response()); ++ assert!( ++ error ++ .to_string() ++ .contains("Could not read server capability") ++ ); ++ } ++ } ++ ++ #[test] ++ fn ehlo_parser_preserves_empty_lines_and_known_capabilities() { ++ let response: Response = "250-localhost\r\n250-\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN XOAUTH2 OTHER\r\n250-8BITMIME\r\n250 SMTPUTF8\r\n" ++ .parse() ++ .unwrap(); ++ let info = ServerInfo::from_response(&response).unwrap(); ++ assert_eq!(info.name(), "localhost"); ++ assert!(info.supports_feature(Extension::StartTls)); ++ assert!(info.supports_feature(Extension::EightBitMime)); ++ assert!(info.supports_feature(Extension::SmtpUtfEight)); ++ assert!(info.supports_auth_mechanism(Mechanism::Plain)); ++ assert!(info.supports_auth_mechanism(Mechanism::Login)); ++ assert!(info.supports_auth_mechanism(Mechanism::Xoauth2)); ++ } ++ ++ #[test] + fn test_clientid_fmt() { + assert_eq!( + format!("{}", ClientId::Domain("test".to_owned())), diff --git a/vendor/lettre-0.11.23/.cargo_vcs_info.json b/vendor/lettre-0.11.23/.cargo_vcs_info.json new file mode 100644 index 000000000..a64649164 --- /dev/null +++ b/vendor/lettre-0.11.23/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "eb6136a06f1aba9432f73ef8f02a06aa0afc8f9c" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/lettre-0.11.23/.clog.toml b/vendor/lettre-0.11.23/.clog.toml new file mode 100644 index 000000000..0e163e8ed --- /dev/null +++ b/vendor/lettre-0.11.23/.clog.toml @@ -0,0 +1,7 @@ +[clog] +repository = "https://github.com/lettre/lettre" +changelog = "CHANGELOG.md" + +[sections] +Style = ["style"] +Documentation = ["docs"] diff --git a/vendor/lettre-0.11.23/.editorconfig b/vendor/lettre-0.11.23/.editorconfig new file mode 100644 index 000000000..730edaf7f --- /dev/null +++ b/vendor/lettre-0.11.23/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +insert_final_newline = true + +[*.rs] +indent_size = 4 +indent_style = space diff --git a/vendor/lettre-0.11.23/.github/FUNDING.yml b/vendor/lettre-0.11.23/.github/FUNDING.yml new file mode 100644 index 000000000..ec9215657 --- /dev/null +++ b/vendor/lettre-0.11.23/.github/FUNDING.yml @@ -0,0 +1 @@ +liberapay: amousset diff --git a/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Bug_report.md b/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 000000000..be3f6caef --- /dev/null +++ b/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Code allowing to reproduce the bug. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** +- Lettre version +- OS + +**Additional context** +Add any other context about the problem here. diff --git a/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Feature_request.md b/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 000000000..538429512 --- /dev/null +++ b/vendor/lettre-0.11.23/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/vendor/lettre-0.11.23/.github/workflows/audit.yml b/vendor/lettre-0.11.23/.github/workflows/audit.yml new file mode 100644 index 000000000..8fd845846 --- /dev/null +++ b/vendor/lettre-0.11.23/.github/workflows/audit.yml @@ -0,0 +1,12 @@ +name: Security audit +on: + schedule: + - cron: '0 0 * * *' +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/vendor/lettre-0.11.23/.github/workflows/test.yml b/vendor/lettre-0.11.23/.github/workflows/test.yml new file mode 100644 index 000000000..67b0a9c2f --- /dev/null +++ b/vendor/lettre-0.11.23/.github/workflows/test.yml @@ -0,0 +1,151 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +env: + RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch" + RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch" + RUST_BACKTRACE: full + +jobs: + rustfmt: + name: rustfmt / nightly-2025-03-20 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install rust + run: | + rustup default nightly-2025-03-20 + rustup component add rustfmt + + - name: cargo fmt + run: cargo fmt --all -- --check + + clippy: + name: clippy / stable + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install rust + run: | + rustup update --no-self-update stable + rustup component add clippy + + - name: Run clippy + run: cargo clippy --all-features --all-targets -- -D warnings + + check: + name: check / stable + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install rust + run: rustup update --no-self-update stable + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo hack + run: cargo install cargo-hack --debug + + - name: Check with cargo hack + run: cargo hack check --feature-powerset --depth 3 --at-least-one-of aws-lc-rs,ring,rustls-no-provider --at-least-one-of rustls-native-certs,webpki-roots + + test: + name: test / ${{ matrix.name }} + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - name: stable + rust: stable + - name: beta + rust: beta + - name: '1.85' + rust: '1.85' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install rust + run: | + rustup default ${{ matrix.rust }} + rustup update --no-self-update ${{ matrix.rust }} + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install postfix + run: | + DEBIAN_FRONTEND=noninteractive sudo apt-get update + DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix + + - name: Run SMTP server + run: smtp-sink 2525 1000& + + - name: Install coredns + run: | + wget -q https://github.com/coredns/coredns/releases/download/v1.14.2/coredns_1.14.2_linux_amd64.tgz + tar xzf coredns_1.14.2_linux_amd64.tgz + + - name: Start coredns + run: | + sudo ./coredns -conf testdata/coredns.conf & + sudo systemctl stop systemd-resolved + echo "nameserver 127.0.0.54" | sudo tee /etc/resolv.conf + + - name: Install dkimverify + run: sudo apt -y install python3-dkim + + - name: Test with no default features + run: cargo test --no-default-features + + - name: Test with default features + run: cargo test + + - name: Test with all features (-native-tls) + run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls,tracing + + - name: Test with all features (-boring-tls) + run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls,tracing + +# coverage: +# name: Coverage +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v6 +# - uses: actions-rs/toolchain@v1 +# with: +# toolchain: nightly +# override: true +# - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix +# - run: smtp-sink 2525 1000& +# - uses: actions-rs/cargo@v1 +# with: +# command: test +# args: --no-fail-fast +# env: +# CARGO_INCREMENTAL: "0" +# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads" +# - id: coverage +# uses: actions-rs/grcov@v0.1 +# - name: Coveralls upload +# uses: coverallsapp/github-action@master +# with: +# github-token: ${{ secrets.GITHUB_TOKEN }} +# path-to-lcov: ${{ steps.coverage.outputs.report }} diff --git a/vendor/lettre-0.11.23/.gitignore b/vendor/lettre-0.11.23/.gitignore new file mode 100644 index 000000000..f3775e865 --- /dev/null +++ b/vendor/lettre-0.11.23/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +.project/ +.idea/ +lettre.sublime-* +lettre.iml +target/ diff --git a/vendor/lettre-0.11.23/CHANGELOG.md b/vendor/lettre-0.11.23/CHANGELOG.md new file mode 100644 index 000000000..6e886968e --- /dev/null +++ b/vendor/lettre-0.11.23/CHANGELOG.md @@ -0,0 +1,820 @@ + +### v0.11.23 (2026-08-03) + +#### Bug Fixes + +* Separate lines of multiline SMTP error replies ([#1153]) + +#### Misc + +* Upgrade `base64` to v0.23 ([#1152]) + +[#1152]: https://github.com/lettre/lettre/pull/1152 +[#1153]: https://github.com/lettre/lettre/pull/1153 + + +### v0.11.22 (2026-05-14) + +#### Security + +* Fix inverted TLS hostname verification flag in `boring-tls` backend that silently disabled hostname verification ([f5efffc]) + +#### Bug Fixes + +* Cap `read_response` buffer to prevent unbounded memory growth ([#1143]) + +#### Misc + +* Upgrade `rustls-platform-verifier` to v0.7 ([#1136]) + +[f5efffc]: https://github.com/lettre/lettre/commit/f5efffc88360dbdbfcef80f465e42d5bce68ca35 +[#1136]: https://github.com/lettre/lettre/pull/1136 +[#1143]: https://github.com/lettre/lettre/pull/1143 + + +### v0.11.21 (2026-04-04) + +#### Features + +* Add `rustls-no-provider` support ([#1134]) +* Add `message_iter` to `AsyncConnection` and `Connection` ([#1116]) + +[#1116]: https://github.com/lettre/lettre/pull/1116 +[#1134]: https://github.com/lettre/lettre/pull/1134 +[#1136]: https://github.com/lettre/lettre/pull/1136 +[#1143]: https://github.com/lettre/lettre/pull/1143 + + +### v0.11.20 (2026-03-28) + +#### Upgrade notes + +* MSRV is now 1.85 ([#1130]) + +#### Features + +* Add `Address::new_dangerous` ([#1107]) + +#### Misc + +* Replace `chumsky` with `nom` for email address parsing ([#1099]) +* Upgrade to Rust 2024 edition ([#1130]) +* Upgrade `boring` and `tokio-boring` to v5 ([#1130]) + +[#1099]: https://github.com/lettre/lettre/pull/1099 +[#1107]: https://github.com/lettre/lettre/pull/1107 +[#1130]: https://github.com/lettre/lettre/pull/1130 + + +### v0.11.19 (2025-10-08) + +#### Features + +* Add raw header setter to `MessageBuilder` ([#1108]) + +#### Misc + +* Fix README example ([#1114]) +* Replace custom `static_assert!` macro with `std::assert!` ([#1112]) + +[#1108]: https://github.com/lettre/lettre/pull/1108 +[#1112]: https://github.com/lettre/lettre/pull/1112 +[#1114]: https://github.com/lettre/lettre/pull/1114 + + +### v0.11.18 (2025-07-28) + +#### Features + +* Allow inline attachments to be named ([#1101]) + +#### Misc + +* Upgrade `socket2` to v0.6 ([#1098]) + +[#1098]: https://github.com/lettre/lettre/pull/1098 +[#1101]: https://github.com/lettre/lettre/pull/1101 + + +### v0.11.17 (2025-06-06) + +#### Features + +* Add support for `rustls-platform-verifier` ([#1081]) + +#### Misc + +* Change readme example to use `Mailbox::new` instead of string parsing ([#1090]) +* Replace futures-util `Mutex` with std `Mutex` in `AsyncStubTransport` ([#1091]) +* Avoid duplicate `abort_concurrent` implementation ([#1092]) + +[#1081]: https://github.com/lettre/lettre/pull/1081 +[#1090]: https://github.com/lettre/lettre/pull/1090 +[#1091]: https://github.com/lettre/lettre/pull/1091 +[#1092]: https://github.com/lettre/lettre/pull/1092 + + +### v0.11.16 (2025-05-12) + +#### Features + +* Always implement `Clone` for `AsyncFileTransport` ([#1075]) + +#### Changes + +* `Tls`, `CertificateStore`, `TlsParameters`, `TlsParametersBuilder`, `Certificate` and `Identity` + are now marked as deprecated when no TLS backend is enabled. They will be properly feature gated + in lettre v0.12 ([#1084]) + +#### Misc + +* Gate `web-time` behind `cfg(target_arch = "wasm32")]` ([#1086]) +* Add missing `#[doc(cfg(...))]` attributes ([#1086]) +* Upgrade `webpki-roots` to v1 ([#1088]) +* Cleanup internal `TlsParameters` and `(Async)NetworkStream` structures ([#1082]) +* Feature gate internal `TransportBuilder::tls` to avoid recursive call site warnings ([#1083]) +* Fix workaround for embedding cargo script in rustdoc output ([#1077]) +* Fix `clippy::io_other_error` warnings ([#1078]) +* Upgrade semver compatible dependencies ([#1076], [#1079], [#1080]) + +[#1075]: https://github.com/lettre/lettre/pull/1075 +[#1076]: https://github.com/lettre/lettre/pull/1076 +[#1077]: https://github.com/lettre/lettre/pull/1077 +[#1078]: https://github.com/lettre/lettre/pull/1078 +[#1079]: https://github.com/lettre/lettre/pull/1079 +[#1080]: https://github.com/lettre/lettre/pull/1080 +[#1082]: https://github.com/lettre/lettre/pull/1082 +[#1083]: https://github.com/lettre/lettre/pull/1083 +[#1084]: https://github.com/lettre/lettre/pull/1084 +[#1086]: https://github.com/lettre/lettre/pull/1086 +[#1088]: https://github.com/lettre/lettre/pull/1088 + + +### v0.11.15 (2025-03-10) + +#### Upgrade notes + +* MSRV is now 1.74 ([#1060]) + +#### Features + +* Add controlled shutdown methods ([#1045], [#1068]) + +#### Misc + +* Deny `unreachable_pub` lint ([#1058]) +* Bump minimum supported `rustls` ([#1063]) +* Bump minimum supported `serde` ([#1064]) +* Upgrade semver compatible dependencies ([#1067]) +* Upgrade `email-encoding` to v0.4 ([#1069]) + +[#1045]: https://github.com/lettre/lettre/pull/1045 +[#1058]: https://github.com/lettre/lettre/pull/1058 +[#1060]: https://github.com/lettre/lettre/pull/1060 +[#1063]: https://github.com/lettre/lettre/pull/1063 +[#1064]: https://github.com/lettre/lettre/pull/1064 +[#1067]: https://github.com/lettre/lettre/pull/1067 +[#1068]: https://github.com/lettre/lettre/pull/1068 +[#1069]: https://github.com/lettre/lettre/pull/1069 + + +### v0.11.14 (2025-02-23) + +This release deprecates the `rustls-tls`, `tokio1-rustls-tls` and `async-std1-rustls-tls` +features, which will be removed in lettre v0.12. + +rustls users should start migrating to the `rustls`, `tokio1-rustls` and +`async-std1-rustls` features. Unlike the deprecated _*rustls-tls_ features, +which automatically enabled the `ring` and `webpki-roots` backends, the new +features do not. To complete the migration, users must either enable the +`aws-lc-rs` or the `ring` feature. Additionally, those who rely on `webpki-roots` +for TLS certificate verification must now explicitly enable its feature. +Users of `rustls-native-certs` do not need to enable `webpki-roots`. + +Find out more about the new features via the [lettre rustls docs]. + +#### Features + +* Make it possible to use different `rustls` crypto providers and TLS verifiers ([#1054]) + +#### Bug fixes + +* Use the same `rustls` crypto provider everywhere ([#1055]) + +#### Misc + +* Deprecate `AsyncNetworkStream` being public ([#1059]) +* Upgrade `nom` to v8 ([#1048]) +* Drop `rustls-pemfile` in favor of `rustls-pki-types` APIs ([#1050]) +* Ban direct use of `std::time::SystemTime::now` via clippy ([#1043]) +* Drop direct dependency on `rustls-pki-types` ([#1051]) +* Remove artifact from `web-time` refactor ([#1049]) +* Fix warnings with `rustls-native-certs` when `tracing` is disabled ([#1053]) +* Bump license year ([#1057]) +* Cleanup `Cargo.toml` style ([#1047]) + +[lettre rustls docs]: https://docs.rs/lettre/0.11.14/lettre/index.html#smtp-over-tls-via-the-rustls-crate +[#1043]: https://github.com/lettre/lettre/pull/1043 +[#1047]: https://github.com/lettre/lettre/pull/1047 +[#1048]: https://github.com/lettre/lettre/pull/1048 +[#1049]: https://github.com/lettre/lettre/pull/1049 +[#1050]: https://github.com/lettre/lettre/pull/1050 +[#1051]: https://github.com/lettre/lettre/pull/1051 +[#1053]: https://github.com/lettre/lettre/pull/1053 +[#1054]: https://github.com/lettre/lettre/pull/1054 +[#1055]: https://github.com/lettre/lettre/pull/1055 +[#1057]: https://github.com/lettre/lettre/pull/1057 +[#1059]: https://github.com/lettre/lettre/pull/1059 + + +### v0.11.13 (2025-02-17) + +#### Features + +* Add WASM support ([#1037], [#1042]) +* Add method to get the TLS verify result with BoringSSL ([#1039]) + +#### Bug fixes + +* Synchronous pool shutdowns being arbitrarily delayed ([#1041]) + +[#1037]: https://github.com/lettre/lettre/pull/1037 +[#1039]: https://github.com/lettre/lettre/pull/1039 +[#1041]: https://github.com/lettre/lettre/pull/1041 +[#1042]: https://github.com/lettre/lettre/pull/1042 + + +### v0.11.12 (2025-02-02) + +#### Misc + +* Warn against manually configuring `port` and `tls` on SMTP transport builder ([#1014]) +* Document variants of `Tls` enum ([#1015]) +* Fix rustdoc warnings ([#1016]) +* Add `ContentType::TEXT_PLAIN` to `Message` builder examples ([#1017]) +* Document `SmtpTransport` and `AsyncSmtpTransport` ([#1018]) +* Fix typo in transport builder `credentials` method ([#1019]) +* Document required system dependencies for OpenSSL ([#1030]) +* Improve docs for the `transport::smtp` module ([#1031]) +* Improve docs for smtp transport builder `from_url` ([#1032]) +* Replace `assert!` with `?` on `send` examples ([#1033]) +* Warn on more pedantic clippy lints and fix them ([#1035], [#1036]) + +[#1014]: https://github.com/lettre/lettre/pull/1014 +[#1015]: https://github.com/lettre/lettre/pull/1015 +[#1016]: https://github.com/lettre/lettre/pull/1016 +[#1017]: https://github.com/lettre/lettre/pull/1017 +[#1018]: https://github.com/lettre/lettre/pull/1018 +[#1019]: https://github.com/lettre/lettre/pull/1019 +[#1030]: https://github.com/lettre/lettre/pull/1030 +[#1031]: https://github.com/lettre/lettre/pull/1031 +[#1032]: https://github.com/lettre/lettre/pull/1032 +[#1033]: https://github.com/lettre/lettre/pull/1033 +[#1035]: https://github.com/lettre/lettre/pull/1035 +[#1036]: https://github.com/lettre/lettre/pull/1036 + + +### v0.11.11 (2024-12-05) + +#### Upgrade notes + +* MSRV is now 1.71 ([#1008]) + +#### Bug fixes + +* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012]) + +#### Misc + +* Fix clippy warnings ([#1009]) +* Fix `-Zminimal-versions` build ([#1007]) + +[#1007]: https://github.com/lettre/lettre/pull/1007 +[#1008]: https://github.com/lettre/lettre/pull/1008 +[#1009]: https://github.com/lettre/lettre/pull/1009 +[#1012]: https://github.com/lettre/lettre/pull/1012 + + +### v0.11.10 (2024-10-23) + +#### Bug fixes + +* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999]) +* Use case insensitive comparisons for matching login challenge requests ([#1000]) + +[#999]: https://github.com/lettre/lettre/pull/999 +[#1000]: https://github.com/lettre/lettre/pull/1000 + + +### v0.11.9 (2024-09-13) + +#### Bug fixes + +* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988]) +* Fix parsing `Mailbox` with trailing spaces ([#986]) + +#### Misc + +* Bump `rustls-native-certs` to v0.8 ([#992]) +* Make getting started example in readme complete ([#990]) + +[#988]: https://github.com/lettre/lettre/pull/988 +[#986]: https://github.com/lettre/lettre/pull/986 +[#990]: https://github.com/lettre/lettre/pull/990 +[#992]: https://github.com/lettre/lettre/pull/992 + + +### v0.11.8 (2024-09-03) + +#### Features + +* Add mTLS support ([#974]) +* Implement `accept_invalid_hostnames` for rustls ([#977]) +* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976]) + +#### Changes + +* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980]) + +#### Misc + +* Fix clippy warnings ([#979]) +* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981]) + +[#974]: https://github.com/lettre/lettre/pull/974 +[#976]: https://github.com/lettre/lettre/pull/976 +[#977]: https://github.com/lettre/lettre/pull/977 +[#980]: https://github.com/lettre/lettre/pull/980 +[#981]: https://github.com/lettre/lettre/pull/981 + + +### v0.11.7 (2024-04-23) + +#### Misc + +* Bump `hostname` to v0.4 ([#956]) +* Fix `tracing` message consistency ([#960]) +* Bump minimum required `rustls` to v0.23.5 ([#958]) +* Dropped use of `ref` syntax in the entire project ([#959]) + +[#956]: https://github.com/lettre/lettre/pull/956 +[#958]: https://github.com/lettre/lettre/pull/958 +[#959]: https://github.com/lettre/lettre/pull/959 +[#960]: https://github.com/lettre/lettre/pull/960 + + +### v0.11.6 (2024-03-28) + +#### Bug fixes + +* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952]) + +#### Misc + +* Updated copyright year in license ([#954]) + +[#952]: https://github.com/lettre/lettre/pull/952 +[#954]: https://github.com/lettre/lettre/pull/954 + + +### v0.11.5 (2024-03-25) + +#### Features + +* Support SMTP SASL draft login challenge ([#911]) +* Add conversion from SMTP response code to integer ([#941]) + +#### Misc + +* Upgrade `rustls` to v0.23 ([#950]) +* Bump `base64` to v0.22 ([#945]) +* Fix typos in documentation ([#943], [#944]) +* Add `Cargo.lock` ([#942]) + +[#911]: https://github.com/lettre/lettre/pull/911 +[#941]: https://github.com/lettre/lettre/pull/941 +[#942]: https://github.com/lettre/lettre/pull/942 +[#943]: https://github.com/lettre/lettre/pull/943 +[#944]: https://github.com/lettre/lettre/pull/944 +[#945]: https://github.com/lettre/lettre/pull/945 +[#950]: https://github.com/lettre/lettre/pull/950 + + +### v0.11.4 (2024-01-28) + +#### Bug fixes + +* Percent decode credentials in SMTP connect URL ([#932], [#934]) +* Fix mimebody DKIM body-hash computation ([#923]) + +[#923]: https://github.com/lettre/lettre/pull/923 +[#932]: https://github.com/lettre/lettre/pull/932 +[#934]: https://github.com/lettre/lettre/pull/934 + + +### v0.11.3 (2024-01-02) + +#### Features + +* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924]) +* Derive `Debug` for `SmtpTransport` ([#925]) + +#### Misc + +* Upgrade `rustls` to v0.22 ([#921]) +* Drop once_cell dependency in favor of OnceLock from std ([#928]) + +[#921]: https://github.com/lettre/lettre/pull/921 +[#924]: https://github.com/lettre/lettre/pull/924 +[#925]: https://github.com/lettre/lettre/pull/925 +[#928]: https://github.com/lettre/lettre/pull/928 + + +### v0.11.2 (2023-11-23) + +#### Upgrade notes + +* MSRV is now 1.70 ([#916]) + +#### Misc + +* Bump `idna` to v0.5 ([#918]) +* Bump `boring` and `tokio-boring` to v4 ([#915]) + +[#915]: https://github.com/lettre/lettre/pull/915 +[#916]: https://github.com/lettre/lettre/pull/916 +[#918]: https://github.com/lettre/lettre/pull/918 + + +### v0.11.1 (2023-10-24) + +#### Bug fixes + +* Fix `webpki-roots` certificate store setup ([#909]) + +[#909]: https://github.com/lettre/lettre/pull/909 + + +### v0.11.0 (2023-10-15) + +While this release technically contains breaking changes, we expect most projects +to be able to upgrade by only bumping the version in `Cargo.toml`. + +#### Upgrade notes + +* MSRV is now 1.65 ([#869] and [#881]) +* `AddressError` is now marked as `#[non_exhaustive]` ([#839]) + +#### Features + +* Improve mailbox parsing ([#839]) +* Add construction of SMTP transport from URL ([#901]) +* Add `From
` implementation for `Mailbox` ([#879]) + +#### Misc + +* Bump `socket2` to v0.5 ([#868]) +* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882]) +* Bump `webpki-roots` to v0.25 ([#884] and [#890]) +* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896]) +* Bump `boring`ssl crates to v3 ([#897]) + +[#839]: https://github.com/lettre/lettre/pull/839 +[#868]: https://github.com/lettre/lettre/pull/868 +[#869]: https://github.com/lettre/lettre/pull/869 +[#879]: https://github.com/lettre/lettre/pull/879 +[#881]: https://github.com/lettre/lettre/pull/881 +[#882]: https://github.com/lettre/lettre/pull/882 +[#884]: https://github.com/lettre/lettre/pull/884 +[#890]: https://github.com/lettre/lettre/pull/890 +[#896]: https://github.com/lettre/lettre/pull/896 +[#897]: https://github.com/lettre/lettre/pull/897 +[#901]: https://github.com/lettre/lettre/pull/901 + + +### v0.10.4 (2023-04-02) + +#### Misc + +* Bumped rustls to 0.21 and all related dependencies ([#867]) + +[#867]: https://github.com/lettre/lettre/pull/867 + + +### v0.10.3 (2023-02-20) + +#### Announcements + +It was found that what had been used until now as a basic lettre 0.10 +`MessageBuilder::body` example failed to mention that for maximum +compatibility with various email clients a `Content-Type` header +should always be present in the message. + +##### Before + +```rust +Message::builder() + // [...] some headers skipped for brevity + .body(String::from("A plaintext or html body"))? +``` + +##### Patch + +```diff + Message::builder() + // [...] some headers skipped for brevity ++ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html + .body(String::from("A plaintext or html body"))? +``` + +#### Features + +* Add support for rustls-native-certs when using rustls ([#843]) + +[#843]: https://github.com/lettre/lettre/pull/843 + + +### v0.10.2 (2023-01-29) + +#### Upgrade notes + +* MSRV is now 1.60 ([#828]) + +#### Features + +* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805]) +* Return whole SMTP error message ([#821]) + +#### Bug fixes + +* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827]) +* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837]) + +#### Misc + +* Increase tracing ([#848]) +* Bump `idna` to 0.3 ([#816]) +* Update `base64` to 0.21 ([#840] and [#851]) +* Update `rsa` to 0.8 ([#829] and [#852]) + +[#805]: https://github.com/lettre/lettre/pull/805 +[#816]: https://github.com/lettre/lettre/pull/816 +[#821]: https://github.com/lettre/lettre/pull/821 +[#827]: https://github.com/lettre/lettre/pull/827 +[#828]: https://github.com/lettre/lettre/pull/828 +[#829]: https://github.com/lettre/lettre/pull/829 +[#837]: https://github.com/lettre/lettre/pull/837 +[#840]: https://github.com/lettre/lettre/pull/840 +[#848]: https://github.com/lettre/lettre/pull/848 +[#851]: https://github.com/lettre/lettre/pull/851 +[#852]: https://github.com/lettre/lettre/pull/852 + + +### v0.10.1 (2022-07-20) + +#### Features + +* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798]) +* Make the minimum TLS version configurable. ([#799]) ([#800]) + +#### Bug Fixes + +* Ensure connections are closed on abort. ([#801]) +* Fix SMTP dot stuffing. ([#803]) + +[#797]: https://github.com/lettre/lettre/pull/797 +[#798]: https://github.com/lettre/lettre/pull/798 +[#799]: https://github.com/lettre/lettre/pull/799 +[#800]: https://github.com/lettre/lettre/pull/800 +[#801]: https://github.com/lettre/lettre/pull/801 +[#803]: https://github.com/lettre/lettre/pull/803 + + +### v0.10.0 (2022-06-29) + +#### Upgrade notes + +Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward: + +* MSRV is now 1.56.0 +* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message` + and make sure to enable the `builder` feature (it's enabled by default). +* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate, + rename `SendableEmail` to `Email`. +* The `serde-impls` feature has been renamed to `serde`. To migrate, rename the feature. + +#### Features + +* Add `tokio` 1 support +* Add `rustls` support +* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport. +* Allow enabling multiple SMTP authentication mechanisms +* Allow providing a custom message id +* Allow sending raw emails + +#### Breaking Changes + +* Merge `lettre_email` into `lettre` +* Merge `Email` and `SendableEmail` into `lettre::message::Email` +* SmtpTransport is now an high level SMTP client. It provides connection pooling and shortcuts for building clients using commonly desired values +* Refactor `TlsParameters` implementation to not expose the internal TLS library +* `FileTransport` writes emails into `.eml` instead of `.json` +* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility) +* The `sendmail` and `file` transports aren't enabled by default anymore. +* The `new` method of `ClientId` is deprecated +* Rename `serde-impls` feature to `serde` +* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of + `/usr/bin/sendmail`. + +#### Bug Fixes + +* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md)) +* Correctly encode header values containing non-ASCII characters +* Timeout bug causing infinite hang +* Fix doc tests in website +* Fix docs for `domain` field + +#### Misc + +* Improve documentation, examples and tests +* Replace `line-wrap`, `email`, `bufstream` with our own implementations +* Remove `bytes` +* Remove `time` +* Remove `fast_chemail` +* Update `base64` to 0.13 +* Update `hostname` to 0.3 +* Update to `nom` 6 +* Replace `log` with `tracing` +* Move CI to GitHub Actions +* Use criterion for benchmarks + + +### v0.9.2 (2019-06-11) + +#### Bug Fixes + +* **email:** + * Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b)) + + +### v0.9.1 (2019-05-05) + +#### Features + +* **email:** + * Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9)) + + +### v0.9.0 (2019-03-17) + +#### Bug Fixes + +* **email:** + * Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69)) + * Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe)) + +* **transport:** + * Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560)) + * Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8)) + +#### Features + +* **all:** + * Update dependencies + * Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d)) + +* **transport:** + * Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb)) + * Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164)) + * Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2)) + * SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e)) + * Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34)) + * Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a)) + +* **email:** + * Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6)) + * Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9)) + + +### v0.8.2 (2018-05-03) + + +#### Bug Fixes + +* **transport:** Write timeout is not set in smtp transport ([cc3580a8](https://github.com/lettre/lettre/commit/cc3580a8942e11c2addf6677f05e16fb451c7ea0)) + +#### Style + +* **all:** Fix typos ([360c42ff](https://github.com/lettre/lettre/commit/360c42ffb8f706222eaad14e72619df1e4857814)) + +#### Features + +* **all:** + * Add set -xe option to build scripts ([57bbabaa](https://github.com/lettre/lettre/commit/57bbabaa6a10cc1a4de6f379e25babfee7adf6ad)) + * Move post-success scripts to separate files ([3177b58c](https://github.com/lettre/lettre/commit/3177b58c6d11ffae73c958713f6f0084173924e1)) + * Add website upload to travis build script ([a5294df6](https://github.com/lettre/lettre/commit/a5294df63728e14e24eeb851bb4403abd6a7bd36)) + * Add codecov upload in travis ([a03bfa00](https://github.com/lettre/lettre/commit/a03bfa008537b1d86ff789d0823e89ad5d99bd79)) + * Update README to put useful links at the top ([1ebbe660](https://github.com/lettre/lettre/commit/1ebbe660f5e142712f702c02d5d1e45211763b42)) + * Update badges in README and Cargo.toml ([f7ee5c42](https://github.com/lettre/lettre/commit/f7ee5c427ad71e4295f2f1d8e3e9e2dd850223e8)) + * Move docs from hugo to gitbook ([27935e32](https://github.com/lettre/lettre/commit/27935e32ef097db8db004569f35cad1d6cd30eca)) +* **transport:** Use md-5 and hmac instead of rust-crypto ([0cf018a8](https://github.com/lettre/lettre/commit/0cf018a85e4ea1ad16c7216670da560cc915ec32)) + + + + +### v0.8.1 (2018-04-11) + +#### Fix + +* **all:** + * Replace skeptic by some custom rustdoc invocations ([81bad131](https://github.com/lettre/lettre/commit/81bad1317519d330c46ea02f2b7a266b97cc00dd)) + +#### Documentation + +* **all:** + * Add changelog sections for style and docs ([b4d03ead](https://github.com/lettre/lettre/commit/b4d03ead8cce04e0c3d65a30e7a07acca9530f30)) + * Use clog to generate changelogs ([8981a775](https://github.com/lettre/lettre/commit/8981a7758c89be69974ef204c4390744aea94e4f), closes [#233](https://github.com/lettre/lettre/issues/233)) + +#### Style + +* **transport-smtp:** Avoid useless empty format strings ([f3271715](https://github.com/lettre/lettre/commit/f3271715ecaf2793c9064462184867e4f22b0ead)) + + + + +### v0.8.0 (2018-03-31) + +#### Added + +* Support binary files as attachment +* Move doc to a dedicated website +* Add tests for the doc using skeptic +* Added a code of conduct +* Use hostname as `ClientId` when available + +#### Changed + +* Detail in SMTP Response is now an enum +* Use nom for parsing smtp responses +* `Envelope` was moved from `lettre_email` to `lettre` +* `EmailAddress::new()` now returns a `Result` +* `SendableEmail` replaces `from` and `to` by `envelope` that returns an `Envelope` +* `File` transport storage format has changed + +#### Fixed + +* Add missing "Bcc" headers when building the email +* Specify utf-8 charset for html +* Use parts for text and html methods to work with attachments + +#### Removed + +* `get_ehlo` and `reset` in SmtpTransport are now private + + +### v0.7.0 (2017-10-08) + +#### Added + +* Allow validating server certificate +* Initial (incomplete) attachments support + +#### Changed + +* Split into the *lettre* and *lettre_email* crates +* A lot of small improvements +* Use *tls-native* instead of *openssl* in smtp transport + + +### v0.6.2 (2017-02-18) + +#### Changed + +* Update env-logger crate to 0.4 +* Update openssl crate to 0.9 +* Update uuid crate to 0.4 + + +### v0.6.1 (2016-10-19) + +#### Changes + +* **documentation** + * #91: Build separate docs for each release + * #96: Add complete documentation information to README + +#### Fixed + +* #85: Use address-list for "To", "From" etc. +* #93: Force building tests before coverage computing + + +### v0.6.0 (2016-05-05) + +#### Changes + +* multipart support +* add non-consuming methods for Email builders +* `add_header` does not return the builder anymore, + for consistency with other methods. Use the `header` + method instead diff --git a/vendor/lettre-0.11.23/CODE_OF_CONDUCT.md b/vendor/lettre-0.11.23/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ee5998d37 --- /dev/null +++ b/vendor/lettre-0.11.23/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4][version] + +[homepage]: https://www.contributor-covenant.org +[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/vendor/lettre-0.11.23/CONTRIBUTING.md b/vendor/lettre-0.11.23/CONTRIBUTING.md new file mode 100644 index 000000000..f447d86bb --- /dev/null +++ b/vendor/lettre-0.11.23/CONTRIBUTING.md @@ -0,0 +1,43 @@ +## Contributing to Lettre + +The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md). + +### Code formatting + +All code must be formatted using `rustfmt`. + +### Commit Message Format + +Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject: + +```text +():