diff --git a/Cargo.lock b/Cargo.lock index 644fe90..e25a6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -549,6 +559,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "html2text" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1637acec3b965bab873352189d887b12c87b4f8d7571f4d185e796be5654ad8" +dependencies = [ + "html5ever", + "tendril", + "thiserror 2.0.18", + "unicode-width", +] + +[[package]] +name = "html5ever" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -762,6 +796,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "mac_address" version = "1.1.8" @@ -783,6 +823,28 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "markup5ever" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e4cd8c02f18a011991a039855480c64d74291c5792fcc160d55d77dc4de4a39" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "memchr" version = "2.8.0" @@ -839,6 +901,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -1103,6 +1171,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1471,6 +1545,7 @@ version = "0.1.0" dependencies = [ "chrono", "crossterm", + "html2text", "imap", "mailparse", "native-tls", @@ -1491,6 +1566,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1553,6 +1653,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -1763,6 +1874,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1905,6 +2022,18 @@ dependencies = [ "semver", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "wezterm-bidi" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 361b287..056c3a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ native-tls = "0.2" serde = { version = "1.0", features = ["derive"] } toml = "1.0" chrono = "0.4" -mailparse = "0.15" \ No newline at end of file +mailparse = "0.15" +html2text = "0.14" \ No newline at end of file diff --git a/src/inbox.rs b/src/inbox.rs index 7d95efd..29dad7c 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -178,23 +178,31 @@ fn extract_raw_body(fetches: &[imap::types::Fetch]) -> Option> { fn extract_plain_text(raw: &[u8]) -> Result { let parsed = mailparse::parse_mail(raw).map_err(|e| e.to_string())?; - // Try to find a text/plain part - if let Some(text) = find_plain_text(&parsed) { + // Try text/plain first + if let Some(text) = find_part(&parsed, "text/plain") { return Ok(text); } - // Fallback: return the body of the top-level message + // Fall back to text/html rendered as text + if let Some(html) = find_part(&parsed, "text/html") { + return Ok(html_to_text(&html)); + } + // Last resort: top-level body parsed.get_body().map_err(|e| e.to_string()) } -fn find_plain_text(mail: &mailparse::ParsedMail) -> Option { +fn find_part(mail: &mailparse::ParsedMail, mime_type: &str) -> Option { let content_type = mail.ctype.mimetype.to_lowercase(); - if content_type == "text/plain" { + if content_type == mime_type { return mail.get_body().ok(); } for part in &mail.subparts { - if let Some(text) = find_plain_text(part) { + if let Some(text) = find_part(part, mime_type) { return Some(text); } } None } + +fn html_to_text(html: &str) -> String { + html2text::from_read(html.as_bytes(), 80).unwrap_or_else(|_| html.to_string()) +}