decode envelope subject and addresses

This commit is contained in:
Clément DOUIN
2026-03-04 11:19:05 +01:00
parent 4992c256ca
commit c953171572
5 changed files with 198 additions and 31 deletions
Generated
+133 -3
View File
@@ -20,6 +20,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -76,6 +82,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "ar_archive_writer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
dependencies = [
"object",
]
[[package]]
name = "ariadne"
version = "0.2.0"
@@ -189,6 +204,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "charset"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
"base64",
"encoding_rs",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -198,6 +223,20 @@ dependencies = [
"num-traits",
]
[[package]]
name = "chumsky"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba4a05c9ce83b07de31b31c874e87c069881ac4355db9e752e3a55c11ec75a6"
dependencies = [
"hashbrown 0.15.5",
"regex-automata 0.3.9",
"serde",
"stacker",
"unicode-ident",
"unicode-segmentation",
]
[[package]]
name = "clap"
version = "4.5.60"
@@ -414,6 +453,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_filter"
version = "1.0.0"
@@ -556,6 +604,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@@ -593,6 +643,7 @@ dependencies = [
"log",
"native-tls",
"pimalaya-toolbox",
"rfc2047-decoder",
"rustls",
"rustls-platform-verifier",
"secrecy",
@@ -1021,6 +1072,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -1191,6 +1251,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
dependencies = [
"ar_archive_writer",
"cc",
]
[[package]]
name = "quote"
version = "1.0.44"
@@ -1200,6 +1270,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
[[package]]
name = "r-efi"
version = "5.3.0"
@@ -1270,8 +1346,19 @@ checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.14",
"regex-syntax 0.8.10",
]
[[package]]
name = "regex-automata"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
]
[[package]]
@@ -1282,15 +1369,35 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.10",
]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rfc2047-decoder"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504ea008279c473dfeafce327fa26bf8825463cfcc06ef82f866187e81334d16"
dependencies = [
"base64",
"charset",
"chumsky",
"memchr",
"quoted_printable",
"thiserror 2.0.18",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -1587,6 +1694,20 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stacker"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -2010,6 +2131,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
+1
View File
@@ -46,6 +46,7 @@ io-imap = { version = "0.0.1", default-features = false, optional = true }
io-process = { version = "0.0.2", default-features = false }
io-stream = { version = "0.0.2", default-features = false, features = ["std"] }
log = "0.4"
rfc2047-decoder = "1"
native-tls = { version = "0.2", optional = true }
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret", "command"] }
rustls = { version = "0.23", default-features = false, optional = true }
+2 -2
View File
@@ -17,7 +17,7 @@ use serde::{Serialize, Serializer};
use crate::{
config::ImapConfig,
imap::{
envelope::command::list::format_addresses,
envelope::command::list::{decode_mime, format_addresses},
mailbox::arg::name::MailboxNameOptionalFlag,
stream,
},
@@ -125,7 +125,7 @@ impl EnvelopeDetailTable {
detail.date = String::from_utf8_lossy(d.as_ref()).to_string();
}
if let Some(s) = &env.subject.0 {
detail.subject = String::from_utf8_lossy(s.as_ref()).to_string();
detail.subject = decode_mime(&String::from_utf8_lossy(s.as_ref()));
}
if let Some(m) = &env.message_id.0 {
detail.message_id = String::from_utf8_lossy(m.as_ref()).to_string();
+58 -24
View File
@@ -7,16 +7,31 @@ use io_imap::{
coroutines::{fetch::*, select::*},
types::{
core::Vec1,
envelope::Address,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
sequence::SequenceSet,
},
};
use io_stream::runtimes::std::handle;
use log::debug;
use pimalaya_toolbox::terminal::printer::Printer;
use rfc2047_decoder::{Decoder, RecoverStrategy};
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalArg, imap::stream};
/// Decode RFC 2047 MIME-encoded string, falling back to original on error.
pub fn decode_mime(s: &str) -> String {
let decoder = Decoder::new().too_long_encoded_word_strategy(RecoverStrategy::Decode);
match decoder.decode(s.as_bytes()) {
Ok(s) => s,
Err(err) => {
debug!("cannot decode rfc2047 string `{s}`: {err}");
s.to_string()
}
}
}
/// List message envelopes in a mailbox.
///
/// This command displays envelopes for messages in the specified
@@ -58,9 +73,8 @@ impl ListEnvelopesCommand {
let sequence_set: SequenceSet = self.sequence.parse()?;
// FETCH envelopes
let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::Envelope,
]);
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
let mut arg = None;
let mut coroutine = ImapFetch::new(context, sequence_set, item_names, self.uid);
@@ -119,9 +133,9 @@ impl EnvelopesTable {
date = String::from_utf8_lossy(d.as_ref()).to_string();
}
if let Some(s) = &env.subject.0 {
subject = String::from_utf8_lossy(s.as_ref()).to_string();
subject = decode_mime(String::from_utf8_lossy(s.as_ref()).as_ref());
}
from = format_addresses(&env.from);
from = format_addresses_short(&env.from);
}
_ => {}
}
@@ -138,10 +152,7 @@ impl EnvelopesTable {
entries.sort_by_key(|e| e.id);
Self {
entries,
uid_mode,
}
Self { entries, uid_mode }
}
}
@@ -184,10 +195,8 @@ impl Serialize for EnvelopesTable {
}
}
use io_imap::types::envelope::Address;
pub fn format_address(addr: &Address<'_>) -> String {
// NString wraps Option<IString>, access via .0
/// Format email address from mailbox and host parts.
fn format_email(addr: &Address<'_>) -> String {
let mailbox = addr
.mailbox
.0
@@ -200,24 +209,49 @@ pub fn format_address(addr: &Address<'_>) -> String {
.as_ref()
.map(|h| String::from_utf8_lossy(h.as_ref()).to_string())
.unwrap_or_default();
let name = addr
.name
.0
.as_ref()
.map(|n| String::from_utf8_lossy(n.as_ref()).to_string());
let email = if !mailbox.is_empty() && !host.is_empty() {
if !mailbox.is_empty() && !host.is_empty() {
format!("{mailbox}@{host}")
} else {
mailbox
};
match name {
Some(n) if !n.is_empty() => format!("{n} <{email}>"),
_ => email,
}
}
/// Short format for list view (name OR email, not both).
pub fn format_address_short(addr: &Address<'_>) -> String {
// If name exists, show decoded name only
if let Some(n) = &addr.name.0 {
let name = decode_mime(&String::from_utf8_lossy(n.as_ref()));
if !name.is_empty() {
return name;
}
}
// Otherwise show email
format_email(addr)
}
/// Full format for detailed view (Name <email> or email).
pub fn format_address(addr: &Address<'_>) -> String {
let email = format_email(addr);
if let Some(n) = &addr.name.0 {
let name = decode_mime(&String::from_utf8_lossy(n.as_ref()));
if !name.is_empty() {
return format!("{name} <{email}>");
}
}
email
}
/// Short addresses formatter for list view.
pub fn format_addresses_short(addrs: &[Address<'_>]) -> String {
addrs
.iter()
.map(format_address_short)
.collect::<Vec<_>>()
.join(", ")
}
/// Full addresses formatter for detailed view.
pub fn format_addresses(addrs: &[Address<'_>]) -> String {
addrs
.iter()
+4 -2
View File
@@ -17,7 +17,9 @@ use serde::{Serialize, Serializer};
use crate::{
config::ImapConfig,
imap::{
envelope::command::search::parse_query, mailbox::arg::name::MailboxNameOptionalArg, stream,
envelope::command::{list::decode_mime, search::parse_query},
mailbox::arg::name::MailboxNameOptionalArg,
stream,
},
};
@@ -188,7 +190,7 @@ fn fetch_subjects(
MessageDataItem::Envelope(env) => {
// NString wraps Option<IString>, access via .0
if let Some(s) = &env.subject.0 {
subject = String::from_utf8_lossy(s.as_ref()).to_string();
subject = decode_mime(&String::from_utf8_lossy(s.as_ref()));
}
}
_ => {}