use chrono::DateTime; use mailparse::{addrparse_header, body::Body, dateparse, DispositionType, ParsedMail}; use crate::error::{Error, Result}; use crate::pb3::jwebmail::mimeheader::ContentDisposition::*; use crate::pb3::jwebmail::{ mail_body::Multipart, mail_header::MailAddr, ListMailHeader, MIMEHeader, MIMEPart, Mail, MailBody, MailHeader, }; fn parse_mail_addrs(inp: &mailparse::MailHeader) -> Result> { 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| { let mut r = MailAddr::new(); r.name = Some(s.display_name.unwrap_or_default()); r.address = s.addr; r }) .collect(), mailparse::MailAddr::Single(s) => { let mut addr = MailAddr::new(); addr.name = Some(s.display_name.unwrap_or_default()); addr.address = s.addr; vec![addr] } }) .collect()) } // ---------------- fn get_received(me: &mut maildir::MailEntry) -> i64 { me.received().unwrap_or_else(|_| { let mut id = me.id(); id = &id[..id.find('.').unwrap()]; id.parse().unwrap_or_default() }) } pub fn me_to_lmh(mut me: maildir::MailEntry) -> Result { let mut lmh = ListMailHeader::new(); lmh.byte_size = me.path().metadata()?.len(); lmh.unread = !me.is_seen(); lmh.rec_date = DateTime::from_timestamp(get_received(&mut me), 0) .unwrap() .to_rfc3339(); lmh.mid = me.id().to_owned(); lmh.header = Some(parse_mail_header(&me.parsed()?)?).into(); Ok(lmh) } pub fn parse_mail_content(v: &ParsedMail) -> Result { let mut c = MIMEHeader::new(); { let mut val = v.ctype.mimetype.clone(); if let Some(i) = val.find(';') { val.truncate(i); } let j = val.find('/').unwrap(); c.subtype = val.split_off(j + 1); val.pop(); c.maintype = val; } match v.get_content_disposition().disposition { DispositionType::Inline => c.contentdispo = CONTENT_DISPOSITION_INLINE.into(), DispositionType::Attachment => { c.contentdispo = CONTENT_DISPOSITION_ATTACHMENT.into(); if let Some(fname) = v.get_content_disposition().params.remove("filename") { c.file_name = Some(fname); } } _ => {} } for h in &v.headers { let mut key = h.get_key(); let val = h.get_value(); key.make_ascii_lowercase(); if key == "filename" { c.file_name = Some(val); } } Ok(c) } fn parse_mail_header(pm: &ParsedMail) -> Result { let v = &pm.headers; let mut mh = MailHeader::new(); let mut mimeh = MIMEHeader::new(); { let mut val = pm.ctype.mimetype.clone(); if let Some(i) = val.find(';') { val.truncate(i); } let j = val.find('/').unwrap(); mimeh.subtype = val.split_off(j + 1); val.pop(); mimeh.maintype = val; } 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() { "date" => { mh.send_date = DateTime::from_timestamp(dateparse(&val)?, 0) .unwrap() .to_rfc3339() } "from" => { if !mh.written_from.is_empty() { return Err(Error::SortOrder("from already set".into())); } mh.written_from = parse_mail_addrs(y)? } "sender" => mh.sender = parse_mail_addrs(y)?.drain(0..1).next().into(), "reply-to" => mh.reply_to = parse_mail_addrs(y)?, "to" => mh.send_to = parse_mail_addrs(y)?, "cc" => mh.cc = parse_mail_addrs(y)?, "bcc" => mh.bcc = parse_mail_addrs(y)?, "subject" => { mh.subject = val; } "comments" => { mh.comments.push(val); } "keywords" => { mh.keywords.push(val); } "mime-version" => { strip_comments(&mut val); if val.trim() != "1.0" { return Err(Error::MailEntryError(maildir::MailEntryError::DateError( "unknown mime version", ))); } } "content-disposition" => { let cd = val.to_ascii_lowercase(); match cd.as_ref() { "inline" => mimeh.contentdispo = CONTENT_DISPOSITION_INLINE.into(), "attachment" => mimeh.contentdispo = CONTENT_DISPOSITION_ATTACHMENT.into(), _ => {} }; } "filename" => { mimeh.file_name = Some(val); } _ => {} }; key.clear(); } mh.mime = Some(mimeh).into(); Ok(mh) } fn parse_mail_body(pm: &ParsedMail) -> Result { let body = if pm.ctype.mimetype.starts_with("message/") { let mut mb = MailBody::new(); mb.set_mail(parsed_mail_to_mail(mailparse::parse_mail( pm.get_body()?.as_ref(), )?)?); mb } else if pm.subparts.is_empty() && pm.ctype.mimetype.starts_with("text/") { let mut mb = MailBody::new(); mb.set_discrete(pm.get_body()?); mb } else if pm.subparts.is_empty() { let b = 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!(), }; let mut mb = MailBody::new(); mb.set_discrete(b); mb } else { let mut mb = MailBody::new(); let mut mp = Multipart::new(); mp.parts = pm .subparts .iter() .map(|part| -> Result { let mut mp = MIMEPart::new(); mp.mime_header = Some(parse_mail_content(part)?).into(); mp.body = Some(parse_mail_body(part)?).into(); Ok(mp) }) .filter_map(|p| p.ok()) .collect(); mb.set_multipart(mp); mb }; 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) -> Result { let mut m = Mail::new(); m.head = Some(parse_mail_header(&pm)?).into(); m.body = Some(parse_mail_body(&pm)?).into(); Ok(m) } /* #[cfg(test)] mod tests { use super::*; #[test] fn comment() { let mut x = r#"(this is ((some) text)) a "some text with (comment \" in) quotes)(" (example) \( included) (xx)b()"#.to_owned(); strip_comments(&mut x); assert_eq!( &x, r#" a "some text with (comment \" in) quotes)(" \( included) b"# ); } #[test] fn unclosed_comment() { let mut x = "(this is (some text) example b".to_owned(); strip_comments(&mut x); assert_eq!(&x, "(this is example b"); } #[test] fn find_first_pair() { let mut r = find_pair(0, "abc def"); assert_eq!(r, None); r = find_pair(0, "abc ( def"); assert_eq!(r, None); r = find_pair(0, "abc ) def"); assert_eq!(r, None); let s = "(abc) def"; if let Some(i) = find_pair(0, s) { assert_eq!(i, 0..5); assert_eq!(&s[i], "(abc)"); } else { assert!(false, "Got None expected Some!"); } let s = "abc (def) ghi"; if let Some(i) = find_pair(0, s) { assert_eq!(i, 4..9); assert_eq!(&s[i], "(def)"); } else { assert!(false, "Got None expected Some!"); } let s = "(abc (def) ghi"; if let Some(i) = find_pair(0, s) { assert_eq!(i, 5..10); assert_eq!(&s[i], "(def)"); } else { assert!(false, "Got None expected Some!"); } let s = "abc ((def) ghi)"; if let Some(i) = find_pair(0, s) { assert_eq!(i, 4..15); assert_eq!(&s[i], "((def) ghi)"); } else { assert!(false, "Got None expected Some!"); } let s = r#" a "some text with (comment \" in) quotes)(" (example)"#; if let Some(i) = find_pair(0, s) { assert_eq!(i, 45..54); assert_eq!(&s[i], "(example)"); } else { assert!(false, "Got None expected Some!"); } } } */