From 3b040d0cd6e2d97380d035810d4e02c324c6e380 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Tue, 13 Jan 2026 23:32:18 +0800 Subject: [PATCH] feat: add support for Outlook PST file import #105 --- Cargo.lock | 474 ++++++++++++++++++++++++++-- Cargo.toml | 10 +- src/bin/bichonctl.rs | 6 +- src/modules/cli/mod.rs | 2 + src/modules/cli/pst/encoding/mod.rs | 45 +++ src/modules/cli/pst/mod.rs | 457 +++++++++++++++++++++++++++ src/modules/cli/thunderbird/mod.rs | 1 - 7 files changed, 968 insertions(+), 27 deletions(-) create mode 100644 src/modules/cli/pst/encoding/mod.rs create mode 100644 src/modules/cli/pst/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 33f2aec..0399276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,18 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", ] [[package]] @@ -35,8 +46,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" dependencies = [ "aead", - "aes", - "cipher", + "aes 0.6.0", + "cipher 0.2.5", "ctr", "ghash", "subtle", @@ -48,7 +59,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -58,7 +69,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -170,6 +181,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -435,6 +455,8 @@ dependencies = [ "cacache", "chrono", "clap", + "codepage-strings", + "compressed-rtf", "console", "dashmap", "dialoguer", @@ -451,6 +473,7 @@ dependencies = [ "itoa", "lru 0.16.2", "mail-parser", + "mail-send", "memmap2 0.9.9", "mimalloc", "mime_guess", @@ -460,6 +483,7 @@ dependencies = [ "num_cpus", "oauth2", "openssl-sys", + "outlook-pst", "poem", "poem-derive", "poem-openapi", @@ -624,6 +648,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "cacache" version = "13.1.0" @@ -736,10 +769,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -747,9 +790,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -784,12 +827,42 @@ dependencies = [ "cc", ] +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "codepage-strings" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e0dcc0fce1af8fe139537bcd0c2522296a701bf11eb6b143ca88ff8c742523" +dependencies = [ + "codepage", + "encoding_rs", + "oem_cp", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compressed-rtf" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bd502b5779b9f999a2d69115f7e2fb1662883aa5a0e50e33cff684dae101d9" +dependencies = [ + "byteorder", + "thiserror 2.0.17", +] + [[package]] name = "compression-codecs" version = "0.4.33" @@ -838,6 +911,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie" version = "0.14.4" @@ -847,7 +926,7 @@ dependencies = [ "aes-gcm", "base64 0.13.1", "hkdf", - "hmac", + "hmac 0.10.1", "percent-encoding", "rand 0.8.5", "sha2 0.9.9", @@ -876,6 +955,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -885,6 +979,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -951,7 +1051,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" dependencies = [ - "cipher", + "cipher 0.2.5", ] [[package]] @@ -1075,6 +1175,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "deranged" version = "0.5.5" @@ -1085,6 +1191,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -1135,6 +1252,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", + "subtle", ] [[package]] @@ -1208,6 +1326,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1294,6 +1424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1699,6 +1830,60 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.26.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62d7684f766b0f96344be88c023f9b6650039aea09d526b4974cce302eb61b1" +dependencies = [ + "async-trait", + "bitflags", + "bytes 1.11.0", + "cfg-if", + "data-encoding", + "enum-as-inner 0.6.1", + "futures-channel", + "futures-io", + "futures-util", + "idna 1.1.0", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "rustls", + "rustls-pki-types", + "thiserror 2.0.17", + "time 0.3.44", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbab5e26a7f82341145ba1fbd1f1858d0490624fcc46270db2d3c4a101f763f4" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "hkdf" version = "0.10.0" @@ -1706,7 +1891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ "digest 0.9.0", - "hmac", + "hmac 0.10.1", ] [[package]] @@ -1719,6 +1904,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "html2text" version = "0.16.5" @@ -2080,6 +2274,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2211,6 +2414,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.177" @@ -2243,6 +2452,15 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.23" @@ -2330,12 +2548,52 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2 0.10.9", +] + [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mail-auth" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7da45f78cc525d3750b623c967ae21c0cd28b2e6a9a2ee4b536a7cce3b21ce" +dependencies = [ + "ahash", + "flate2", + "hashify", + "hickory-resolver", + "mail-builder", + "mail-parser", + "quick-xml 0.38.4", + "quick_cache", + "ring", + "rustls-pki-types", + "serde", + "serde_json", + "zip", +] + +[[package]] +name = "mail-builder" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b" +dependencies = [ + "gethostname", +] + [[package]] name = "mail-parser" version = "0.11.1" @@ -2347,6 +2605,26 @@ dependencies = [ "serde", ] +[[package]] +name = "mail-send" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114a4e27f3cfaf8918783e8fa4149b820c813b1bedc7755e20e12eff4518331e" +dependencies = [ + "base64 0.22.1", + "gethostname", + "mail-auth", + "mail-builder", + "md5", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "smtp-proto", + "tokio", + "tokio-rustls", + "webpki-roots", +] + [[package]] name = "markup5ever" version = "0.36.1" @@ -2373,6 +2651,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + [[package]] name = "measure_time" version = "0.9.0" @@ -2481,6 +2765,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "multer" version = "3.1.0" @@ -2690,11 +2991,25 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "oem_cp" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a95602f5a6eec5b15394516448c0b6fc2f2ef4ca2a9ab2951a91a18cec516bf0" +dependencies = [ + "ahash", + "lazy_static", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -2742,6 +3057,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "outlook-pst" +version = "1.1.0" +source = "git+https://github.com/rustmailer/outlook-pst-rs.git?branch=main#c12f3595ee5a6f8407d19497133725a3741570aa" +dependencies = [ + "byteorder", + "thiserror 2.0.17", + "tracing", +] + [[package]] name = "ownedbytes" version = "0.9.0" @@ -2780,6 +3105,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2915,7 +3250,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "poem-derive", - "quick-xml", + "quick-xml 0.36.2", "regex", "rfc7239", "rust-embed", @@ -2965,7 +3300,7 @@ dependencies = [ "num-traits", "poem", "poem-openapi-derive", - "quick-xml", + "quick-xml 0.36.2", "regex", "serde", "serde_json", @@ -3039,6 +3374,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3124,6 +3465,27 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick_cache" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3919,6 +4281,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smtp-proto" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55cc1c74d3b758d7dd1fa4dc4cf694cad2732cac14f304228477c2b0ce6233a" + [[package]] name = "snafu" version = "0.8.9" @@ -4193,6 +4561,12 @@ dependencies = [ "windows 0.61.3", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tantivy" version = "0.25.0" @@ -4524,9 +4898,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes 1.11.0", "libc", @@ -4808,7 +5182,7 @@ dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", + "enum-as-inner 0.5.1", "futures-channel", "futures-io", "futures-util", @@ -4934,14 +5308,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna 1.1.0", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5764,6 +6139,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "zerotrie" @@ -5798,12 +6187,57 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes 0.8.4", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac 0.12.1", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1 0.10.6", + "time 0.3.44", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + [[package]] name = "zmij" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3280a1b827474fcd5dbef4b35a674deb52ba5c312363aef9135317df179d81b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index a87596a..6da79b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ codegen-units = 1 [dependencies] chrono = "0.4.42" -clap = { version = "4.5.53", features = ["derive", "env"] } +clap = { version = "4.5.54", features = ["derive", "env"] } mimalloc = "0.1.48" native_db = "0.8.2" itertools = "0.14.0" @@ -41,7 +41,7 @@ poem-openapi = { version = "5.1.16", features = [ ring = { version = "0.17.14", features = ["std"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" -tokio = { version = "1.48.0", features = ["full"] } +tokio = { version = "1.49.0", features = ["full"] } tracing = "0.1.44" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } @@ -71,7 +71,7 @@ tokio-rustls = { version = "0.26.4", default-features = false, features = [ timeago = "0.5.0" ahash = "0.8.12" oauth2 = { version = "5.0.0", features = ["reqwest-blocking"] } -url = { version = "2.5.7", features = ["serde"] } +url = { version = "2.5.8", features = ["serde"] } sysinfo = "0.37.2" num_cpus = "1.17.0" cacache = { version = "13.1.0", default-features = false, features = [ @@ -115,6 +115,10 @@ dialoguer = "0.12.0" console = "0.16.2" toml = "0.9.8" memmap2 = "0.9.9" +outlook-pst = { git = "https://github.com/rustmailer/outlook-pst-rs.git", branch = "main" } +compressed-rtf = "1.0.0" +codepage-strings = "1.0.2" +mail-send = "0.5.2" [dev-dependencies] #bincode = "1.3.3" #secret-lib = "1.0.0" diff --git a/src/bin/bichonctl.rs b/src/bin/bichonctl.rs index 9b86993..0e0c5ee 100644 --- a/src/bin/bichonctl.rs +++ b/src/bin/bichonctl.rs @@ -1,7 +1,5 @@ use bichon::modules::cli::{ - auth::verify_user_and_get_account, eml::handle_eml_directory_import, - mbox::handle_mbox_single_file_import, thunderbird::handle_thunderbird_import, BichonCli, - BichonCtlConfig, + BichonCli, BichonCtlConfig, auth::verify_user_and_get_account, eml::handle_eml_directory_import, mbox::handle_mbox_single_file_import, pst::handle_pst_import, thunderbird::handle_thunderbird_import }; use clap::Parser; use console::style; @@ -77,6 +75,7 @@ async fn main() { "EML: Scan directory recursively (Maintains folder structure)", "MBOX: Single archive file (Stream from one file)", "Thunderbird: Import from local profile directory", + "PST: Outlook Personal Storage (Single .pst file)", ]; let mode_idx = Select::with_theme(&theme) @@ -90,6 +89,7 @@ async fn main() { 0 => handle_eml_directory_import(&final_config, target_account_id, &theme).await, 1 => handle_mbox_single_file_import(&final_config, target_account_id, &theme).await, 2 => handle_thunderbird_import(&final_config, target_account_id, &theme).await, + 3 => handle_pst_import(&final_config, target_account_id, &theme).await, _ => unreachable!(), } } diff --git a/src/modules/cli/mod.rs b/src/modules/cli/mod.rs index 9c5da8f..906f9ac 100644 --- a/src/modules/cli/mod.rs +++ b/src/modules/cli/mod.rs @@ -6,8 +6,10 @@ use crate::bichon_version; pub mod auth; pub mod eml; pub mod mbox; +pub mod pst; pub mod sender; pub mod thunderbird; + #[derive(Parser, Debug)] #[command( name = "bichonctl", diff --git a/src/modules/cli/pst/encoding/mod.rs b/src/modules/cli/pst/encoding/mod.rs new file mode 100644 index 0000000..e1b564b --- /dev/null +++ b/src/modules/cli/pst/encoding/mod.rs @@ -0,0 +1,45 @@ +use compressed_rtf::*; +use outlook_pst::ltp::prop_context::PropertyValue; + +pub fn decode_subject(value: &PropertyValue) -> Option { + match value { + PropertyValue::String8(value) => { + let offset = match value.buffer().first() { + Some(1) => 2, + _ => 0, + }; + let buffer: Vec<_> = value + .buffer() + .iter() + .skip(offset) + .map(|&b| u16::from(b)) + .collect(); + Some(String::from_utf16_lossy(&buffer)) + } + PropertyValue::Unicode(value) => { + let offset = match value.buffer().first() { + Some(1) => 2, + _ => 0, + }; + Some(String::from_utf16_lossy(&value.buffer()[offset..])) + } + _ => None, + } +} + +pub fn decode_html_body(buffer: &[u8], code_page: u16) -> Option { + match code_page { + 20127 => { + let buffer: Vec<_> = buffer.iter().map(|&b| u16::from(b)).collect(); + Some(String::from_utf16_lossy(&buffer)) + } + _ => { + let coding = codepage_strings::Coding::new(code_page).ok()?; + Some(coding.decode(buffer).ok()?.to_string()) + } + } +} + +pub fn decode_rtf_compressed(buffer: &[u8]) -> Option { + decompress_rtf(buffer).ok() +} diff --git a/src/modules/cli/pst/mod.rs b/src/modules/cli/pst/mod.rs new file mode 100644 index 0000000..39fb4e2 --- /dev/null +++ b/src/modules/cli/pst/mod.rs @@ -0,0 +1,457 @@ +use chrono::{DateTime, TimeZone, Utc}; +use dialoguer::theme::ColorfulTheme; +use dialoguer::Input; +use mail_send::mail_builder::headers::text::Text; +use mail_send::mail_builder::MessageBuilder; +use outlook_pst::ltp::prop_context::PropertyValue; + +use crate::base64_encode_url_safe; +use crate::modules::cli::pst::encoding::decode_subject; +use crate::modules::cli::sender::send_batch_request; +use crate::modules::cli::BichonCtlConfig; +use dialoguer::Confirm; +use outlook_pst::messaging::attachment::AttachmentProperties; +use outlook_pst::messaging::folder::Folder; +use outlook_pst::messaging::message::{Message, MessageProperties}; +use outlook_pst::ndb::node_id::NodeId; +use reqwest::Client; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::rc::Rc; + +mod encoding; + +#[derive(Debug, Default)] +pub struct EmailMetadata { + pub message_id: Option, + pub subject: Option, + pub from: Option, + pub to: Option>, + pub cc: Option>, + pub bcc: Option>, + pub html: Option, + pub text: Option, + pub in_reply_to: Option, +} + +#[derive(Debug, Default)] +pub struct EmailAttachment { + pub name: Option, + pub mime_type: Option, + pub data: Option>, +} + +pub async fn handle_pst_import(config: &BichonCtlConfig, account_id: u64, theme: &ColorfulTheme) { + let path_str: String = Input::with_theme(theme) + .with_prompt("Enter the path to your SINGLE .pst file") + .validate_with(|input: &String| { + let p = std::path::Path::new(input); + if !p.exists() { + return Err("The specified path does not exist."); + } + + if !p.is_file() { + return Err("PST mode requires a SINGLE file, not a directory."); + } + let is_pst = p + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("pst")) + .unwrap_or(false); + + if !is_pst { + return Err("The selected file must have a .pst extension."); + } + + Ok(()) + }) + .interact_text() + .unwrap(); + + let pst_path = std::path::PathBuf::from(path_str); + + println!( + "\n{} Ready to process PST file: {}", + console::style("βœ”").green(), + console::style(pst_path.display()).cyan() + ); + + if let Ok(meta) = std::fs::metadata(&pst_path) { + let size_mb = meta.len() as f64 / 1024.0 / 1024.0; + println!( + "{}", + console::style(format!("PST File Size: {:.1} MB", size_mb)).dim() + ); + } + + if Confirm::with_theme(theme) + .with_prompt("Start importing emails from this PST?") + .default(true) + .interact() + .unwrap() + { + parse_pst(pst_path, config, account_id).await; + } else { + println!("{}", console::style("Operation cancelled by user.").red()); + } +} + +async fn parse_pst(pst_path: PathBuf, config: &BichonCtlConfig, account_id: u64) { + let client = Client::new(); + + let pst_store = match outlook_pst::open_store(&pst_path) { + Ok(store) => store, + Err(e) => { + println!( + "{} Failed to open PST file: {}", + console::style("✘").red(), + console::style(format!("{:#?}", e)).dim() + ); + return; + } + }; + + let ipm_sub_tree = match pst_store.properties().ipm_sub_tree_entry_id() { + Ok(id) => id, + Err(e) => { + println!( + "{} Could not find IPM_SUBTREE (Mailbox Root): {}", + console::style("✘").red(), + console::style(format!("{:#?}", e)).dim() + ); + return; + } + }; + + let ipm_subtree_folder = match pst_store.open_folder(&ipm_sub_tree) { + Ok(folder) => folder, + Err(e) => { + println!( + "{} Failed to open the root mailbox folder: {}", + console::style("✘").red(), + console::style(format!("{:#?}", e)).dim() + ); + return; + } + }; + + process_folder_recursively(&client, &ipm_subtree_folder, "", config, account_id).await; +} + +fn process_folder_recursively<'a>( + client: &'a Client, + folder: &'a Rc, + parent_path: &'a str, + config: &'a BichonCtlConfig, + account_id: u64, +) -> Pin + 'a>> { + Box::pin(async move { + let folder_name = folder + .properties() + .display_name() + .unwrap_or_else(|_| "Unknown".to_string()); + + let current_path = if parent_path.is_empty() { + folder_name + } else { + format!("{}/{}", parent_path, folder_name) + }; + + println!( + "{} {}", + console::style("πŸ“ Folder:").dim(), + console::style(¤t_path).cyan() + ); + + let mut emls_batch = Vec::new(); + + if let Some(contents_table) = folder.contents_table() { + for row in contents_table.rows_matrix() { + let store = folder.store().clone(); + + let entry_id = match store + .properties() + .make_entry_id(NodeId::from(u32::from(row.id()))) + { + Ok(id) => id, + Err(e) => { + eprintln!( + " {} Skip row {}: {:?}", + console::style("⚠").yellow(), + row.unique(), + e + ); + continue; + } + }; + + match store.open_message(&entry_id, None) { + Ok(message) => match build_eml_base64(message) { + Some(base64_eml) => emls_batch.push(base64_eml), + None => {} + }, + Err(e) => eprintln!(" {} Open error: {:?}", console::style("⚠").yellow(), e), + } + + if emls_batch.len() >= 50 { + let batch = emls_batch.clone(); + emls_batch.clear(); + send_to_bichon(client, config, account_id, ¤t_path, batch).await; + } + } + } + + if !emls_batch.is_empty() { + send_to_bichon(client, config, account_id, ¤t_path, emls_batch).await; + } + + if let Some(hierarchy_table) = folder.hierarchy_table() { + for row in hierarchy_table.rows_matrix() { + let node = NodeId::from(u32::from(row.id())); + if let Ok(entry_id) = folder.store().properties().make_entry_id(node) { + if let Ok(sub_folder) = folder.store().open_folder(&entry_id) { + process_folder_recursively( + client, + &sub_folder, + ¤t_path, + config, + account_id, + ) + .await; + } + } + } + } + }) +} + +fn build_eml_base64(message: Rc) -> Option { + let properties = message.properties(); + + let mut builder = MessageBuilder::new(); + if let Some(sub) = extract_subject(properties) { + builder = builder.subject(sub); + } + if let Some(mid) = extract_string_property(properties, 0x1035) { + builder = builder.message_id(mid); + } + if let Some(irt) = extract_string_property(properties, 0x1042) { + builder = builder.in_reply_to(irt); + } + + if let Some(refs) = extract_string_property(properties, 0x1039) { + builder = builder.header("References", Text::new(refs)); + } + + if let Some(cid_val) = properties.get(0x3013) { + if let PropertyValue::Binary(bin) = cid_val { + builder = builder.header( + "X-Bichon-Conversation-ID", + Text::new(hex::encode(bin.buffer())), + ); + } + } + + let from = extract_string_property(properties, 0x5D01) + .or_else(|| extract_string_property(properties, 0x5D02)); + + if let Some(f) = from { + builder = builder.from(f); + } + + if let Some(filetime) = extract_i64_property(properties, &[0x0039, 0x0E06]) { + let dt = filetime_to_datetime(filetime).timestamp(); + builder = builder.date(dt); + } + + let (to, cc, bcc) = extract_recipients_list(&message); + if !to.is_empty() { + builder = builder.to(to.iter().map(|s| s.as_str()).collect::>()); + } + if !cc.is_empty() { + builder = builder.cc(cc.iter().map(|s| s.as_str()).collect::>()); + } + if !bcc.is_empty() { + builder = builder.bcc(bcc.iter().map(|s| s.as_str()).collect::>()); + } + + if let Some(html) = extract_html(properties) { + builder = builder.html_body(html); + } + + if let Some(text) = extract_text(properties) { + builder = builder.text_body(text); + } + + if let Some(attachment_table) = message.attachment_table() { + for row in attachment_table.rows_matrix() { + let node_id = NodeId::from(u32::from(row.id())); + if let Ok(attachment) = message.clone().read_attachment(node_id, None) { + let att_props = attachment.properties(); + let name = extract_attachment_string_property(att_props, 0x3707); + let mime = extract_attachment_string_property(att_props, 0x370E) + .unwrap_or_else(|| "application/octet-stream".into()); + let cid = extract_attachment_string_property(att_props, 0x3712); + let is_inline = att_props + .get(0x3714) + .and_then(|val| { + if let PropertyValue::Integer32(f) = val { + Some(f) + } else { + None + } + }) + .map(|flag| (flag & 0x4) != 0) + .unwrap_or(false); + + if let Some(PropertyValue::Binary(bin)) = att_props.get(0x3701) { + let data = bin.buffer().to_vec(); + let file_name = name.unwrap_or_else(|| "unnamed_attachment".to_string()); + + if is_inline && cid.is_some() { + let content_id = cid.unwrap(); + builder = builder.inline(mime, content_id, data); + } else { + builder = builder.attachment(mime, file_name, data); + } + } + } + } + } + + match builder.write_to_vec() { + Ok(eml_vec) => Some(base64_encode_url_safe!(eml_vec)), + Err(e) => { + eprintln!("Failed to generate EML: {:?}", e); + None + } + } +} + +fn filetime_to_datetime(filetime: i64) -> DateTime { + let unix_secs = (filetime / 10_000_000) - 11_644_473_600; + let nsecs = (filetime % 10_000_000) * 100; + Utc.timestamp_opt(unix_secs, nsecs as u32).unwrap() +} + +fn extract_recipients_list(message: &Rc) -> (Vec, Vec, Vec) { + let mut to = Vec::new(); + let mut cc = Vec::new(); + let mut bcc = Vec::new(); + + let recipient_table = message.recipient_table(); + let context = recipient_table.context(); + + for row in recipient_table.rows_matrix() { + if let Ok(cols) = row.columns(context) { + let mut r_type = 0; + let mut email = String::new(); + + for (col, val) in context.columns().iter().zip(cols) { + let prop_val = val + .as_ref() + .and_then(|v| recipient_table.read_column(v, col.prop_type()).ok()); + match col.prop_id() { + 0x0C15 => { + if let Some(PropertyValue::Integer32(t)) = prop_val { + r_type = t; + } + } + 0x39FE | 0x3003 => { + if let Some(s) = prop_val.and_then(|v| extract_string(&v)) { + email = s; + } + } + _ => {} + } + } + + if !email.is_empty() { + match r_type { + 1 => to.push(email), + 2 => cc.push(email), + 3 => bcc.push(email), + _ => {} + } + } + } + } + (to, cc, bcc) +} + +async fn send_to_bichon( + client: &Client, + config: &BichonCtlConfig, + account_id: u64, + folder_path: &str, + emls: Vec, +) { + send_batch_request(client, config, account_id, folder_path, emls).await; +} + +fn extract_subject(props: &MessageProperties) -> Option { + props.get(0x0037).and_then(|val| decode_subject(val)) +} + +fn extract_string_property(properties: &MessageProperties, prop_id: u16) -> Option { + properties + .get(prop_id) + .and_then(|value| extract_string(value)) +} + +fn extract_attachment_string_property( + properties: &AttachmentProperties, + prop_id: u16, +) -> Option { + properties + .get(prop_id) + .and_then(|value| extract_string(value)) +} + +fn extract_string(value: &PropertyValue) -> Option { + match value { + PropertyValue::String8(value) => Some(value.to_string()), + PropertyValue::Unicode(value) => Some(value.to_string()), + _ => None, + } +} + +fn extract_text(properties: &MessageProperties) -> Option { + properties.get(0x1000).and_then(extract_string).or_else(|| { + properties.get(0x1009).and_then(|value| match value { + PropertyValue::Binary(value) => encoding::decode_rtf_compressed(value.buffer()), + _ => None, + }) + }) +} + +fn extract_html(properties: &MessageProperties) -> Option { + properties.get(0x1013).and_then(|value| match value { + PropertyValue::Binary(value) => { + let code_page = properties + .get(0x3FDE) + .and_then(|v| { + if let PropertyValue::Integer32(cpid) = v { + Some(*cpid as u16) + } else { + None + } + }) + .unwrap_or(65001); + encoding::decode_html_body(value.buffer(), code_page) + } + PropertyValue::String8(value) => Some(value.to_string()), + PropertyValue::Unicode(value) => Some(value.to_string()), + _ => None, + }) +} + +fn extract_i64_property(properties: &MessageProperties, prop_ids: &[u16]) -> Option { + for &prop_id in prop_ids { + if let Some(PropertyValue::Time(value)) = properties.get(prop_id) { + return Some(*value); + } + } + None +} diff --git a/src/modules/cli/thunderbird/mod.rs b/src/modules/cli/thunderbird/mod.rs index a9bf6e1..c4d29b4 100644 --- a/src/modules/cli/thunderbird/mod.rs +++ b/src/modules/cli/thunderbird/mod.rs @@ -97,7 +97,6 @@ pub async fn handle_thunderbird_import( .interact() .unwrap() { - // 3. ζ‰§θ‘Œε―Όε…₯ for (mailbox_name, mbox_file) in mbox_tasks { println!("\nπŸš€ Importing: {}", style(&mailbox_name).cyan().bold()); run_import(account_id, &mbox_file, config, Some(mailbox_name)).await;