diff --git a/Cargo.lock b/Cargo.lock index 88688d7a1..a5f50a21e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,12 +207,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "ascii_utils" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" - [[package]] name = "asn1-rs" version = "0.6.2" @@ -226,7 +220,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror 1.0.69", - "time 0.3.37", + "time", ] [[package]] @@ -487,10 +481,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.12", + "getrandom", "instant", "pin-project-lite", - "rand 0.8.5", + "rand", "tokio", ] @@ -515,12 +509,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - [[package]] name = "base64" version = "0.12.3" @@ -838,10 +826,8 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1165,7 +1151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -1177,7 +1163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -1233,7 +1219,7 @@ dependencies = [ "curve25519-dalek-derive", "digest", "fiat-crypto", - "rand_core 0.6.4", + "rand_core", "rustc_version", "serde", "subtle", @@ -1336,7 +1322,6 @@ dependencies = [ "deltachat-contact-tools", "deltachat-time", "deltachat_derive", - "email", "encoded-words", "escaper", "fast-socks5", @@ -1354,9 +1339,9 @@ dependencies = [ "iroh", "iroh-gossip", "kamadak-exif", - "lettre_email", "libc", "log", + "mail-builder", "mailparse", "mime", "nu-ansi-term", @@ -1373,7 +1358,7 @@ dependencies = [ "qrcodegen", "quick-xml", "quoted_printable", - "rand 0.8.5", + "rand", "ratelimit", "regex", "rusqlite", @@ -1499,7 +1484,7 @@ dependencies = [ "libc", "num-traits", "once_cell", - "rand 0.8.5", + "rand", "serde_json", "thiserror 2.0.11", "tokio", @@ -1784,7 +1769,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", + "rand_core", "serde", "sha2", "subtle", @@ -1812,26 +1797,12 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", ] -[[package]] -name = "email" -version = "0.0.20" -source = "git+https://github.com/deltachat/rust-email?branch=master#ba176ca31ae000203368eb9baacc7eb469fd7692" -dependencies = [ - "base64 0.11.0", - "chrono", - "encoded-words", - "encoding", - "lazy_static", - "rand 0.7.3", - "version_check", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -1859,70 +1830,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -2098,15 +2005,6 @@ dependencies = [ "tokio-stream", ] -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -2139,7 +2037,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -2395,17 +2293,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.12" @@ -2415,7 +2302,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -2467,7 +2354,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.8.5", + "rand", "smallvec", "spinning_top", ] @@ -2479,7 +2366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -2566,7 +2453,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.8.5", + "rand", "thiserror 2.0.11", "tinyvec", "tokio", @@ -2587,7 +2474,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.8.5", + "rand", "resolv-conf", "smallvec", "thiserror 2.0.11", @@ -2991,7 +2878,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.8.5", + "rand", "tokio", "url", "xmltree", @@ -3117,7 +3004,7 @@ dependencies = [ "pin-project", "pkarr", "portmapper", - "rand 0.8.5", + "rand", "rcgen", "reqwest", "ring", @@ -3149,8 +3036,8 @@ dependencies = [ "data-encoding", "derive_more", "ed25519-dalek", - "getrandom 0.2.12", - "rand_core 0.6.4", + "getrandom", + "rand_core", "serde", "thiserror 2.0.11", "url", @@ -3189,8 +3076,8 @@ dependencies = [ "iroh-blake3", "iroh-metrics", "postcard", - "rand 0.8.5", - "rand_core 0.6.4", + "rand", + "rand_core", "serde", "serde-error", "thiserror 2.0.11", @@ -3236,7 +3123,7 @@ dependencies = [ "n0-future", "netwatch", "portmapper", - "rand 0.8.5", + "rand", "reqwest", "rustls", "surge-ping", @@ -3274,8 +3161,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" dependencies = [ "bytes", - "getrandom 0.2.12", - "rand 0.8.5", + "getrandom", + "rand", "ring", "rustc-hash", "rustls", @@ -3327,7 +3214,7 @@ dependencies = [ "num_enum", "pin-project", "postcard", - "rand 0.8.5", + "rand", "reqwest", "rustls", "rustls-webpki", @@ -3448,30 +3335,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lettre" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#2ffdb5347f1255b4aed51cf87cda1f8207160f36" -dependencies = [ - "fast_chemail", - "log", -] - -[[package]] -name = "lettre_email" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#2ffdb5347f1255b4aed51cf87cda1f8207160f36" -dependencies = [ - "base64 0.11.0", - "email", - "lazy_static", - "lettre", - "mime", - "regex", - "time 0.1.45", - "uuid", -] - [[package]] name = "libc" version = "0.2.169" @@ -3581,6 +3444,11 @@ version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" +[[package]] +name = "mail-builder" +version = "0.4.1" +source = "git+https://github.com/stalwartlabs/mail-builder?branch=main#b86e558d54d43c1c1991fbda84109268815abcac" + [[package]] name = "mailparse" version = "0.16.0" @@ -3664,7 +3532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] @@ -3720,7 +3588,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.12", + "getrandom", ] [[package]] @@ -3860,7 +3728,7 @@ dependencies = [ "serde", "socket2", "thiserror 2.0.11", - "time 0.3.37", + "time", "tokio", "tokio-util", "tracing", @@ -3980,7 +3848,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand", "serde", "smallvec", "zeroize", @@ -4223,7 +4091,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core 0.6.4", + "rand_core", "sha2", ] @@ -4263,7 +4131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -4353,7 +4221,7 @@ dependencies = [ "aes-gcm", "aes-kw", "argon2", - "base64 0.21.7", + "base64 0.22.1", "bitfield", "block-padding", "blowfish", @@ -4394,7 +4262,7 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.8.5", + "rand", "ripemd", "rsa", "sha1", @@ -4621,12 +4489,12 @@ dependencies = [ "libc", "netwatch", "num_enum", - "rand 0.8.5", + "rand", "serde", "smallvec", "socket2", "thiserror 2.0.11", - "time 0.3.37", + "time", "tokio", "tokio-util", "tracing", @@ -4797,8 +4665,8 @@ dependencies = [ "bitflags 2.8.0", "lazy_static", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "rand_xorshift", "regex-syntax 0.8.2", "unarray", @@ -4836,7 +4704,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -4887,8 +4755,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom 0.2.12", - "rand 0.8.5", + "getrandom", + "rand", "ring", "rustc-hash", "rustls", @@ -4949,19 +4817,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -4969,18 +4824,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -4990,16 +4835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -5008,16 +4844,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", + "getrandom", ] [[package]] @@ -5026,7 +4853,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -5071,7 +4898,7 @@ dependencies = [ "pem", "ring", "rustls-pki-types", - "time 0.3.37", + "time", "yasna", ] @@ -5099,7 +4926,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.12", + "getrandom", "libredox", "thiserror 2.0.11", ] @@ -5224,7 +5051,7 @@ checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.12", + "getrandom", "libc", "untrusted", "windows-sys 0.52.0", @@ -5244,7 +5071,7 @@ dependencies = [ "p256", "p384", "pkcs8", - "rand_core 0.6.4", + "rand_core", "ring", "signature", ] @@ -5271,7 +5098,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", "spki", "subtle", @@ -5815,7 +5642,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project", - "rand 0.8.5", + "rand", "sendfd", "serde", "serde_json", @@ -5845,7 +5672,7 @@ dependencies = [ "chacha20poly1305", "hkdf", "md-5", - "rand 0.8.5", + "rand", "ring-compat", "sha1", ] @@ -5881,7 +5708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -6054,7 +5881,7 @@ dependencies = [ "precis-core", "precis-profiles", "quoted-string-parser", - "rand 0.8.5", + "rand", ] [[package]] @@ -6072,7 +5899,7 @@ dependencies = [ "hex", "parking_lot", "pnet_packet", - "rand 0.8.5", + "rand", "socket2", "thiserror 1.0.69", "tokio", @@ -6263,17 +6090,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.37" @@ -6664,7 +6480,7 @@ dependencies = [ "http 1.1.0", "httparse", "log", - "rand 0.8.5", + "rand", "sha1", "thiserror 1.0.69", "url", @@ -6683,7 +6499,7 @@ dependencies = [ "http 1.1.0", "httparse", "log", - "rand 0.8.5", + "rand", "sha1", "thiserror 1.0.69", "utf-8", @@ -6867,7 +6683,7 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ - "getrandom 0.2.12", + "getrandom", "serde", ] @@ -6908,18 +6724,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -7581,7 +7385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", + "rand_core", "serde", "zeroize", ] @@ -7600,7 +7404,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror 1.0.69", - "time 0.3.37", + "time", ] [[package]] @@ -7641,7 +7445,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.37", + "time", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 97eb26af2..81794de31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ brotli = { version = "7", default-features=false, features = ["std"] } bytes = "1" chrono = { workspace = true, features = ["alloc", "clock", "std"] } data-encoding = "2.7.0" -email = { git = "https://github.com/deltachat/rust-email", branch = "master" } encoded-words = "0.2" escaper = "0.1" fast-socks5 = "0.10" @@ -67,8 +66,8 @@ image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", iroh-gossip = { version = "0.32", default-features = false, features = ["net"] } iroh = { version = "0.32", default-features = false } kamadak-exif = "0.6.1" -lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = { workspace = true } +mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", branch = "main", default-features = false } mailparse = "0.16" mime = "0.3.17" num_cpus = "1.16" diff --git a/deny.toml b/deny.toml index e658fab68..85f462e69 100644 --- a/deny.toml +++ b/deny.toml @@ -1,7 +1,5 @@ [advisories] ignore = [ - "RUSTSEC-2020-0071", - # Timing attack on RSA. # Delta Chat does not use RSA for new keys # and this requires precise measurement of the decryption time by the attacker. @@ -9,9 +7,6 @@ ignore = [ # "RUSTSEC-2023-0071", - # Unmaintained encoding - "RUSTSEC-2021-0153", - # Unmaintained instant "RUSTSEC-2024-0384", @@ -32,16 +27,12 @@ skip = [ { name = "core-foundation", version = "0.9.4" }, { name = "event-listener", version = "2.5.3" }, { name = "generator", version = "0.7.5" }, - { name = "getrandom", version = "<0.2" }, { name = "http", version = "0.2.12" }, { name = "loom", version = "0.5.6" }, { name = "netlink-packet-route", version = "0.17.1" }, { name = "nix", version = "0.26.4" }, { name = "nix", version = "0.27.1" }, { name = "quick-error", version = "<2.0" }, - { name = "rand_chacha", version = "<0.3" }, - { name = "rand_core", version = "<0.6" }, - { name = "rand", version = "<0.8" }, { name = "redox_syscall", version = "0.3.5" }, { name = "regex-automata", version = "0.1.10" }, { name = "regex-syntax", version = "0.6.29" }, @@ -51,11 +42,9 @@ skip = [ { name = "syn", version = "1.0.109" }, { name = "thiserror-impl", version = "1.0.69" }, { name = "thiserror", version = "1.0.69" }, - { name = "time", version = "<0.3" }, { name = "tokio-tungstenite", version = "0.21.0" }, { name = "tungstenite", version = "0.21.0" }, { name = "unicode-width", version = "0.1.11" }, - { name = "wasi", version = "<0.11" }, { name = "windows" }, { name = "windows_aarch64_gnullvm" }, { name = "windows_aarch64_msvc" }, @@ -102,5 +91,5 @@ license-files = [ [sources.allow-org] # Organisations which we allow git sources from. github = [ - "deltachat", + "stalwartlabs", ] diff --git a/src/chat.rs b/src/chat.rs index 428b2ad2b..d0305c627 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3,6 +3,7 @@ use std::cmp; use std::collections::{HashMap, HashSet}; use std::fmt; +use std::io::Cursor; use std::marker::Sync; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -11,6 +12,7 @@ use std::time::Duration; use anyhow::{anyhow, bail, ensure, Context as _, Result}; use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress}; use deltachat_derive::{FromSql, ToSql}; +use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use tokio::task; @@ -32,7 +34,6 @@ use crate::debug_logging::maybe_set_logging_xdc; use crate::download::DownloadState; use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer}; use crate::events::EventType; -use crate::html::new_html_mimepart; use crate::location; use crate::log::LogExt; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; @@ -2157,14 +2158,18 @@ impl Chat { } else { None }; - let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string()); + let new_mime_headers: Option = new_mime_headers.map(|s| { + let html_part = MimePart::new("text/html", s); + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + html_part.write_part(cursor).ok(); + String::from_utf8_lossy(&buffer).to_string() + }); let new_mime_headers = new_mime_headers.or_else(|| match was_truncated { // We need to add some headers so that they are stripped before formatting HTML by // `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's // anyway a useful metadata about the stored text. - true => Some( - "Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n", - ), + true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text), false => None, }); let new_mime_headers = match new_mime_headers { diff --git a/src/contact.rs b/src/contact.rs index a27b8e305..57dd9144c 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -247,7 +247,16 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result + // is merged. + Ok(contact_tools::make_vcard(&vcard_contacts) + .trim_end() + .to_string()) } /// Imports contacts from the given vCard. diff --git a/src/e2ee.rs b/src/e2ee.rs index 70c835cf2..0e7af8324 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -1,6 +1,9 @@ //! End-to-end encryption support. +use std::io::Cursor; + use anyhow::{format_err, Context as _, Result}; +use mail_builder::mime::MimePart; use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; @@ -95,7 +98,7 @@ impl EncryptHelper { self, context: &Context, verified: bool, - mail_to_encrypt: lettre_email::PartBuilder, + mail_to_encrypt: MimePart<'static>, peerstates: Vec<(Option, String)>, compress: bool, ) -> Result { @@ -136,7 +139,9 @@ impl EncryptHelper { let sign_key = load_self_secret_key(context).await?; - let raw_message = mail_to_encrypt.build().as_string().into_bytes(); + let mut raw_message = Vec::new(); + let cursor = Cursor::new(&mut raw_message); + mail_to_encrypt.clone().write_part(cursor).ok(); let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?; @@ -145,15 +150,13 @@ impl EncryptHelper { /// Signs the passed-in `mail` using the private key from `context`. /// Returns the payload and the signature. - pub async fn sign( - self, - context: &Context, - mail: lettre_email::PartBuilder, - ) -> Result<(lettre_email::MimeMessage, String)> { + pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result { let sign_key = load_self_secret_key(context).await?; - let mime_message = mail.build(); - let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?; - Ok((mime_message, signature)) + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + mail.clone().write_part(cursor).ok(); + let signature = pgp::pk_calc_signature(&buffer, &sign_key)?; + Ok(signature) } } diff --git a/src/html.rs b/src/html.rs index 7c26ffb47..ad4538c2b 100644 --- a/src/html.rs +++ b/src/html.rs @@ -11,9 +11,8 @@ use std::mem; use anyhow::{Context as _, Result}; use base64::Engine as _; -use lettre_email::mime::Mime; -use lettre_email::PartBuilder; use mailparse::ParsedContentType; +use mime::Mime; use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -277,16 +276,6 @@ impl MsgId { } } -/// Wraps HTML text into a new text/html mimepart structure. -/// -/// Used on forwarding messages to avoid leaking the original mime structure -/// and also to avoid sending too much, maybe large data. -pub fn new_html_mimepart(html: String) -> PartBuilder { - PartBuilder::new() - .content_type(&"text/html; charset=utf-8".parse::().unwrap()) - .body(html) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 9bb9bc73d..b418f73a6 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,13 +1,16 @@ //! # MIME message production. use std::collections::HashSet; +use std::io::Cursor; use std::path::Path; use anyhow::{bail, Context as _, Result}; use base64::Engine as _; use chrono::TimeZone; -use email::Mailbox; -use lettre_email::{Address, Header, MimeMultipartType, PartBuilder}; +use deltachat_contact_tools::sanitize_bidi_characters; +use mail_builder::headers::address::{Address, EmailAddress}; +use mail_builder::headers::HeaderType; +use mail_builder::mime::MimePart; use tokio::fs; use crate::blob::BlobObject; @@ -18,8 +21,6 @@ use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::headerdef::HeaderDef; -use crate::html::new_html_mimepart; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; @@ -102,13 +103,8 @@ pub struct MimeFactory { loaded: Loaded, in_reply_to: String, - /// Space-separated list of Message-IDs for `References` header. - /// - /// Each Message-ID in the list - /// may or may not be enclosed in angle brackets, - /// angle brackets must be added during message rendering - /// as needed. - references: String, + /// List of Message-IDs for `References` header. + references: Vec, /// True if the message requests Message Disposition Notification /// using `Chat-Disposition-Notification-To` header. @@ -146,11 +142,15 @@ pub struct RenderedEmail { pub subject: String, } -fn new_address_with_name(name: &str, address: String) -> Address { - match name == address { - true => Address::new_mailbox(address), - false => Address::new_mailbox_with_name(name.to_string(), address), - } +fn new_address_with_name(name: &str, address: String) -> Address<'static> { + Address::new_address( + if name == address || name.is_empty() { + None + } else { + Some(name.to_string()) + }, + address, + ) } impl MimeFactory { @@ -295,13 +295,15 @@ impl MimeFactory { let in_reply_to: String = row.get(0)?; let references: String = row.get(1)?; - Ok(( - render_rfc724_mid_list(&in_reply_to), - render_rfc724_mid_list(&references), - )) + Ok((in_reply_to, references)) }, ) .await?; + let references: Vec = references + .trim() + .split_ascii_whitespace() + .map(|s| s.trim_start_matches('<').trim_end_matches('>').to_string()) + .collect(); let selfstatus = match attach_profile_data { true => context .get_config(Config::Selfstatus) @@ -361,7 +363,7 @@ impl MimeFactory { additional_msg_ids, }, in_reply_to: String::default(), - references: String::default(), + references: Vec::new(), req_mdn: false, last_added_location_id: None, sync_ids_to_delete: None, @@ -572,26 +574,32 @@ impl MimeFactory { /// Consumes a `MimeFactory` and renders it into a message which is then stored in /// `smtp`-table to be used by the SMTP loop pub async fn render(mut self, context: &Context) -> Result { - let mut headers = Vec::
::new(); + let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); let from = new_address_with_name(&self.from_displayname, self.from_addr.clone()); - let mut to = Vec::new(); + let mut to: Vec> = Vec::new(); for (name, addr) in &self.to { - if name.is_empty() { - to.push(Address::new_mailbox(addr.clone())); - } else { - to.push(new_address_with_name(name, addr.clone())); - } + to.push(Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + )); } - let mut past_members = Vec::new(); // Contents of `Chat-Group-Past-Members` header. + let mut past_members: Vec> = Vec::new(); // Contents of `Chat-Group-Past-Members` header. for (name, addr) in &self.past_members { - if name.is_empty() { - past_members.push(Address::new_mailbox(addr.clone())); - } else { - past_members.push(new_address_with_name(name, addr.clone())); - } + past_members.push(Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + )); } debug_assert!( @@ -600,26 +608,28 @@ impl MimeFactory { ); if to.is_empty() { to.push(Address::new_group( - "hidden-recipients".to_string(), + Some("hidden-recipients".to_string()), Vec::new(), )); } // Start with Internet Message Format headers in the order of the standard example // . - let from_header = Header::new_with_value("From".into(), vec![from]).unwrap(); - headers.push(from_header.clone()); + headers.push(("From", from.into())); if let Some(sender_displayname) = &self.sender_displayname { let sender = new_address_with_name(sender_displayname, self.from_addr.clone()); - headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap()); + headers.push(("Sender", sender.into())); } - headers.push(Header::new_with_value("To".into(), to.clone()).unwrap()); + headers.push(( + "To", + mail_builder::headers::address::Address::new_list(to.clone()).into(), + )); if !past_members.is_empty() { - headers.push( - Header::new_with_value("Chat-Group-Past-Members".into(), past_members.clone()) - .unwrap(), - ); + headers.push(( + "Chat-Group-Past-Members", + mail_builder::headers::address::Address::new_list(past_members.clone()).into(), + )); } if let Loaded::Message { chat, .. } = &self.loaded { @@ -627,72 +637,75 @@ impl MimeFactory { && !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? { - headers.push( - Header::new_with_value( - "Chat-Group-Member-Timestamps".into(), + headers.push(( + "Chat-Group-Member-Timestamps", + mail_builder::headers::raw::Raw::new( self.member_timestamps .iter() .map(|ts| ts.to_string()) .collect::>() .join(" "), ) - .unwrap(), - ); + .into(), + )); } } let subject_str = self.subject_str(context).await?; - let encoded_subject = if subject_str - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ') - // We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space - // but we do not want to encode all subjects just because they contain a space. - { - subject_str.clone() - } else { - encode_words(&subject_str) - }; - headers.push(Header::new("Subject".into(), encoded_subject)); + headers.push(( + "Subject", + mail_builder::headers::text::Text::new(subject_str.to_string()).into(), + )); let date = chrono::DateTime::::from_timestamp(self.timestamp, 0) .unwrap() .to_rfc2822(); - headers.push(Header::new("Date".into(), date)); + headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); let rfc724_mid = match &self.loaded { Loaded::Message { msg, .. } => msg.rfc724_mid.clone(), Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), }; - let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid); - let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue); - headers.push(rfc724_mid_header); + headers.push(( + "Message-ID", + mail_builder::headers::message_id::MessageId::new(rfc724_mid.clone()).into(), + )); // Reply headers as in . if !self.in_reply_to.is_empty() { - headers.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone())); + headers.push(( + "In-Reply-To", + mail_builder::headers::message_id::MessageId::new(self.in_reply_to.clone()).into(), + )); } if !self.references.is_empty() { - headers.push(Header::new("References".into(), self.references.clone())); + headers.push(( + "References", + mail_builder::headers::message_id::MessageId::<'static>::new_list( + self.references.iter().map(|s| s.to_string()), + ) + .into(), + )); } // Automatic Response headers if let Loaded::Mdn { .. } = self.loaded { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-replied".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(), )); } else if context.get_config_bool(Config::Bot).await? { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-generated".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(), )); } else if let Loaded::Message { msg, .. } = &self.loaded { if msg.param.get_cmd() == SystemMessage::SecurejoinMessage { let step = msg.param.get(Param::Arg).unwrap_or_default(); if step != "vg-request" && step != "vc-request" { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-replied".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(), )); } } @@ -701,25 +714,30 @@ impl MimeFactory { if let Loaded::Message { chat, .. } = &self.loaded { if chat.typ == Chattype::Broadcast { let encoded_chat_name = encode_words(&chat.name); - headers.push(Header::new( - "List-ID".into(), - format!("{encoded_chat_name} <{}>", chat.grpid), + headers.push(( + "List-ID", + mail_builder::headers::raw::Raw::new(format!( + "{encoded_chat_name} <{}>", + chat.grpid + )) + .into(), )); } } // Non-standard headers. - headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string())); + headers.push(( + "Chat-Version", + mail_builder::headers::raw::Raw::new("1.0").into(), + )); if self.req_mdn { // we use "Chat-Disposition-Notification-To" // because replies to "Disposition-Notification-To" are weird in many cases // eg. are just freetext and/or do not follow any standard. - headers.push(Header::new( - HeaderDef::ChatDispositionNotificationTo - .get_headername() - .to_string(), - self.from_addr.clone(), + headers.push(( + "Chat-Disposition-Notification-To", + mail_builder::headers::raw::Raw::new(self.from_addr.clone()).into(), )); } @@ -732,7 +750,10 @@ impl MimeFactory { if !skip_autocrypt { // unless determined otherwise we add the Autocrypt header let aheader = encrypt_helper.get_aheader().to_string(); - headers.push(Header::new("Autocrypt".into(), aheader)); + headers.push(( + "Autocrypt", + mail_builder::headers::raw::Raw::new(aheader).into(), + )); } // Add ephemeral timer for non-MDN messages. @@ -741,9 +762,9 @@ impl MimeFactory { if let Loaded::Message { msg, .. } = &self.loaded { let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?; if let EphemeralTimer::Enabled { duration } = ephemeral_timer { - headers.push(Header::new( - "Ephemeral-Timer".to_string(), - duration.to_string(), + headers.push(( + "Ephemeral-Timer", + mail_builder::headers::raw::Raw::new(duration.to_string()).into(), )); } } @@ -761,48 +782,31 @@ impl MimeFactory { false }; - let message = match &self.loaded { + let message: MimePart<'static> = match &self.loaded { Loaded::Message { msg, .. } => { let msg = msg.clone(); - let (main_part, parts) = self + let (main_part, mut parts) = self .render_message(context, &mut headers, &grpimage, is_encrypted) .await?; if parts.is_empty() { // Single part, render as regular message. main_part } else { - // Multiple parts, render as multipart. - let part_holder = if msg.param.get_cmd() == SystemMessage::MultiDeviceSync { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=multi-device-sync".to_string(), - )) - } else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=status-update".to_string(), - )) - } else { - PartBuilder::new().message_type(MimeMultipartType::Mixed) - }; + parts.insert(0, main_part); - parts - .into_iter() - .fold(part_holder.child(main_part.build()), |message, part| { - message.child(part.build()) - }) + // Multiple parts, render as multipart. + if msg.param.get_cmd() == SystemMessage::MultiDeviceSync { + MimePart::new("multipart/report; report-type=multi-device-sync", parts) + } else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { + MimePart::new("multipart/report; report-type=status-update", parts) + } else { + MimePart::new("multipart/mixed", parts) + } } } Loaded::Mdn { .. } => self.render_mdn()?, }; - let get_content_type_directives_header = || { - ( - "Content-Type-Deltachat-Directives".to_string(), - "protected-headers=\"v1\"".to_string(), - ) - }; - // Split headers based on header confidentiality policy. // Headers that must go into IMF header section. @@ -811,7 +815,7 @@ impl MimeFactory { // anywhere else according to the standard. Placing headers here also allows them to be fetched // individually over IMAP without downloading the message body. This is why Chat-Version is // placed here. - let mut unprotected_headers: Vec
= Vec::new(); + let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); // Headers that MUST NOT go into IMF header section. // @@ -823,7 +827,7 @@ impl MimeFactory { // by moving it either into protected part // in case of encrypted mails // or unprotected MIME preamble in case of unencrypted mails. - let mut hidden_headers: Vec
= Vec::new(); + let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); // Opportunistically protected headers. // @@ -833,17 +837,20 @@ impl MimeFactory { // // If the message is not encrypted, these headers are placed into IMF header section, so make // sure that the message will be encrypted if you place any sensitive information here. - let mut protected_headers: Vec
= Vec::new(); + let mut protected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); // MIME header . - unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into())); - for header in headers { - let header_name = header.name.to_lowercase(); + unprotected_headers.push(( + "MIME-Version", + mail_builder::headers::raw::Raw::new("1.0").into(), + )); + for header @ (original_header_name, _header_value) in &headers { + let header_name = original_header_name.to_lowercase(); if header_name == "message-id" { unprotected_headers.push(header.clone()); - hidden_headers.push(header); + hidden_headers.push(header.clone()); } else if header_name == "chat-user-avatar" { - hidden_headers.push(header); + hidden_headers.push(header.clone()); } else if header_name == "autocrypt" && !context.get_config_bool(Config::ProtectAutocrypt).await? { @@ -854,50 +861,41 @@ impl MimeFactory { protected_headers.push(header.clone()); } - unprotected_headers.push( - Header::new_with_value( - header.name, - vec![Address::new_mailbox(self.from_addr.clone())], - ) - .unwrap(), - ); + unprotected_headers.push(( + original_header_name, + Address::new_address(None::<&'static str>, self.from_addr.clone()).into(), + )); } else if header_name == "to" { protected_headers.push(header.clone()); if is_encrypted { - unprotected_headers.push( - Header::new_with_value( - header.name, + unprotected_headers.push(( + original_header_name, + Address::new_list( to.clone() .into_iter() - .map(|header| match header { - Address::Mailbox(mb) => Address::Mailbox(Mailbox { - address: mb.address, + .filter_map(|header| match header { + Address::Address(mb) => Some(Address::Address(EmailAddress { name: None, - }), - Address::Group(name, participants) => Address::new_group( - name, - participants - .into_iter() - .map(|mb| Mailbox { - address: mb.address, - name: None, - }) - .collect(), - ), + email: mb.email, + })), + _ => None, }) .collect::>(), ) - .unwrap(), - ); + .into(), + )); } else { - unprotected_headers.push(header); + unprotected_headers.push(header.clone()); } } else if is_encrypted { protected_headers.push(header.clone()); match header_name.as_str() { "subject" => { - unprotected_headers.push(Header::new(header.name, "[...]".to_string())); + unprotected_headers.push(( + "Subject", + mail_builder::headers::raw::Raw::new("[...]").into(), + )); } "date" | "in-reply-to" @@ -905,7 +903,7 @@ impl MimeFactory { | "auto-submitted" | "chat-version" | "autocrypt-setup-message" => { - unprotected_headers.push(header); + unprotected_headers.push(header.clone()); } _ => { // Other headers are removed from unprotected part. @@ -916,7 +914,7 @@ impl MimeFactory { // in case of signed-only message. // If the message is not signed, this value will not be used. protected_headers.push(header.clone()); - unprotected_headers.push(header) + unprotected_headers.push(header.clone()) } } @@ -924,12 +922,16 @@ impl MimeFactory { // Store protected headers in the inner message. let message = protected_headers .into_iter() - .fold(message, |message, header| message.header(header)); + .fold(message, |message, (header, value)| { + message.header(header, value) + }); // Add hidden headers to encrypted payload. - let mut message = hidden_headers + let mut message: MimePart<'static> = hidden_headers .into_iter() - .fold(message, |message, header| message.header(header)); + .fold(message, |message, (header, value)| { + message.header(header, value) + }); // Add gossip headers in chats with multiple recipients let multiple_recipients = @@ -937,35 +939,22 @@ impl MimeFactory { if self.should_do_gossip(context, multiple_recipients).await? { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { if let Some(header) = peerstate.render_gossip_header(verified) { - message = message.header(Header::new("Autocrypt-Gossip".into(), header)); + message = message.header( + "Autocrypt-Gossip", + mail_builder::headers::raw::Raw::new(header), + ); is_gossiped = true; } } } // Set the appropriate Content-Type for the inner message. - let mut existing_ct = message - .get_header("Content-Type".to_string()) - .and_then(|h| h.get_value::().ok()) - .unwrap_or_else(|| "text/plain; charset=utf-8;".to_string()); - - if !existing_ct.ends_with(';') { - existing_ct += ";"; - } - let message = message.header(get_content_type_directives_header()); - - // Set the appropriate Content-Type for the outer message - let outer_message = PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(), - )); - - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!( - context, - "mimefactory: unencrypted message mime-body:\n{}", - message.clone().build().as_string(), - ); + for (h, ref mut v) in &mut message.headers { + if h == "Content-Type" { + if let mail_builder::headers::HeaderType::ContentType(ref mut ct) = v { + *ct = ct.clone().attribute("protected-headers", "v1"); + } + } } // Disable compression for SecureJoin to ensure @@ -977,32 +966,39 @@ impl MimeFactory { } Loaded::Mdn { .. } => true, }; + + // XXX: additional newline is needed + // to pass filtermail at + // let encrypted = encrypt_helper .encrypt(context, verified, message, peerstates, compress) - .await?; + .await? + + "\n"; - outer_message - .child( + // Set the appropriate Content-Type for the outer message + MimePart::new( + "multipart/encrypted; protocol=\"application/pgp-encrypted\"", + vec![ // Autocrypt part 1 - PartBuilder::new() - .content_type(&"application/pgp-encrypted".parse::().unwrap()) - .header(("Content-Description", "PGP/MIME version identification")) - .body("Version: 1\r\n") - .build(), - ) - .child( + MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header( + "Content-Description", + mail_builder::headers::raw::Raw::new("PGP/MIME version identification"), + ), // Autocrypt part 2 - PartBuilder::new() - .content_type( - &"application/octet-stream; name=\"encrypted.asc\"" - .parse::() - .unwrap(), - ) - .header(("Content-Description", "OpenPGP encrypted message")) - .header(("Content-Disposition", "inline; filename=\"encrypted.asc\";")) - .body(encrypted) - .build(), - ) + MimePart::new( + "application/octet-stream; name=\"encrypted.asc\"", + encrypted, + ) + .header( + "Content-Description", + mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"), + ) + .header( + "Content-Disposition", + mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"), + ), + ], + ) } else if matches!(self.loaded, Loaded::Mdn { .. }) { // Never add outer multipart/mixed wrapper to MDN // as multipart/report Content-Type is used to recognize MDNs @@ -1016,65 +1012,72 @@ impl MimeFactory { } else { let message = hidden_headers .into_iter() - .fold(message, |message, header| message.header(header)); - let message = PartBuilder::new() - .message_type(MimeMultipartType::Mixed) - .child(message.build()); - let message = protected_headers + .fold(message, |message, (header, value)| { + message.header(header, value) + }); + let message = MimePart::new("multipart/mixed", vec![message]); + let mut message = protected_headers .iter() - .fold(message, |message, header| message.header(header.clone())); + .fold(message, |message, (header, value)| { + message.header(*header, value.clone()) + }); if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? { // Deduplicate unprotected headers that also are in the protected headers: let protected: HashSet<&str> = - HashSet::from_iter(protected_headers.iter().map(|h| h.name.as_str())); - unprotected_headers.retain(|h| !protected.contains(&h.name.as_str())); + HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header)); + unprotected_headers.retain(|(header, _value)| !protected.contains(header)); message } else { - let message = message.header(get_content_type_directives_header()); - let (payload, signature) = encrypt_helper.sign(context, message).await?; - PartBuilder::new() - .header(( - "Content-Type", - "multipart/signed; protocol=\"application/pgp-signature\"", - )) - .child(payload) - .child( - PartBuilder::new() - .content_type( - &"application/pgp-signature; name=\"signature.asc\"" - .parse::() - .unwrap(), - ) - .header(("Content-Description", "OpenPGP digital signature")) - .header(("Content-Disposition", "attachment; filename=\"signature\";")) - .body(signature) - .build(), - ) + for (h, ref mut v) in &mut message.headers { + if h == "Content-Type" { + if let mail_builder::headers::HeaderType::ContentType(ref mut ct) = v { + *ct = ct.clone().attribute("protected-headers", "v1"); + } + } + } + + let signature = encrypt_helper.sign(context, &message).await?; + MimePart::new( + "multipart/signed; protocol=\"application/pgp-signature\"; protected", + vec![ + message, + MimePart::new( + "application/pgp-signature; name=\"signature.asc\"", + signature, + ) + .header( + "Content-Description", + mail_builder::headers::raw::Raw::<'static>::new( + "OpenPGP digital signature", + ), + ) + .attachment("signature"), + ], + ) } }; // Store the unprotected headers on the outer message. let outer_message = unprotected_headers .into_iter() - .fold(outer_message, |message, header| message.header(header)); - - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!( - context, - "mimefactory: outgoing message mime-body:\n{}", - outer_message.clone().build().as_string(), - ); - } + .fold(outer_message, |message, (header, value)| { + message.header(header, value) + }); let MimeFactory { last_added_location_id, .. } = self; + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + outer_message.clone().write_part(cursor).ok(); + let message = String::from_utf8_lossy(&buffer).to_string(); + Ok(RenderedEmail { - message: outer_message.build().as_string(), + message, // envelope: Envelope::new, is_encrypted, is_gossiped, @@ -1086,7 +1089,7 @@ impl MimeFactory { } /// Returns MIME part with a `message.kml` attachment. - fn get_message_kml_part(&self) -> Option { + fn get_message_kml_part(&self) -> Option> { let Loaded::Message { msg, .. } = &self.loaded else { return None; }; @@ -1095,22 +1098,16 @@ impl MimeFactory { let longitude = msg.param.get_float(Param::SetLongitude)?; let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude); - let part = PartBuilder::new() - .content_type( - &"application/vnd.google-earth.kml+xml" - .parse::() - .unwrap(), - ) - .header(( - "Content-Disposition", - "attachment; filename=\"message.kml\"", - )) - .body(kml_file); + let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_file) + .attachment("message.kml"); Some(part) } /// Returns MIME part with a `location.kml` attachment. - async fn get_location_kml_part(&mut self, context: &Context) -> Result> { + async fn get_location_kml_part( + &mut self, + context: &Context, + ) -> Result>> { let Loaded::Message { msg, .. } = &self.loaded else { return Ok(None); }; @@ -1121,17 +1118,8 @@ impl MimeFactory { return Ok(None); }; - let part = PartBuilder::new() - .content_type( - &"application/vnd.google-earth.kml+xml" - .parse::() - .unwrap(), - ) - .header(( - "Content-Disposition", - "attachment; filename=\"location.kml\"", - )) - .body(kml_content); + let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_content) + .attachment("location.kml"); if !msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed self.last_added_location_id = Some(last_added_location_id); @@ -1139,23 +1127,13 @@ impl MimeFactory { Ok(Some(part)) } - fn add_message_text(&self, part: PartBuilder, mut text: String) -> PartBuilder { - // This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable - // encoding and thus breaking messages and signatures. It's unlikely that the reader uses a - // MUA not supporting Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for - // encrypted messages. - let part = part.header(("Content-Transfer-Encoding", "quoted-printable")); - text = quoted_printable::encode_to_str(text); - part.body(text) - } - async fn render_message( &mut self, context: &Context, - headers: &mut Vec
, + headers: &mut Vec<(&'static str, HeaderType<'static>)>, grpimage: &Option, is_encrypted: bool, - ) -> Result<(PartBuilder, Vec)> { + ) -> Result<(MimePart<'static>, Vec>)> { let Loaded::Message { chat, msg } = &self.loaded else { bail!("Attempt to render MDN as a message"); }; @@ -1172,17 +1150,25 @@ impl MimeFactory { Chattype::Broadcast => false, }; if chat.is_protected() && send_verified_headers { - headers.push(Header::new("Chat-Verified".to_string(), "1".to_string())); + headers.push(( + "Chat-Verified", + mail_builder::headers::raw::Raw::new("1").into(), + )); } if chat.typ == Chattype::Group { // Send group ID unless it is an ad hoc group that has no ID. if !chat.grpid.is_empty() { - headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone())); + headers.push(( + "Chat-Group-ID", + mail_builder::headers::raw::Raw::new(chat.grpid.clone()).into(), + )); } - let encoded = encode_words(&chat.name); - headers.push(Header::new("Chat-Group-Name".into(), encoded)); + headers.push(( + "Chat-Group-Name", + mail_builder::headers::text::Text::new(chat.name.to_string()).into(), + )); match command { SystemMessage::MemberRemovedFromGroup => { @@ -1201,9 +1187,10 @@ impl MimeFactory { }; if !email_to_remove.is_empty() { - headers.push(Header::new( - "Chat-Group-Member-Removed".into(), - email_to_remove.into(), + headers.push(( + "Chat-Group-Member-Removed", + mail_builder::headers::raw::Raw::new(email_to_remove.to_string()) + .into(), )); } } @@ -1213,9 +1200,9 @@ impl MimeFactory { Some(stock_str::msg_add_member_remote(context, email_to_add).await); if !email_to_add.is_empty() { - headers.push(Header::new( - "Chat-Group-Member-Added".into(), - email_to_add.into(), + headers.push(( + "Chat-Group-Member-Added", + mail_builder::headers::raw::Raw::new(email_to_add.to_string()).into(), )); } if 0 != msg.param.get_int(Param::Arg2).unwrap_or_default() & DC_FROM_HANDSHAKE { @@ -1223,28 +1210,29 @@ impl MimeFactory { context, "Sending secure-join message {:?}.", "vg-member-added", ); - headers.push(Header::new( - "Secure-Join".to_string(), - "vg-member-added".to_string(), + headers.push(( + "Secure-Join", + mail_builder::headers::raw::Raw::new("vg-member-added".to_string()) + .into(), )); } } SystemMessage::GroupNameChanged => { - let old_name = msg.param.get(Param::Arg).unwrap_or_default(); - headers.push(Header::new( - "Chat-Group-Name-Changed".into(), - maybe_encode_words(old_name), + let old_name = msg.param.get(Param::Arg).unwrap_or_default().to_string(); + headers.push(( + "Chat-Group-Name-Changed", + mail_builder::headers::text::Text::new(old_name).into(), )); } SystemMessage::GroupImageChanged => { - headers.push(Header::new( - "Chat-Content".to_string(), - "group-avatar-changed".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::text::Text::new("group-avatar-changed").into(), )); if grpimage.is_none() { - headers.push(Header::new( - "Chat-Group-Avatar".to_string(), - "0".to_string(), + headers.push(( + "Chat-Group-Avatar", + mail_builder::headers::raw::Raw::new("0").into(), )); } } @@ -1254,15 +1242,15 @@ impl MimeFactory { match command { SystemMessage::LocationStreamingEnabled => { - headers.push(Header::new( - "Chat-Content".into(), - "location-streaming-enabled".into(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("location-streaming-enabled").into(), )); } SystemMessage::EphemeralTimerChanged => { - headers.push(Header::new( - "Chat-Content".to_string(), - "ephemeral-timer-changed".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("ephemeral-timer-changed").into(), )); } SystemMessage::LocationOnly @@ -1276,13 +1264,16 @@ impl MimeFactory { // Adding this header without encryption leaks some // information about the message contents, but it can // already be easily guessed from message timing and size. - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-generated".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-generated").into(), )); } SystemMessage::AutocryptSetupMessage => { - headers.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into())); + headers.push(( + "Autocrypt-Setup-Message", + mail_builder::headers::raw::Raw::new("v1").into(), + )); placeholdertext = Some(stock_str::ac_setup_msg_body(context).await); } @@ -1290,54 +1281,61 @@ impl MimeFactory { let step = msg.param.get(Param::Arg).unwrap_or_default(); if !step.is_empty() { info!(context, "Sending secure-join message {step:?}."); - headers.push(Header::new("Secure-Join".into(), step.into())); + headers.push(( + "Secure-Join", + mail_builder::headers::raw::Raw::new(step.to_string()).into(), + )); let param2 = msg.param.get(Param::Arg2).unwrap_or_default(); if !param2.is_empty() { - headers.push(Header::new( + headers.push(( if step == "vg-request-with-auth" || step == "vc-request-with-auth" { - "Secure-Join-Auth".into() + "Secure-Join-Auth" } else { - "Secure-Join-Invitenumber".into() + "Secure-Join-Invitenumber" }, - param2.into(), + mail_builder::headers::text::Text::new(param2.to_string()).into(), )); } let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default(); if !fingerprint.is_empty() { - headers.push(Header::new( - "Secure-Join-Fingerprint".into(), - fingerprint.into(), + headers.push(( + "Secure-Join-Fingerprint", + mail_builder::headers::raw::Raw::new(fingerprint.to_string()).into(), )); } if let Some(id) = msg.param.get(Param::Arg4) { - headers.push(Header::new("Secure-Join-Group".into(), id.into())); + headers.push(( + "Secure-Join-Group", + mail_builder::headers::raw::Raw::new(id.to_string()).into(), + )); }; } } SystemMessage::ChatProtectionEnabled => { - headers.push(Header::new( - "Chat-Content".to_string(), - "protection-enabled".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("protection-enabled").into(), )); } SystemMessage::ChatProtectionDisabled => { - headers.push(Header::new( - "Chat-Content".to_string(), - "protection-disabled".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("protection-disabled").into(), )); } SystemMessage::IrohNodeAddr => { - headers.push(Header::new( - HeaderDef::IrohNodeAddr.get_headername().to_string(), - serde_json::to_string( + headers.push(( + "Iroh-Node-Addr", + mail_builder::headers::text::Text::new(serde_json::to_string( &context .get_or_try_init_peer_channel() .await? .get_node_addr() .await?, - )?, + )?) + .into(), )); } _ => {} @@ -1348,22 +1346,31 @@ impl MimeFactory { let avatar = build_avatar_file(context, grpimage) .await .context("Cannot attach group image")?; - headers.push(Header::new( - "Chat-Group-Avatar".into(), - format!("base64:{avatar}"), + headers.push(( + "Chat-Group-Avatar", + mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(), )); } if msg.viewtype == Viewtype::Sticker { - headers.push(Header::new("Chat-Content".into(), "sticker".into())); - } else if msg.viewtype == Viewtype::VideochatInvitation { - headers.push(Header::new( - "Chat-Content".into(), - "videochat-invitation".into(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("sticker").into(), )); - headers.push(Header::new( - "Chat-Webrtc-Room".into(), - msg.param.get(Param::WebrtcRoom).unwrap_or_default().into(), + } else if msg.viewtype == Viewtype::VideochatInvitation { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("videochat-invitation").into(), + )); + headers.push(( + "Chat-Webrtc-Room", + mail_builder::headers::raw::Raw::new( + msg.param + .get(Param::WebrtcRoom) + .unwrap_or_default() + .to_string(), + ) + .into(), )); } @@ -1372,12 +1379,18 @@ impl MimeFactory { || msg.viewtype == Viewtype::Video { if msg.viewtype == Viewtype::Voice { - headers.push(Header::new("Chat-Voice-Message".into(), "1".into())); + headers.push(( + "Chat-Voice-Message", + mail_builder::headers::raw::Raw::new("1").into(), + )); } let duration_ms = msg.param.get_int(Param::Duration).unwrap_or_default(); if duration_ms > 0 { let dur = duration_ms.to_string(); - headers.push(Header::new("Chat-Duration".into(), dur)); + headers.push(( + "Chat-Duration", + mail_builder::headers::raw::Raw::new(dur).into(), + )); } } @@ -1440,12 +1453,12 @@ impl MimeFactory { footer ); - let mut main_part = - PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8")); - main_part = self.add_message_text(main_part, message_text); - + let mut main_part = MimePart::new("text/plain", message_text); if is_reaction { - main_part = main_part.header(("Content-Disposition", "reaction")); + main_part = main_part.header( + "Content-Disposition", + mail_builder::headers::raw::Raw::new("reaction"), + ); } let mut parts = Vec::new(); @@ -1461,10 +1474,10 @@ impl MimeFactory { msg.param.get(Param::SendHtml).map(|s| s.to_string()) }; if let Some(html) = html { - main_part = PartBuilder::new() - .message_type(MimeMultipartType::Alternative) - .child(main_part.build()) - .child(new_html_mimepart(html).build()); + main_part = MimePart::new( + "multipart/alternative", + vec![main_part, MimePart::new("text/html", html)], + ) } } @@ -1495,7 +1508,11 @@ impl MimeFactory { let json = msg.param.get(Param::Arg).unwrap_or_default(); parts.push(context.build_status_update_part(json)); } else if msg.viewtype == Viewtype::Webxdc { - headers.push(create_iroh_header(context, msg.id).await?); + headers.push(( + "Iroh-Gossip-Topic", + mail_builder::headers::raw::Raw::new(create_iroh_header(context, msg.id).await?) + .into(), + )); if let (Some(json), _) = context .render_webxdc_status_update_object( msg.id, @@ -1512,13 +1529,16 @@ impl MimeFactory { if self.attach_selfavatar { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { - Ok(avatar) => headers.push(Header::new( - "Chat-User-Avatar".into(), - format!("base64:{avatar}"), + Ok(avatar) => headers.push(( + "Chat-User-Avatar", + mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(), )), Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), }, - None => headers.push(Header::new("Chat-User-Avatar".into(), "0".into())), + None => headers.push(( + "Chat-User-Avatar", + mail_builder::headers::raw::Raw::new("0").into(), + )), } } @@ -1526,7 +1546,7 @@ impl MimeFactory { } /// Render an MDN - fn render_mdn(&mut self) -> Result { + fn render_mdn(&mut self) -> Result> { // RFC 6522, this also requires the `report-type` parameter which is equal // to the MIME subtype of the second body part of the multipart/report // @@ -1547,21 +1567,15 @@ impl MimeFactory { bail!("Attempt to render a message as MDN"); }; - let mut message = PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=disposition-notification".to_string(), - )); - // first body part: always human-readable, always REQUIRED by RFC 6522. // untranslated to no reveal sender's language. // moreover, translations in unknown languages are confusing, and clients may not display them at all - let text_part = PartBuilder::new().header(( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - )); - let text_part = - self.add_message_text(text_part, "This is a receipt notification.\r\n".to_string()); - message = message.child(text_part.build()); + let text_part = MimePart::new("text/plain", "This is a receipt notification."); + + let mut message = MimePart::new( + "multipart/report; report-type=disposition-notification", + vec![text_part], + ); // second body part: machine-readable, always REQUIRED by RFC 6522 let message_text2 = format!( @@ -1584,33 +1598,16 @@ impl MimeFactory { + "\r\n" }; - message = message.child( - PartBuilder::new() - .content_type(&"message/disposition-notification".parse().unwrap()) - .body(message_text2 + &extension_fields) - .build(), - ); + message.add_part(MimePart::new( + "message/disposition-notification", + message_text2 + &extension_fields, + )); Ok(message) } } -/// Returns base64-encoded buffer `buf` split into 76-bytes long -/// chunks separated by CRLF. -/// -/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8) -/// says that "The encoded output stream must be represented in lines of no more than 76 characters each." -/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin. -pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String { - let base64 = base64::engine::general_purpose::STANDARD.encode(buf); - let mut chars = base64.chars(); - std::iter::repeat_with(|| chars.by_ref().take(76).collect::()) - .take_while(|s| !s.is_empty()) - .collect::>() - .join("\r\n") -} - -async fn build_body_file(context: &Context, msg: &Message) -> Result { +async fn build_body_file(context: &Context, msg: &Message) -> Result> { let file_name = msg.get_filename().context("msg has no file")?; let suffix = Path::new(&file_name) .extension() @@ -1665,35 +1662,26 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result mtype.parse()?, + let mimetype = match msg.param.get(Param::MimeType) { + Some(mtype) => mtype.to_string(), None => { - if let Some(res) = message::guess_msgtype_from_suffix(msg) { - res.1.parse()? + if let Some((_viewtype, res)) = message::guess_msgtype_from_suffix(msg) { + res.to_string() } else { - mime::APPLICATION_OCTET_STREAM + "application/octet-stream".to_string() } } }; + let body = fs::read(blob.to_abs_path()).await?; + // create mime part, for Content-Disposition, see RFC 2183. // `Content-Disposition: attachment` seems not to make a difference to `Content-Disposition: inline` // at least on tested Thunderbird and Gma'l in 2017. // But I've heard about problems with inline and outl'k, so we just use the attachment-type until we // run into other problems ... - let cd_value = format!( - "attachment; filename=\"{}\"", - maybe_encode_words(&filename_to_send) - ); - - let body = fs::read(blob.to_abs_path()).await?; - let encoded_body = wrapped_base64_encode(&body); - - let mail = PartBuilder::new() - .content_type(&mimetype) - .header(("Content-Disposition", cd_value)) - .header(("Content-Transfer-Encoding", "base64")) - .body(encoded_body); + let mail = + MimePart::new(mimetype, body).attachment(sanitize_bidi_characters(&filename_to_send)); Ok(mail) } @@ -1704,7 +1692,17 @@ async fn build_avatar_file(context: &Context, path: &str) -> Result { false => BlobObject::from_path(context, path.as_ref())?, }; let body = fs::read(blob.to_abs_path()).await?; - let encoded_body = wrapped_base64_encode(&body); + let encoded_body = base64::engine::general_purpose::STANDARD + .encode(&body) + .chars() + .enumerate() + .fold(String::new(), |mut res, (i, c)| { + if i % 78 == 77 { + res.push(' ') + } + res.push(c); + res + }); Ok(encoded_body) } @@ -1725,15 +1723,6 @@ fn render_rfc724_mid(rfc724_mid: &str) -> String { } } -fn render_rfc724_mid_list(mid_list: &str) -> String { - mid_list - .trim() - .split_ascii_whitespace() - .map(render_rfc724_mid) - .collect::>() - .join(" ") -} - /* ****************************************************************************** * Encode/decode header words, RFC 2047 ******************************************************************************/ @@ -1742,23 +1731,10 @@ fn encode_words(word: &str) -> String { encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None) } -fn needs_encoding(to_check: &str) -> bool { - !to_check.chars().all(|c| { - c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' || c == '%' - }) -} - -fn maybe_encode_words(words: &str) -> String { - if needs_encoding(words) { - encode_words(words) - } else { - words.to_string() - } -} - #[cfg(test)] mod tests { use deltachat_contact_tools::ContactAddress; + use mail_builder::headers::Header; use mailparse::{addrparse_header, MailHeaderMap}; use std::str; @@ -1770,10 +1746,21 @@ mod tests { use crate::chatlist::Chatlist; use crate::constants; use crate::contact::Origin; + use crate::headerdef::HeaderDef; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf; use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; + fn render_email_address(display_name: &str, addr: &str) -> String { + let mut output = Vec::::new(); + new_address_with_name(display_name, addr.to_string()) + .unwrap_address() + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() + } + #[test] fn test_render_email_address() { let display_name = "ä space"; @@ -1784,11 +1771,11 @@ mod tests { .chars() .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - let s = format!("{}", new_address_with_name(display_name, addr.to_string())); + let s = render_email_address(display_name, addr); println!("{s}"); - assert_eq!(s, "=?utf-8?q?=C3=A4_space?= "); + assert_eq!(s, r#""=?utf-8?B?w6Qgc3BhY2U=?=" "#); } #[test] @@ -1801,16 +1788,16 @@ mod tests { .chars() .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - let s = format!("{}", new_address_with_name(display_name, addr.to_string())); + let s = render_email_address(display_name, addr); // Addresses should not be unnecessarily be encoded, see : - assert_eq!(s, "a space "); + assert_eq!(s, r#""a space" "#); } #[test] fn test_render_email_address_duplicated_as_name() { let addr = "x@y.org"; - let s = format!("{}", new_address_with_name(addr, addr.to_string())); + let s = render_email_address(addr, addr); assert_eq!(s, ""); } @@ -1830,38 +1817,20 @@ mod tests { ); } - #[test] - fn test_render_rc724_mid_list() { - assert_eq!(render_rfc724_mid_list("123@q "), "<123@q>".to_string()); - assert_eq!(render_rfc724_mid_list(" 123@q "), "<123@q>".to_string()); - assert_eq!( - render_rfc724_mid_list("123@q 456@d "), - "<123@q> <456@d>".to_string() - ); + fn render_header_text(text: &str) -> String { + let mut output = Vec::::new(); + mail_builder::headers::text::Text::new(text.to_string()) + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() } #[test] - fn test_wrapped_base64_encode() { - let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - let output = - "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\ - QUFBQUFBQUFBQQ=="; - assert_eq!(wrapped_base64_encode(input), output); - } - - #[test] - fn test_needs_encoding() { - assert!(!needs_encoding("")); - assert!(!needs_encoding("foobar")); - assert!(needs_encoding(" ")); - assert!(needs_encoding("foo bar")); - } - - #[test] - fn test_maybe_encode_words() { - assert_eq!(maybe_encode_words("foobar"), "foobar"); - assert_eq!(maybe_encode_words("-_.~%"), "-_.~%"); - assert_eq!(maybe_encode_words("äöü"), "=?utf-8?b?w6TDtsO8?="); + fn test_header_encoding() { + assert_eq!(render_header_text("foobar"), "foobar\r\n"); + assert_eq!(render_header_text("-_.~%"), "-_.~%\r\n"); + assert_eq!(render_header_text("äöü"), "=?utf-8?B?w6TDtsO8?=\r\n"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -2331,57 +2300,6 @@ mod tests { .unwrap(); } - #[test] - fn test_no_empty_lines_in_header() { - // See - let to_tuples = [ - ("Nnnn", "nnn@ttttttttt.de"), - ("😀 ttttttt", "ttttttt@rrrrrr.net"), - ("dididididididi", "t@iiiiiii.org"), - ("Ttttttt", "oooooooooo@abcd.de"), - ("Mmmmm", "mmmmm@rrrrrr.net"), - ("Zzzzzz", "rrrrrrrrrrrrr@ttttttttt.net"), - ("Xyz", "qqqqqqqqqq@rrrrrr.net"), - ("", "geug@ttttttttt.de"), - ("qqqqqq", "q@iiiiiii.org"), - ("bbbb", "bbbb@iiiiiii.org"), - ("", "fsfs@iiiiiii.org"), - ("rqrqrqrqr", "rqrqr@iiiiiii.org"), - ("tttttttt", "tttttttt@iiiiiii.org"), - ("", "tttttt@rrrrrr.net"), - ] - .iter(); - let to: Vec<_> = to_tuples - .map(|(name, addr)| { - if name.is_empty() { - Address::new_mailbox(addr.to_string()) - } else { - new_address_with_name(name, addr.to_string()) - } - }) - .collect(); - - let mut message = email::MimeMessage::new_blank_message(); - message.headers.insert( - ( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - ) - .into(), - ); - message - .headers - .insert(Header::new_with_value("To".into(), to).unwrap()); - message.body = "Hi".to_string(); - - let msg = message.as_string(); - - let header_end = msg.find("Hi").unwrap(); - let headers = msg[0..header_end].trim(); - - assert!(!headers.lines().any(|l| l.trim().is_empty())); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { // create chat with bob, set selfavatar @@ -2415,7 +2333,6 @@ mod tests { assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(inner.match_indices("Subject:").count(), 0); - assert_eq!(inner.match_indices("quoted-printable").count(), 1); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2436,7 +2353,6 @@ mod tests { assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(inner.match_indices("Subject:").count(), 0); - assert_eq!(inner.match_indices("quoted-printable").count(), 1); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2493,7 +2409,6 @@ mod tests { assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 0); - assert_eq!(part.match_indices("quoted-printable").count(), 1); let body = payload.next().unwrap(); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2541,7 +2456,6 @@ mod tests { assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 0); - assert_eq!(part.match_indices("quoted-printable").count(), 1); let body = payload.next().unwrap(); assert_eq!(body.match_indices("this is the text!").count(), 1); diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 03c7cd40b..850e59a99 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -10,8 +10,8 @@ use anyhow::{bail, Context as _, Result}; use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters}; use deltachat_derive::{FromSql, ToSql}; use format_flowed::unformat_flowed; -use lettre_email::mime::Mime; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; +use mime::Mime; use crate::aheader::{Aheader, EncryptPreference}; use crate::authres::handle_authres; diff --git a/src/peer_channels.rs b/src/peer_channels.rs index 158f3785d..ecb384b81 100644 --- a/src/peer_channels.rs +++ b/src/peer_channels.rs @@ -25,7 +25,6 @@ use anyhow::{anyhow, bail, Context as _, Result}; use data_encoding::BASE32_NOPAD; -use email::Header; use futures_lite::StreamExt; use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMap, RelayMode, RelayUrl, SecretKey}; use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN}; @@ -40,7 +39,6 @@ use url::Url; use crate::chat::send_msg; use crate::config::Config; use crate::context::Context; -use crate::headerdef::HeaderDef; use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; use crate::EventType; @@ -496,14 +494,11 @@ fn create_random_topic() -> TopicId { /// Creates `Iroh-Gossip-Header` with a new random topic /// and stores the topic for the message. -pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result
{ +pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result { let topic = create_random_topic(); insert_topic_stub(ctx, msg_id, topic).await?; let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase(); - Ok(Header::new( - HeaderDef::IrohGossipTopic.get_headername().to_string(), - topic_string, - )) + Ok(topic_string) } async fn subscribe_loop( diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 59a07f6fe..594bdc9fe 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1061,8 +1061,8 @@ async fn test_classic_mailing_list() -> Result<()> { let mime = sent.payload(); println!("Sent mime message is:\n\n{mime}\n\n"); - assert!(mime.contains("Content-Type: text/plain; charset=utf-8\r\n")); - assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); + assert!(mime.contains("Content-Type: text/plain; charset=\"utf-8\"\r\n")); + assert!(mime.contains("Subject: Re: [delta-dev] What's up?\r\n")); assert!(mime.contains("MIME-Version: 1.0\r\n")); assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); assert!(mime.contains("Chat-Version: 1.0\r\n")); diff --git a/src/sync.rs b/src/sync.rs index 4905ca0e5..ba8139b77 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,7 @@ //! # Synchronize items between devices. use anyhow::Result; -use lettre_email::PartBuilder; +use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use crate::chat::{self, ChatId}; @@ -227,14 +227,8 @@ impl Context { } } - pub(crate) fn build_sync_part(&self, json: String) -> PartBuilder { - PartBuilder::new() - .content_type(&"application/json".parse::().unwrap()) - .header(( - "Content-Disposition", - "attachment; filename=\"multi-device-sync.json\"", - )) - .body(json) + pub(crate) fn build_sync_part(&self, json: String) -> MimePart<'static> { + MimePart::new("application/json", json).attachment("multi-device-sync.json") } /// Takes a JSON string created by `build_sync_json()` diff --git a/src/webxdc.rs b/src/webxdc.rs index 5f9320474..a4ea27116 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -27,7 +27,7 @@ use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result}; use async_zip::tokio::read::seek::ZipFileReader as SeekZipFileReader; use deltachat_contact_tools::sanitize_bidi_characters; use deltachat_derive::FromSql; -use lettre_email::PartBuilder; +use mail_builder::mime::MimePart; use rusqlite::OptionalExtension; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -41,7 +41,6 @@ use crate::context::Context; use crate::events::EventType; use crate::key::{load_self_public_key, DcKey}; use crate::message::{Message, MessageState, MsgId, Viewtype}; -use crate::mimefactory::wrapped_base64_encode; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::mimeparser::SystemMessage; use crate::param::Param; @@ -651,17 +650,8 @@ impl Context { } } - pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder { - let encoded_body = wrapped_base64_encode(json.as_bytes()); - - PartBuilder::new() - .content_type(&"application/json".parse::().unwrap()) - .header(( - "Content-Disposition", - "attachment; filename=\"status-update.json\"", - )) - .header(("Content-Transfer-Encoding", "base64")) - .body(encoded_body) + pub(crate) fn build_status_update_part(&self, json: &str) -> MimePart<'static> { + MimePart::new("application/json", json.as_bytes().to_vec()).attachment("status-update.json") } /// Receives status updates from receive_imf to the database