Files
Stalwart/crates/imap/src/op/fetch.rs
2024-09-26 14:49:46 +02:00

1154 lines
42 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc, time::Instant};
use crate::{
core::{SelectedMailbox, Session, SessionData},
spawn_op,
};
use ahash::AHashMap;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
parser::PushUnique,
protocol::{
expunge::Vanished,
fetch::{
self, Arguments, Attribute, BodyContents, BodyPart, BodyPartExtension, BodyPartFields,
DataItem, Envelope, FetchItem, Section,
},
Flag,
},
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
use jmap::{
blob::download::BlobDownload,
changes::{get::ChangesLookup, write::ChangeLog},
email::metadata::MessageMetadata,
services::state::StateManager,
JmapMethods,
};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
};
use mail_parser::{Address, GetHeader, HeaderName, Message, PartType};
use store::{
query::log::{Change, Query},
write::{assert::HashedValue, BatchBuilder, Bincode, F_BITMAP, F_VALUE},
};
use super::{FromModSeq, ImapContext};
impl<T: SessionStream> Session<T> {
pub async fn handle_fetch(
&mut self,
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapFetch)?;
let op_start = Instant::now();
let arguments = request.parse_fetch()?;
let (data, mailbox) = self.state.select_data();
let is_qresync = self.is_qresync;
let is_rev2 = self.version.is_rev2();
let enabled_condstore = if !self.is_condstore && arguments.changed_since.is_some()
|| arguments.attributes.contains(&Attribute::ModSeq)
{
self.is_condstore = true;
true
} else {
false
};
spawn_op!(data, {
let response = data
.fetch(
arguments,
mailbox,
is_uid,
is_qresync,
is_rev2,
enabled_condstore,
op_start,
)
.await?;
data.write_bytes(response.into_bytes()).await
})
}
}
impl<T: SessionStream> SessionData<T> {
#[allow(clippy::too_many_arguments)]
pub async fn fetch(
&self,
mut arguments: Arguments,
mailbox: Arc<SelectedMailbox>,
is_uid: bool,
is_qresync: bool,
_is_rev2: bool,
enabled_condstore: bool,
op_start: Instant,
) -> trc::Result<StatusResponse> {
// Validate VANISHED parameter
if arguments.include_vanished {
if !is_qresync {
return Err(trc::ImapEvent::Error
.into_err()
.details("Enable QRESYNC first to use the VANISHED parameter.")
.ctx(trc::Key::Type, ResponseType::Bad)
.id(arguments.tag));
} else if !is_uid {
return Err(trc::ImapEvent::Error
.into_err()
.details("VANISHED parameter is only available for UID FETCH.")
.ctx(trc::Key::Type, ResponseType::Bad)
.id(arguments.tag));
}
}
// Resync messages if needed
let account_id = mailbox.id.account_id;
let mut modseq = self
.synchronize_messages(&mailbox)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Convert IMAP ids to JMAP ids.
let mut ids = mailbox
.sequence_to_ids(&arguments.sequence_set, is_uid)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Convert state to modseq
if let Some(changed_since) = arguments.changed_since {
// Obtain changes since the modseq.
let changelog = self
.server
.changes_(
account_id,
Collection::Email,
Query::from_modseq(changed_since),
)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Process changes
let mut changed_ids = AHashMap::new();
let mut has_vanished = false;
for change in changelog.changes {
match change {
Change::Insert(id) | Change::Update(id) | Change::ChildUpdate(id) => {
let id = (id & u32::MAX as u64) as u32;
if let Some(uid) = ids.get(&id) {
changed_ids.insert(id, *uid);
}
if !has_vanished {
has_vanished = matches!(change, Change::Update(_));
}
}
Change::Delete(_) => {
has_vanished = true;
}
}
}
// Send vanished UIDs
if arguments.include_vanished && has_vanished {
// Add to vanished all known destroyed Ids
let vanished = mailbox
.sequence_expand_missing(&arguments.sequence_set, true)
.await;
if !vanished.is_empty() {
let mut buf = Vec::with_capacity(vanished.len() * 3);
Vanished {
earlier: true,
ids: vanished,
}
.serialize(&mut buf);
self.write_bytes(buf).await?;
}
}
// Filter out ids without changes
if changed_ids.is_empty() {
// Condstore was just enabled, return highest modseq.
if enabled_condstore {
self.write_bytes(
StatusResponse::ok("Highest Modseq")
.with_code(ResponseCode::highest_modseq(modseq))
.into_bytes(),
)
.await?;
}
trc::event!(
Imap(trc::ImapEvent::Fetch),
SpanId = self.session_id,
AccountId = account_id,
MailboxId = mailbox.id.mailbox_id,
Elapsed = op_start.elapsed()
);
return Ok(
StatusResponse::completed(Command::Fetch(is_uid)).with_tag(arguments.tag)
);
}
ids = changed_ids;
arguments.attributes.push_unique(Attribute::ModSeq);
}
// Build properties list
let mut set_seen_flags = false;
let mut needs_thread_id = false;
let mut needs_blobs = false;
for attribute in &arguments.attributes {
match attribute {
Attribute::BodySection { sections, .. }
if sections.first().map_or(false, |s| {
matches!(s, Section::Header | Section::HeaderFields { .. })
}) => {}
Attribute::Body | Attribute::BodyStructure | Attribute::BinarySize { .. } => {
/*
Note that this did not result in \Seen being set, because
RFC822.HEADER response data occurs as a result of a FETCH
of RFC822.HEADER. BODY[HEADER] response data occurs as a
result of a FETCH of BODY[HEADER] (which sets \Seen) or
BODY.PEEK[HEADER] (which does not set \Seen).
*/
needs_blobs = true;
}
Attribute::BodySection { peek, .. } | Attribute::Binary { peek, .. } => {
if mailbox.is_select && !*peek {
set_seen_flags = true;
}
needs_blobs = true;
}
Attribute::Rfc822Text | Attribute::Rfc822 => {
if mailbox.is_select {
set_seen_flags = true;
}
needs_blobs = true;
}
Attribute::ThreadId => {
needs_thread_id = true;
}
_ => (),
}
}
if set_seen_flags
&& !self
.check_mailbox_acl(
mailbox.id.account_id,
mailbox.id.mailbox_id,
Acl::ModifyItems,
)
.await
.imap_ctx(&arguments.tag, trc::location!())?
{
set_seen_flags = false;
}
if is_uid {
if arguments.attributes.is_empty() {
arguments.attributes.push(Attribute::Flags);
} else if !arguments.attributes.contains(&Attribute::Uid) {
arguments.attributes.insert(0, Attribute::Uid);
}
}
let mut set_seen_ids = Vec::new();
// Process each message
let mut ids = ids
.into_iter()
.map(|(id, imap_id)| (imap_id.seqnum, imap_id.uid, id))
.collect::<Vec<_>>();
ids.sort_unstable_by_key(|(seqnum, _, _)| *seqnum);
let fetched_ids = ids
.iter()
.map(|id| trc::Value::from(id.2))
.collect::<Vec<_>>();
for (seqnum, uid, id) in ids {
// Obtain attributes and keywords
let (email, keywords) = if let (Some(email), Some(keywords)) = (
self.server
.get_property::<Bincode<MessageMetadata>>(
account_id,
Collection::Email,
id,
&Property::BodyStructure,
)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
id,
&Property::Keywords,
)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
) {
(email.inner, keywords)
} else {
trc::event!(
Store(trc::StoreEvent::NotFound),
AccountId = account_id,
DocumentId = id,
Collection = Collection::Email,
Details = "Message metadata not found.",
CausedBy = trc::location!(),
);
continue;
};
// Fetch and parse blob
let raw_message = if needs_blobs {
// Retrieve raw message if needed
match self
.server
.get_blob(&email.blob_hash, 0..usize::MAX)
.await
.imap_ctx(&arguments.tag, trc::location!())?
{
Some(raw_message) => raw_message,
None => {
trc::event!(
Store(trc::StoreEvent::NotFound),
AccountId = account_id,
DocumentId = id,
Collection = Collection::Email,
BlobId = email.blob_hash.to_hex(),
Details = "Blob not found.",
CausedBy = trc::location!(),
);
continue;
}
}
} else {
email.raw_headers
};
let message = email.contents.into_message(&raw_message);
// Build response
let mut items = Vec::with_capacity(arguments.attributes.len());
let set_seen_flag =
set_seen_flags && !keywords.inner.iter().any(|k| k == &Keyword::Seen);
let thread_id = if needs_thread_id || set_seen_flag {
if let Some(thread_id) = self
.server
.get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId)
.await
.imap_ctx(&arguments.tag, trc::location!())?
{
thread_id
} else {
continue;
}
} else {
0
};
for attribute in &arguments.attributes {
match attribute {
Attribute::Envelope => {
items.push(DataItem::Envelope {
envelope: message.envelope(),
});
}
Attribute::Flags => {
let mut flags = keywords
.inner
.iter()
.map(|k| Flag::from(k.clone()))
.collect::<Vec<_>>();
if set_seen_flag {
flags.push(Flag::Seen);
}
items.push(DataItem::Flags { flags });
}
Attribute::InternalDate => {
items.push(DataItem::InternalDate {
date: email.received_at as i64,
});
}
Attribute::Preview { .. } => {
items.push(DataItem::Preview {
contents: if !email.preview.is_empty() {
Some(email.preview.as_bytes().into())
} else {
None
},
});
}
Attribute::Rfc822Size => {
items.push(DataItem::Rfc822Size { size: email.size });
}
Attribute::Uid => {
items.push(DataItem::Uid { uid });
}
Attribute::Rfc822 => {
items.push(DataItem::Rfc822 {
contents: raw_message.as_slice().into(),
});
}
Attribute::Rfc822Header => {
let message = message.root_part();
if let Some(header) =
raw_message.get(message.offset_header..message.offset_body)
{
items.push(DataItem::Rfc822Header {
contents: header.into(),
});
}
}
Attribute::Rfc822Text => {
items.push(DataItem::Rfc822Text {
contents: raw_message.as_slice().into(),
});
}
Attribute::Body => {
items.push(DataItem::Body {
part: message.body_structure(false),
});
}
Attribute::BodyStructure => {
items.push(DataItem::BodyStructure {
part: message.body_structure(true),
});
}
Attribute::BodySection {
sections, partial, ..
} => {
if let Some(contents) = message.body_section(sections, *partial) {
items.push(DataItem::BodySection {
sections: sections.to_vec(),
origin_octet: partial.map(|(start, _)| start),
contents,
});
}
}
Attribute::Binary {
sections, partial, ..
} => match message.binary(sections, *partial) {
Ok(Some(contents)) => {
items.push(DataItem::Binary {
sections: sections.to_vec(),
offset: partial.map(|(start, _)| start),
contents,
});
}
Err(_) => {
self.write_error(
trc::ImapEvent::Error
.into_err()
.details(format!(
"Failed to decode part {} of message {}.",
sections
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join("."),
if is_uid { uid } else { seqnum }
))
.code(ResponseCode::UnknownCte),
)
.await?;
continue;
}
_ => (),
},
Attribute::BinarySize { sections } => {
if let Some(size) = message.binary_size(sections) {
items.push(DataItem::BinarySize {
sections: sections.to_vec(),
size,
});
}
}
Attribute::ModSeq => {
if let Ok(Some(modseq)) = self
.server
.get_property::<u64>(account_id, Collection::Email, id, Property::Cid)
.await
{
items.push(DataItem::ModSeq { modseq: modseq + 1 });
}
}
Attribute::EmailId => {
items.push(DataItem::EmailId {
email_id: Id::from_parts(account_id, id).to_string(),
});
}
Attribute::ThreadId => {
items.push(DataItem::ThreadId {
thread_id: Id::from_parts(account_id, thread_id).to_string(),
});
}
}
}
// Add flags to the response if the message was unseen
if set_seen_flag && !arguments.attributes.contains(&Attribute::Flags) {
let mut flags = keywords
.inner
.iter()
.map(|k| Flag::from(k.clone()))
.collect::<Vec<_>>();
flags.push(Flag::Seen);
items.push(DataItem::Flags { flags });
}
// Serialize fetch item
let mut buf = Vec::with_capacity(128);
FetchItem { id: seqnum, items }.serialize(&mut buf);
self.write_bytes(buf).await?;
// Add to set flags
if set_seen_flag {
set_seen_ids.push((Id::from_parts(thread_id, id), keywords));
}
}
// Set Seen ids
if !set_seen_ids.is_empty() {
let mut changelog = self
.server
.begin_changes(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
for (id, mut keywords) in set_seen_ids {
keywords.inner.push(Keyword::Seen);
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Email)
.update_document(id.document_id())
.assert_value(Property::Keywords, &keywords)
.value(Property::Keywords, keywords.inner, F_VALUE)
.value(Property::Keywords, Keyword::Seen, F_BITMAP)
.value(Property::Cid, changelog.change_id, F_VALUE);
match self.server.write_batch(batch).await {
Ok(_) => {
changelog.log_update(Collection::Email, id);
}
Err(err) => {
if !err.is_assertion_failure() {
return Err(err.id(arguments.tag));
}
}
}
}
if !changelog.is_empty() {
// Write changes
let change_id = self
.server
.commit_changes(account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
modseq = change_id.into();
self.server
.broadcast_state_change(
StateChange::new(account_id).with_change(DataType::Email, change_id),
)
.await;
}
}
trc::event!(
Imap(trc::ImapEvent::Fetch),
SpanId = self.session_id,
AccountId = account_id,
MailboxId = mailbox.id.mailbox_id,
DocumentId = fetched_ids,
Details = arguments
.attributes
.iter()
.map(|c| trc::Value::from(format!("{c:?}")))
.collect::<Vec<_>>(),
Elapsed = op_start.elapsed()
);
// Condstore was enabled with this command
if enabled_condstore {
self.write_bytes(
StatusResponse::ok("Highest Modseq")
.with_code(ResponseCode::highest_modseq(modseq))
.into_bytes(),
)
.await?;
}
Ok(StatusResponse::completed(Command::Fetch(is_uid)).with_tag(arguments.tag))
}
}
#[allow(clippy::result_unit_err)]
pub trait AsImapDataItem<'x> {
fn body_structure(&self, is_extended: bool) -> BodyPart;
fn body_section<'z: 'x>(
&'z self,
sections: &[Section],
partial: Option<(u32, u32)>,
) -> Option<Cow<'x, [u8]>>;
fn binary(
&self,
sections: &[u32],
partial: Option<(u32, u32)>,
) -> Result<Option<BodyContents>, ()>;
fn binary_size(&self, sections: &[u32]) -> Option<usize>;
fn as_body_part(&self, part_id: usize, is_extended: bool) -> BodyPart;
fn envelope(&self) -> Envelope;
}
impl<'x> AsImapDataItem<'x> for Message<'x> {
fn body_structure(&self, is_extended: bool) -> BodyPart {
let mut stack = Vec::new();
let mut parts = [0].iter();
let mut message = self;
let mut root_part = None;
loop {
while let Some(part_id) = parts.next() {
let mut part = message.as_body_part(*part_id, is_extended);
match &message.parts[*part_id].body {
PartType::Message(nested_message) => {
part.set_envelope(nested_message.envelope());
if let Some(root_part) = root_part {
stack.push((root_part, parts, message.into()));
}
root_part = part.into();
parts = [0].iter();
message = nested_message;
continue;
}
PartType::Multipart(subparts) => {
if let Some(root_part) = root_part {
stack.push((root_part, parts, None));
}
root_part = part.into();
parts = subparts.iter();
continue;
}
_ => (),
}
if let Some(root_part) = &mut root_part {
root_part.add_part(part);
} else {
return part;
}
}
if let Some((mut prev_root_part, prev_parts, prev_message)) = stack.pop() {
if let Some(prev_message) = prev_message {
message = prev_message;
}
prev_root_part.add_part(root_part.unwrap());
parts = prev_parts;
root_part = prev_root_part.into();
} else {
break;
}
}
root_part.unwrap()
}
fn as_body_part(&self, part_id: usize, is_extended: bool) -> BodyPart {
let part = &self.parts[part_id];
let body = self.raw_message.get(part.offset_body..part.offset_end);
let (is_multipart, is_text) = match &part.body {
PartType::Text(_) | PartType::Html(_) => (false, true),
PartType::Multipart(_) => (true, false),
_ => (false, false),
};
let content_type = part
.headers
.header_value(&HeaderName::ContentType)
.and_then(|ct| ct.as_content_type());
let mut body_md5 = None;
let mut extension = BodyPartExtension::default();
let mut fields = BodyPartFields::default();
if !is_multipart || is_extended {
fields.body_parameters = content_type.as_ref().and_then(|ct| {
ct.attributes.as_ref().map(|at| {
at.iter()
.map(|(h, v)| (h.as_ref().into(), v.as_ref().into()))
.collect::<Vec<_>>()
})
})
}
if !is_multipart {
fields.body_subtype = content_type
.as_ref()
.and_then(|ct| ct.c_subtype.as_ref().map(|cs| cs.as_ref().into()));
fields.body_id = part
.headers
.header_value(&HeaderName::ContentId)
.and_then(|id| id.as_text().map(|id| format!("<{}>", id).into()));
fields.body_description = part
.headers
.header_value(&HeaderName::ContentDescription)
.and_then(|ct| ct.as_text().map(|ct| ct.into()));
fields.body_encoding = part
.headers
.header_value(&HeaderName::ContentTransferEncoding)
.and_then(|ct| ct.as_text().map(|ct| ct.into()));
fields.body_size_octets = body.as_ref().map(|b| b.len()).unwrap_or(0);
if is_text {
if fields.body_subtype.is_none() {
fields.body_subtype = Some("plain".into());
}
if fields.body_encoding.is_none() {
fields.body_encoding = Some("7bit".into());
}
if fields.body_parameters.is_none() {
fields.body_parameters = Some(vec![("charset".into(), "us-ascii".into())]);
}
}
}
if is_extended {
if !is_multipart {
body_md5 = body
.as_ref()
.map(|b| format!("{:x}", md5::compute(b)).into());
}
extension.body_disposition = part
.headers
.header_value(&HeaderName::ContentDisposition)
.and_then(|cd| cd.as_content_type())
.map(|cd| {
(
cd.c_type.as_ref().into(),
cd.attributes
.as_ref()
.map(|at| {
at.iter()
.map(|(h, v)| (h.as_ref().into(), v.as_ref().into()))
.collect::<Vec<_>>()
})
.unwrap_or_default(),
)
});
extension.body_language = part
.headers
.header_value(&HeaderName::ContentLanguage)
.and_then(|hv| {
hv.as_text_list()
.map(|list| list.into_iter().map(|item| item.into()).collect())
});
extension.body_location = part
.headers
.header_value(&HeaderName::ContentLocation)
.and_then(|ct| ct.as_text().map(|ct| ct.into()));
}
match &part.body {
PartType::Multipart(parts) => BodyPart::Multipart {
body_parts: Vec::with_capacity(parts.len()),
body_subtype: content_type
.as_ref()
.and_then(|ct| ct.c_subtype.as_ref().map(|cs| cs.as_ref().into()))
.unwrap_or_else(|| "".into()),
body_parameters: fields.body_parameters,
extension,
},
PartType::Message(_) => BodyPart::Message {
fields,
envelope: None,
body: None,
body_size_lines: 0,
body_md5,
extension,
},
_ => {
if is_text {
BodyPart::Text {
fields,
body_size_lines: body
.as_ref()
.map(|b| b.iter().filter(|&&ch| ch == b'\n').count())
.unwrap_or(0),
body_md5,
extension,
}
} else {
BodyPart::Basic {
body_type: content_type
.as_ref()
.map(|ct| Cow::from(ct.c_type.as_ref())),
fields,
body_md5,
extension,
}
}
}
}
}
fn body_section<'z: 'x>(
&'z self,
sections: &[Section],
partial: Option<(u32, u32)>,
) -> Option<Cow<'x, [u8]>> {
let mut part = self.root_part();
if sections.is_empty() {
return Some(
get_partial_bytes(
self.raw_message.get(part.offset_header..part.offset_end)?,
partial,
)
.into(),
);
}
let mut message = self;
let mut sections_iter = sections.iter().enumerate().peekable();
while let Some((section_num, section)) = sections_iter.next() {
match section {
Section::Part { num } => {
part = if let Some(sub_part_ids) = part.sub_parts() {
sub_part_ids
.get((*num).saturating_sub(1) as usize)
.and_then(|pos| message.parts.get(*pos))
} else if *num == 1 && (section_num == sections.len() - 1 || part.is_message())
{
Some(part)
} else {
None
}?;
if let (
PartType::Message(nested_message),
Some((
_,
Section::Part { .. }
| Section::Header
| Section::HeaderFields { .. }
| Section::Text,
)),
) = (&part.body, sections_iter.peek())
{
message = nested_message;
part = message.root_part();
}
}
Section::Header => {
return Some(
get_partial_bytes(
message
.raw_message
.get(part.offset_header..part.offset_body)?,
partial,
)
.into(),
);
}
Section::HeaderFields { not, fields } => {
let mut headers =
Vec::with_capacity(part.offset_body.saturating_sub(part.offset_header));
for header in &part.headers {
let header_name = header.name.as_str();
if fields.iter().any(|f| header_name.eq_ignore_ascii_case(f)) != *not {
headers.extend_from_slice(header_name.as_bytes());
headers.push(b':');
headers.extend_from_slice(
message
.raw_message
.get(header.offset_start..header.offset_end)
.unwrap_or(b""),
);
}
}
headers.extend_from_slice(b"\r\n");
return Some(if partial.is_none() {
headers.into()
} else {
get_partial_bytes(&headers, partial).to_vec().into()
});
}
Section::Text => {
return Some(
get_partial_bytes(
message.raw_message.get(part.offset_body..part.offset_end)?,
partial,
)
.into(),
);
}
Section::Mime => {
let mut headers =
Vec::with_capacity(part.offset_body.saturating_sub(part.offset_header));
for header in &part.headers {
if header.name.is_mime_header()
|| header.name.as_str().starts_with("Content-")
{
headers.extend_from_slice(header.name.as_str().as_bytes());
headers.extend_from_slice(b":");
headers.extend_from_slice(
message
.raw_message
.get(header.offset_start..header.offset_end)
.unwrap_or(b""),
);
}
}
headers.extend_from_slice(b"\r\n");
return Some(if partial.is_none() {
headers.into()
} else {
get_partial_bytes(&headers, partial).to_vec().into()
});
}
}
}
// BODY[x] should return both headers and body, but most clients
// expect BODY[x] to return only the body, just like BOXY[x.TEXT] does.
Some(
get_partial_bytes(
message.raw_message.get(part.offset_body..part.offset_end)?,
partial,
)
.into(),
)
}
fn binary(
&self,
sections: &[u32],
partial: Option<(u32, u32)>,
) -> Result<Option<BodyContents>, ()> {
let mut message = self;
let mut part = self.root_part();
let mut sections_iter = sections.iter().enumerate().peekable();
while let Some((section_num, num)) = sections_iter.next() {
part = if let Some(sub_part_ids) = part.sub_parts() {
if let Some(part) = sub_part_ids
.get((*num).saturating_sub(1) as usize)
.and_then(|pos| message.parts.get(*pos))
{
part
} else {
return Ok(None);
}
} else if *num == 1 && (section_num == sections.len() - 1 || part.is_message()) {
part
} else {
return Ok(None);
};
if let (PartType::Message(nested_message), Some(_)) = (&part.body, sections_iter.peek())
{
message = nested_message;
part = message.root_part();
}
}
if !part.is_encoding_problem {
Ok(match &part.body {
PartType::Text(text) | PartType::Html(text) => BodyContents::Text(
String::from_utf8_lossy(get_partial_bytes(text.as_bytes(), partial)),
)
.into(),
PartType::Binary(bytes) | PartType::InlineBinary(bytes) => {
BodyContents::Bytes(get_partial_bytes(bytes.as_ref(), partial).into()).into()
}
PartType::Message(message) => BodyContents::Bytes(
get_partial_bytes(
message
.raw_message
.get(
message.root_part().raw_header_offset()
..message.root_part().raw_end_offset(),
)
.unwrap_or_default(),
partial,
)
.into(),
)
.into(),
PartType::Multipart(_) => BodyContents::Bytes(
get_partial_bytes(
message
.raw_message
.get(part.raw_header_offset()..part.raw_end_offset())
.unwrap_or_default(),
partial,
)
.into(),
)
.into(),
})
} else {
Err(())
}
}
fn binary_size(&self, sections: &[u32]) -> Option<usize> {
let mut message = self;
let mut part = self.root_part();
let mut sections_iter = sections.iter().enumerate().peekable();
while let Some((section_num, num)) = sections_iter.next() {
part = if let Some(sub_part_ids) = part.sub_parts() {
sub_part_ids
.get((*num).saturating_sub(1) as usize)
.and_then(|pos| message.parts.get(*pos))
} else if *num == 1 && (section_num == sections.len() - 1 || part.is_message()) {
Some(part)
} else {
None
}?;
if let (PartType::Message(nested_message), Some(_)) = (&part.body, sections_iter.peek())
{
message = nested_message;
part = message.root_part();
}
}
match &part.body {
PartType::Text(text) | PartType::Html(text) => text.len(),
PartType::Binary(bytes) | PartType::InlineBinary(bytes) => bytes.len(),
PartType::Message(message) => message.root_part().raw_len(),
PartType::Multipart(_) => part.raw_len(),
}
.into()
}
fn envelope(&self) -> Envelope {
Envelope {
date: self.date().cloned(),
subject: self.subject().map(|s| s.into()),
from: self
.header_values(HeaderName::From)
.flat_map(|a| a.as_imap_address())
.collect(),
sender: self
.header_values(HeaderName::Sender)
.flat_map(|a| a.as_imap_address())
.collect(),
reply_to: self
.header_values(HeaderName::ReplyTo)
.flat_map(|a| a.as_imap_address())
.collect(),
to: self
.header_values(HeaderName::To)
.flat_map(|a| a.as_imap_address())
.collect(),
cc: self
.header_values(HeaderName::Cc)
.flat_map(|a| a.as_imap_address())
.collect(),
bcc: self
.header_values(HeaderName::Bcc)
.flat_map(|a| a.as_imap_address())
.collect(),
in_reply_to: self.in_reply_to().as_text_list().map(|list| {
let mut irt = String::with_capacity(list.len() * 10);
for (pos, l) in list.iter().enumerate() {
if pos > 0 {
irt.push(' ');
}
irt.push('<');
irt.push_str(l.as_ref());
irt.push('>');
}
irt.into()
}),
message_id: self.message_id().map(|id| format!("<{}>", id).into()),
}
}
}
#[inline(always)]
fn get_partial_bytes(bytes: &[u8], partial: Option<(u32, u32)>) -> &[u8] {
if let Some((start, end)) = partial {
bytes
.get(start as usize..std::cmp::min((start + end) as usize, bytes.len()))
.unwrap_or_default()
} else {
bytes
}
}
trait AsImapAddress {
fn as_imap_address(&self) -> Vec<fetch::Address>;
}
impl AsImapAddress for mail_parser::HeaderValue<'_> {
fn as_imap_address(&self) -> Vec<fetch::Address> {
let mut addresses = Vec::new();
match self {
mail_parser::HeaderValue::Address(Address::List(list)) => {
for addr in list {
if let Some(email) = &addr.address {
addresses.push(fetch::Address::Single(fetch::EmailAddress {
name: addr.name.as_ref().map(|n| n.as_ref().into()),
address: email.as_ref().into(),
}));
}
}
}
mail_parser::HeaderValue::Address(Address::Group(list)) => {
for group in list {
addresses.push(fetch::Address::Group(fetch::AddressGroup {
name: group.name.as_ref().map(|n| n.as_ref().into()),
addresses: group
.addresses
.iter()
.filter_map(|addr| {
fetch::EmailAddress {
name: addr.name.as_ref().map(|n| n.as_ref().into()),
address: addr.address.as_ref()?.as_ref().into(),
}
.into()
})
.collect(),
}));
}
}
_ => (),
}
addresses
}
}