Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
396 lines
17 KiB
Rust
396 lines
17 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* 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<MessagePart<'_>> {
|
|
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::<Vec<Value<'static, EmailProperty, EmailValue>>>()
|
|
.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::<Vec<Value<'static, EmailProperty, EmailValue>>>()
|
|
.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::<Vec<_>>();
|
|
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', ""))
|
|
}
|
|
}
|