use chrono::DateTime; use maildir::MailEntryError; use mailparse::{ addrparse_header, body::Body, dateparse, DispositionType, MailParseError, ParsedMail, }; use crate::de_jmhoffmann_jwebmail_mailstorage::MIMEHeader_content_dispo as CD; use crate::de_jmhoffmann_jwebmail_mailstorage::*; pub fn parse_mail_addrs( inp: &mailparse::MailHeader, ) -> std::result::Result, MailParseError> { let mut mal = addrparse_header(inp)?; Ok(mal .drain(..) .flat_map(|mail_addr| match mail_addr { mailparse::MailAddr::Group(mut g) => g .addrs .drain(..) .map(|s| MailAddr { name: Some(s.display_name.unwrap_or_default()), address: s.addr, }) .collect(), mailparse::MailAddr::Single(s) => { let addr = MailAddr { name: Some(s.display_name.unwrap_or_default()), address: s.addr, }; vec![addr] } }) .collect()) } // ---------------- pub fn me_to_lmh( mut me: maildir::MailEntry, ) -> std::result::Result { let lmh = ListMailHeader { byte_size: me.path().metadata()?.len() as i64, unread: !me.is_seen(), rec_date: DateTime::from_timestamp(me.received().unwrap_or_default(), 0) .unwrap() .to_rfc3339(), mid: me.id().to_owned(), header: parse_mail_header(&me.headers()?)?, }; Ok(lmh) } pub fn parse_mail_content(v: &ParsedMail) -> MIMEHeader { let mut val = v.ctype.mimetype.clone(); if let Some(i) = val.find(';') { val.truncate(i); } let j = val.find('/').unwrap(); let subtype = val.split_off(j + 1); val.pop(); let maintype = val; let mut file_name = None; let cd = match v.get_content_disposition().disposition { DispositionType::Inline => CD::inline, DispositionType::Attachment => { if let Some(fname) = v.get_content_disposition().params.remove("filename") { file_name = Some(fname); } CD::attachment } _ => CD::none, }; for h in &v.headers { let mut key = h.get_key(); let val = h.get_value(); key.make_ascii_lowercase(); if key == "filename" { file_name = Some(val); } } MIMEHeader { mime_type: MIMEHeader_mime_type { main_type: maintype, sub_type: subtype, }, content_dispo: cd, file_name, } } fn parse_mail_header( v: &Vec, ) -> std::result::Result { let mut sub_type = String::new(); let mut main_type = String::new(); let mut file_name = None; let mut content_dispo = CD::none; let mut cc = vec![]; let mut bcc = vec![]; let mut comments = vec![]; let mut keywords = vec![]; let mut reply_to = vec![]; let mut written_from = vec![]; let mut send_to = vec![]; let mut subject = String::new(); let mut sender = None; let mut send_date = String::new(); let mut key = String::new(); for y in v { key.push_str(&y.get_key_ref()); let mut val = y.get_value(); key.make_ascii_lowercase(); match key.as_str() { "content-type" => { if let Some(i) = val.find(';') { val.truncate(i); } let j = val.find('/').unwrap(); sub_type = val.split_off(j + 1); val.pop(); main_type = val; } "date" => { send_date = DateTime::from_timestamp(dateparse(&val)?, 0) .unwrap() .to_rfc3339() } "from" => { if !written_from.is_empty() { const FROM_MULTIPLE_TIMES_ERROR: &str = "from already set"; return Err(MailParseError::Generic(FROM_MULTIPLE_TIMES_ERROR)); } written_from = parse_mail_addrs(y)? } "sender" => sender = parse_mail_addrs(y)?.drain(0..1).next(), "reply-to" => reply_to = parse_mail_addrs(y)?, "to" => send_to = parse_mail_addrs(y)?, "cc" => cc = parse_mail_addrs(y)?, "bcc" => bcc = parse_mail_addrs(y)?, "subject" => { subject = val; } "comments" => { comments.push(val); } "keywords" => { keywords.push(val); } "mime-version" => { strip_comments(&mut val); if val.trim() != "1.0" { const UNKNOWN_MIME_VERSION_ERROR: &str = "unknown mime version"; return Err(MailParseError::Generic(UNKNOWN_MIME_VERSION_ERROR)); } } "content-disposition" => { let cd = val.to_ascii_lowercase(); match cd.as_ref() { "inline" => content_dispo = CD::inline, "attachment" => content_dispo = CD::attachment, _ => {} }; } "filename" => { file_name = Some(val); } _ => {} }; key.clear(); } // this is not correct for multipart/digest if main_type.is_empty() || sub_type.is_empty() { main_type = "text".to_string(); sub_type = "plain".to_string(); } Ok(MailHeader { written_from, sender, send_to, cc, bcc, reply_to, subject, send_date, keywords, comments, mime: MIMEHeader { mime_type: MIMEHeader_mime_type { main_type, sub_type, }, content_dispo, file_name, }, }) } fn parse_mail_body(pm: &ParsedMail) -> std::result::Result { let body = if pm.ctype.mimetype.starts_with("message/") { MailBody { mail: Some(Box::new(parsed_mail_to_mail(mailparse::parse_mail( pm.get_body()?.as_ref(), )?)?)), discrete: None, multipart: None, } } else if pm.subparts.is_empty() && pm.ctype.mimetype.starts_with("text/") { MailBody { discrete: Some(pm.get_body()?), multipart: None, mail: None, } } else if pm.subparts.is_empty() { MailBody { discrete: Some(match pm.get_body_encoded() { Body::Base64(eb) => { let db = eb.get_raw(); if db.len() < 512 * 1024 { String::from_utf8_lossy(db).into_owned() } else { String::new() } } Body::SevenBit(eb) => eb.get_as_string()?, Body::QuotedPrintable(eb) => eb.get_decoded_as_string()?, _ => todo!(), }), multipart: None, mail: None, } } else { let mp = Multipart { parts: pm .subparts .iter() .map(|part| -> std::result::Result { Ok(MIMEPart { mime_header: parse_mail_content(part), body: parse_mail_body(part)?, }) }) .filter_map(|p| p.ok()) .collect(), preamble: None, epilogue: None, }; MailBody { multipart: Some(mp), discrete: None, mail: None, } }; Ok(body) } enum FindMatchParen { Open, Close, } impl FindMatchParen { fn value(&self) -> char { match self { FindMatchParen::Open => '(', FindMatchParen::Close => ')', } } fn len(&self) -> usize { 1 } fn of_char(c: char) -> Option { match c { '(' => Some(FindMatchParen::Open), ')' => Some(FindMatchParen::Close), _ => None, } } } fn find_in_header(s: &str, f: FindMatchParen) -> Option { let mut in_q = false; let mut q_pair = false; let mut open_p = 0; for (i, c) in s.char_indices() { if q_pair { q_pair = false; continue; } match c { '\\' => { q_pair = true; } '"' => { in_q = !in_q; } _ if !in_q => { if open_p == 0 { if c == f.value() { return Some(i); } if c == FindMatchParen::Open.value() { open_p += 1; } } else { match FindMatchParen::of_char(c) { Some(FindMatchParen::Open) => open_p += 1, Some(FindMatchParen::Close) => open_p -= 1, None => {} } } } _ => {} }; } None } fn find_pair(offset: usize, s: &str) -> Option> { if let Some(open) = find_in_header(s, FindMatchParen::Open) { if let Some(mut close) = find_in_header( &s[open + FindMatchParen::Open.len()..], FindMatchParen::Close, ) { close += open + FindMatchParen::Open.len(); Some(offset + open..offset + close + FindMatchParen::Close.len()) } else { find_pair( offset + open + FindMatchParen::Open.len(), &s[open + FindMatchParen::Open.len()..], ) } } else { None } } fn strip_comments(s: &mut String) { let mut off = 0; while let Some(r) = find_pair(off, &s[off..]) { s.drain(r.clone()); off = r.start; } } pub fn parsed_mail_to_mail(pm: ParsedMail) -> std::result::Result { Ok(Mail { head: parse_mail_header(&pm.headers)?, body: parse_mail_body(&pm)?, }) }