/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::message::metadata::{ ArchivedMessageMetadataContents, ArchivedMetadataHeaderValue, ArchivedMetadataPartType, PART_ENCODING_BASE64, PART_ENCODING_QP, PART_SIZE_MASK, }; use jmap_proto::object::email::{EmailProperty, EmailValue}; use jmap_tools::{Map, Value}; use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType}; use types::blob::BlobId; use utils::chained_bytes::ChainedBytes; use super::headers::HeaderToValue; pub trait ToBodyPart { fn to_body_part( &self, part_id: u32, properties: &[EmailProperty], raw_message: &ChainedBytes<'_>, blob_id: &BlobId, blob_body_offset: isize, ) -> Value<'static, EmailProperty, EmailValue>; } impl ToBodyPart for Vec> { fn to_body_part( &self, part_id: u32, properties: &[EmailProperty], raw_message: &ChainedBytes<'_>, blob_id: &BlobId, blob_body_offset: isize, ) -> Value<'static, EmailProperty, EmailValue> { let mut parts = vec![part_id].into_iter(); let mut parts_stack = Vec::new(); let mut subparts = Vec::with_capacity(1); loop { if let Some((part_id, part)) = parts .next() .map(|part_id| (part_id, &self[part_id as usize])) { let mut values = Map::with_capacity(properties.len()); let multipart = if let PartType::Multipart(parts) = &part.body { parts.into() } else { None }; for property in properties { let value = match property { EmailProperty::PartId if multipart.is_none() => part_id.to_string().into(), EmailProperty::BlobId if multipart.is_none() => { let base_offset = blob_id.start_offset() as isize + blob_body_offset; BlobId::new_section( blob_id.hash.clone(), blob_id.class.clone(), (part.offset_body as isize + base_offset) as usize, (part.offset_end as isize + base_offset) as usize, part.encoding as u8, ) .into() } EmailProperty::Size if multipart.is_none() => match &part.body { PartType::Text(text) | PartType::Html(text) => text.len(), PartType::Binary(bin) | PartType::InlineBinary(bin) => bin.len(), PartType::Message(message) => message.root_part().raw_len() as usize, PartType::Multipart(_) => 0, } .into(), EmailProperty::Name => part.attachment_name().map(|v| v.to_string()).into(), EmailProperty::Type => part .content_type() .map(|ct| { ct.subtype() .map(|st| format!("{}/{}", ct.ctype(), st)) .unwrap_or_else(|| ct.ctype().to_string()) }) .or_else(|| match &part.body { PartType::Text(_) => Some("text/plain".to_string()), PartType::Html(_) => Some("text/html".to_string()), PartType::Message(_) => Some("message/rfc822".to_string()), _ => None, }) .into(), EmailProperty::Charset => part .content_type() .and_then(|ct| ct.attribute("charset")) .or(match &part.body { PartType::Text(_) | PartType::Html(_) => Some("us-ascii"), _ => None, }) .map(|v| v.to_string()) .into(), EmailProperty::Disposition => part .content_disposition() .map(|cd| cd.ctype()) .map(|v| v.to_string()) .into(), EmailProperty::Cid => part.content_id().map(|v| v.to_string()).into(), EmailProperty::Language => match part.content_language() { HeaderValue::Text(text) => vec![text.to_string()].into(), HeaderValue::TextList(list) => list .iter() .map(|text| text.to_string().into()) .collect::>>() .into(), _ => Value::Null, }, EmailProperty::Location => { part.content_location().map(|v| v.to_string()).into() } EmailProperty::Header(_) => { part.headers.header_to_value(property, raw_message) } EmailProperty::Headers => part.headers.headers_to_value(raw_message), EmailProperty::SubParts => continue, _ => Value::Null, }; values.insert_unchecked(property.clone(), value); } subparts.push(values); if let Some(multipart) = multipart { if parts_stack.len() == 10_000 { debug_assert!(false, "Too much nesting in message metadata"); return Value::Null; } let multipart = multipart.clone(); parts_stack.push(( parts, std::mem::replace(&mut subparts, Vec::with_capacity(multipart.len())), )); parts = multipart.into_iter(); } } else if let Some((prev_parts, mut prev_subparts)) = parts_stack.pop() { prev_subparts .last_mut() .unwrap() .insert_unchecked(EmailProperty::SubParts, subparts); parts = prev_parts; subparts = prev_subparts; } else { return subparts.pop().map(Into::into).unwrap_or_default(); } } } } impl ToBodyPart for ArchivedMessageMetadataContents { fn to_body_part( &self, part_id: u32, properties: &[EmailProperty], raw_message: &ChainedBytes<'_>, blob_id: &BlobId, blob_body_offset: isize, ) -> Value<'static, EmailProperty, EmailValue> { let mut parts = vec![part_id].into_iter(); let mut parts_stack = Vec::new(); let mut subparts = Vec::with_capacity(1); loop { if let Some((part_id, part)) = parts .next() .map(|part_id| (part_id, &self.parts[part_id as usize])) { let mut values = Map::with_capacity(properties.len()); let multipart = if let ArchivedMetadataPartType::Multipart(parts) = &part.body { parts.into() } else { None }; for property in properties { let value = match property { EmailProperty::PartId if multipart.is_none() => part_id.to_string().into(), EmailProperty::BlobId if multipart.is_none() => { let base_offset = blob_id.start_offset() as isize + blob_body_offset; let flags = part.flags.to_native(); let encoding = if flags & PART_ENCODING_BASE64 != 0 { 2 } else if flags & PART_ENCODING_QP != 0 { 1 } else { 0 }; BlobId::new_section( blob_id.hash.clone(), blob_id.class.clone(), (u32::from(part.offset_body) as isize + base_offset) as usize, (u32::from(part.offset_end) as isize + base_offset) as usize, encoding, ) .into() } EmailProperty::Size if multipart.is_none() => { (part.flags.to_native() & PART_SIZE_MASK).into() } EmailProperty::Name => part.attachment_name().map(|v| v.to_string()).into(), EmailProperty::Type => part .content_type() .map(|ct| { ct.subtype() .map(|st| format!("{}/{}", ct.ctype(), st)) .unwrap_or_else(|| ct.ctype().to_string()) }) .or_else(|| match &part.body { ArchivedMetadataPartType::Text => Some("text/plain".to_string()), ArchivedMetadataPartType::Html => Some("text/html".to_string()), ArchivedMetadataPartType::Message(_) => { Some("message/rfc822".to_string()) } _ => None, }) .into(), EmailProperty::Charset => { part.content_type() .and_then(|ct| ct.attribute("charset")) .or(match &part.body { ArchivedMetadataPartType::Text | ArchivedMetadataPartType::Html => Some("us-ascii"), _ => None, }) .map(|v| v.to_string()) .into() } EmailProperty::Disposition => part .content_disposition() .map(|cd| cd.ctype()) .map(|v| v.to_string()) .into(), EmailProperty::Cid => part.content_id().map(|v| v.to_string()).into(), EmailProperty::Language => match part.content_language() { ArchivedMetadataHeaderValue::Text(text) => { vec![text.to_string()].into() } ArchivedMetadataHeaderValue::TextList(list) => list .iter() .map(|text| text.to_string().into()) .collect::>>() .into(), _ => Value::Null, }, EmailProperty::Location => { part.content_location().map(|v| v.to_string()).into() } EmailProperty::Header(_) => part.header_to_value(property, raw_message), EmailProperty::Headers => part.headers_to_value(raw_message), EmailProperty::SubParts => continue, _ => Value::Null, }; values.insert_unchecked(property.clone(), value); } subparts.push(values); if let Some(multipart) = multipart { if parts_stack.len() == 10_000 { debug_assert!(false, "Too much nesting in message metadata"); return Value::Null; } let multipart = multipart .iter() .map(|id| u16::from(id) as u32) .collect::>(); parts_stack.push(( parts, std::mem::replace(&mut subparts, Vec::with_capacity(multipart.len())), )); parts = multipart.into_iter(); } } else if let Some((prev_parts, mut prev_subparts)) = parts_stack.pop() { prev_subparts .last_mut() .unwrap() .insert_unchecked(EmailProperty::SubParts, subparts); parts = prev_parts; subparts = prev_subparts; } else { return subparts.pop().map(Into::into).unwrap_or_default(); } } } } pub trait TruncateBody { fn truncate(&self, max_len: usize) -> (bool, String); } impl TruncateBody for PartType<'_> { fn truncate(&self, max_len: usize) -> (bool, String) { match self { PartType::Text(text) => truncate_plain(text, max_len), PartType::Html(html) => truncate_html(html, max_len), PartType::Binary(bytes) | PartType::InlineBinary(bytes) => { PartType::Text(String::from_utf8_lossy(bytes)).truncate(max_len) } _ => (false, "".into()), } } } pub fn truncate_plain(text: &str, mut max_len: usize) -> (bool, String) { if max_len != 0 && text.len() > max_len { let add_dots = max_len > 6; if add_dots { max_len -= 3; } let mut result = String::with_capacity(max_len); for ch in text.chars() { if ch != '\r' { if ch.len_utf8() + result.len() > max_len { break; } result.push(ch); } } if add_dots { result.push_str("..."); } (true, result) } else { (false, text.replace('\r', "")) } } pub fn truncate_html(html: &str, mut max_len: usize) -> (bool, String) { if max_len != 0 && html.len() > max_len { let add_dots = max_len > 6; if add_dots { max_len -= 3; } let mut result = String::with_capacity(max_len); let mut in_tag = false; let mut in_comment = false; let mut last_tag_end_pos = 0; let mut cr_count = 0; for (pos, ch) in html.char_indices() { let mut set_last_tag = 0; match ch { '<' if !in_tag => { in_tag = true; if let Some("!--") = html.get(pos + 1..pos + 4) { in_comment = true; } set_last_tag = pos; } '>' if in_tag => { if in_comment { if let Some("--") = html.get(pos - 2..pos) { in_comment = false; in_tag = false; set_last_tag = pos + 1; } } else { in_tag = false; set_last_tag = pos + 1; } } '\r' => { cr_count += 1; continue; } _ => (), } if ch.len_utf8() + pos - cr_count > max_len { result.push_str( &html[0..if (in_tag || set_last_tag > 0) && last_tag_end_pos > 0 { last_tag_end_pos } else { pos }] .replace('\r', ""), ); if add_dots { result.push_str("..."); } break; } else if set_last_tag > 0 { last_tag_end_pos = set_last_tag; } } (true, result) } else { (false, html.replace('\r', "")) } }