mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-09 21:31:12 +08:00
436 lines
18 KiB
Rust
436 lines
18 KiB
Rust
/*
|
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
|
*
|
|
* This file is part of Stalwart Mail Server.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
* in the LICENSE file at the top-level directory of this distribution.
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* You can be released from the requirements of the AGPLv3 license by
|
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
|
* for more details.
|
|
*/
|
|
|
|
use jmap_proto::{
|
|
error::method::MethodError,
|
|
method::get::{GetRequest, GetResponse},
|
|
object::{email::GetArguments, Object},
|
|
types::{
|
|
acl::Acl,
|
|
blob::BlobId,
|
|
collection::Collection,
|
|
date::UTCDate,
|
|
id::Id,
|
|
keyword::Keyword,
|
|
property::{HeaderForm, Property},
|
|
value::Value,
|
|
},
|
|
};
|
|
use mail_parser::HeaderName;
|
|
use store::{write::Bincode, BlobClass};
|
|
|
|
use crate::{auth::AccessToken, email::headers::HeaderToValue, mailbox::UidMailbox, JMAP};
|
|
|
|
use super::{
|
|
body::{ToBodyPart, TruncateBody},
|
|
headers::IntoForm,
|
|
metadata::{MessageMetadata, MetadataPartType},
|
|
};
|
|
|
|
impl JMAP {
|
|
pub async fn email_get(
|
|
&self,
|
|
mut request: GetRequest<GetArguments>,
|
|
access_token: &AccessToken,
|
|
) -> Result<GetResponse, MethodError> {
|
|
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
|
let properties = request.unwrap_properties(&[
|
|
Property::Id,
|
|
Property::BlobId,
|
|
Property::ThreadId,
|
|
Property::MailboxIds,
|
|
Property::Keywords,
|
|
Property::Size,
|
|
Property::ReceivedAt,
|
|
Property::MessageId,
|
|
Property::InReplyTo,
|
|
Property::References,
|
|
Property::Sender,
|
|
Property::From,
|
|
Property::To,
|
|
Property::Cc,
|
|
Property::Bcc,
|
|
Property::ReplyTo,
|
|
Property::Subject,
|
|
Property::SentAt,
|
|
Property::HasAttachment,
|
|
Property::Preview,
|
|
Property::BodyValues,
|
|
Property::TextBody,
|
|
Property::HtmlBody,
|
|
Property::Attachments,
|
|
]);
|
|
let body_properties = request.arguments.body_properties.unwrap_or_else(|| {
|
|
vec![
|
|
Property::PartId,
|
|
Property::BlobId,
|
|
Property::Size,
|
|
Property::Name,
|
|
Property::Type,
|
|
Property::Charset,
|
|
Property::Disposition,
|
|
Property::Cid,
|
|
Property::Language,
|
|
Property::Location,
|
|
]
|
|
});
|
|
let fetch_text_body_values = request.arguments.fetch_text_body_values.unwrap_or(false);
|
|
let fetch_html_body_values = request.arguments.fetch_html_body_values.unwrap_or(false);
|
|
let fetch_all_body_values = request.arguments.fetch_all_body_values.unwrap_or(false);
|
|
let max_body_value_bytes = request.arguments.max_body_value_bytes.unwrap_or(0);
|
|
|
|
let account_id = request.account_id.document_id();
|
|
let message_ids = self
|
|
.owned_or_shared_messages(access_token, account_id, Acl::ReadItems)
|
|
.await?;
|
|
let ids = if let Some(ids) = ids {
|
|
ids
|
|
} else {
|
|
let document_ids = message_ids
|
|
.iter()
|
|
.take(self.config.get_max_objects)
|
|
.collect::<Vec<_>>();
|
|
self.get_cached_thread_ids(account_id, document_ids.iter().copied())
|
|
.await
|
|
.map_err(|err| {
|
|
tracing::error!(event = "error",
|
|
context = "store",
|
|
account_id = account_id,
|
|
error = ?err,
|
|
"Failed to retrieve thread Ids");
|
|
MethodError::ServerPartialFail
|
|
})?
|
|
.into_iter()
|
|
.filter_map(|(thread_id, document_id)| {
|
|
Id::from_parts(thread_id, document_id).into()
|
|
})
|
|
.collect()
|
|
};
|
|
let mut response = GetResponse {
|
|
account_id: request.account_id.into(),
|
|
state: self.get_state(account_id, Collection::Email).await?.into(),
|
|
list: Vec::with_capacity(ids.len()),
|
|
not_found: vec![],
|
|
};
|
|
|
|
// Check if we need to fetch the raw headers or body
|
|
let mut needs_body = false;
|
|
for property in &properties {
|
|
if matches!(
|
|
property,
|
|
Property::BodyValues
|
|
| Property::TextBody
|
|
| Property::HtmlBody
|
|
| Property::Attachments
|
|
| Property::BodyStructure
|
|
) {
|
|
needs_body = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
'outer: for id in ids {
|
|
// Obtain the email object
|
|
if !message_ids.contains(id.document_id()) {
|
|
response.not_found.push(id.into());
|
|
continue;
|
|
}
|
|
let mut metadata = match self
|
|
.get_property::<Bincode<MessageMetadata>>(
|
|
account_id,
|
|
Collection::Email,
|
|
id.document_id(),
|
|
&Property::BodyStructure,
|
|
)
|
|
.await?
|
|
{
|
|
Some(metadata) => metadata.inner,
|
|
None => {
|
|
response.not_found.push(id.into());
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// Retrieve raw message if needed
|
|
let raw_message = if needs_body {
|
|
if let Some(raw_message) = self.get_blob(&metadata.blob_hash, 0..usize::MAX).await?
|
|
{
|
|
raw_message
|
|
} else {
|
|
tracing::warn!(event = "not-found",
|
|
account_id = account_id,
|
|
collection = ?Collection::Email,
|
|
document_id = id.document_id(),
|
|
blob_id = ?metadata.blob_hash,
|
|
"Blob not found");
|
|
response.not_found.push(id.into());
|
|
continue;
|
|
}
|
|
} else {
|
|
metadata.raw_headers
|
|
};
|
|
let blob_id = BlobId {
|
|
hash: metadata.blob_hash.clone(),
|
|
class: BlobClass::Linked {
|
|
account_id,
|
|
collection: Collection::Email.into(),
|
|
document_id: id.document_id(),
|
|
},
|
|
section: None,
|
|
};
|
|
|
|
// Prepare response
|
|
let mut email = Object::with_capacity(properties.len());
|
|
for property in &properties {
|
|
match property {
|
|
Property::Id => {
|
|
email.append(Property::Id, Id::from(*id));
|
|
}
|
|
Property::ThreadId => {
|
|
email.append(Property::ThreadId, Id::from(id.prefix_id()));
|
|
}
|
|
Property::BlobId => {
|
|
email.append(Property::BlobId, blob_id.clone());
|
|
}
|
|
Property::MailboxIds => {
|
|
if let Some(mailboxes) = self
|
|
.get_property::<Vec<UidMailbox>>(
|
|
account_id,
|
|
Collection::Email,
|
|
id.document_id(),
|
|
&Property::MailboxIds,
|
|
)
|
|
.await?
|
|
.map(|ids| {
|
|
let mut obj = Object::with_capacity(ids.len());
|
|
for id in ids {
|
|
obj.append(
|
|
Property::_T(Id::from(id.mailbox_id).to_string()),
|
|
true,
|
|
);
|
|
}
|
|
Value::Object(obj)
|
|
})
|
|
{
|
|
email.append(property.clone(), mailboxes);
|
|
} else {
|
|
tracing::debug!(event = "not-found",
|
|
account_id = account_id,
|
|
collection = ?Collection::Email,
|
|
document_id = id.document_id(),
|
|
"Mailbox property not found");
|
|
response.not_found.push(id.into());
|
|
continue 'outer;
|
|
}
|
|
}
|
|
Property::Keywords => {
|
|
if let Some(keywords) = self
|
|
.get_property::<Vec<Keyword>>(
|
|
account_id,
|
|
Collection::Email,
|
|
id.document_id(),
|
|
&Property::Keywords,
|
|
)
|
|
.await?
|
|
.map(|keywords| {
|
|
let mut obj = Object::with_capacity(keywords.len());
|
|
for keyword in keywords {
|
|
obj.append(Property::_T(keyword.to_string()), true);
|
|
}
|
|
Value::Object(obj)
|
|
})
|
|
{
|
|
email.append(property.clone(), keywords);
|
|
} else {
|
|
tracing::debug!(event = "not-found",
|
|
account_id = account_id,
|
|
collection = ?Collection::Email,
|
|
document_id = id.document_id(),
|
|
"Keywords property not found");
|
|
response.not_found.push(id.into());
|
|
continue 'outer;
|
|
}
|
|
}
|
|
Property::Size => {
|
|
email.append(Property::Size, metadata.size);
|
|
}
|
|
Property::ReceivedAt => {
|
|
email.append(
|
|
Property::ReceivedAt,
|
|
Value::Date(UTCDate::from_timestamp(metadata.received_at as i64)),
|
|
);
|
|
}
|
|
Property::Preview => {
|
|
if !metadata.preview.is_empty() {
|
|
email.append(Property::Preview, std::mem::take(&mut metadata.preview));
|
|
}
|
|
}
|
|
Property::HasAttachment => {
|
|
email.append(Property::HasAttachment, metadata.has_attachments);
|
|
}
|
|
Property::Subject => {
|
|
email.append(
|
|
Property::Subject,
|
|
metadata.contents.parts[0]
|
|
.remove_header(&HeaderName::Subject)
|
|
.map(|value| value.into_form(&HeaderForm::Text))
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
Property::SentAt => {
|
|
email.append(
|
|
Property::SentAt,
|
|
metadata.contents.parts[0]
|
|
.remove_header(&HeaderName::Date)
|
|
.map(|value| value.into_form(&HeaderForm::Date))
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
Property::MessageId | Property::InReplyTo | Property::References => {
|
|
email.append(
|
|
property.clone(),
|
|
metadata.contents.parts[0]
|
|
.remove_header(&match property {
|
|
Property::MessageId => HeaderName::MessageId,
|
|
Property::InReplyTo => HeaderName::InReplyTo,
|
|
Property::References => HeaderName::References,
|
|
_ => unreachable!(),
|
|
})
|
|
.map(|value| value.into_form(&HeaderForm::MessageIds))
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
|
|
Property::Sender
|
|
| Property::From
|
|
| Property::To
|
|
| Property::Cc
|
|
| Property::Bcc
|
|
| Property::ReplyTo => {
|
|
email.append(
|
|
property.clone(),
|
|
metadata.contents.parts[0]
|
|
.remove_header(&match property {
|
|
Property::Sender => HeaderName::Sender,
|
|
Property::From => HeaderName::From,
|
|
Property::To => HeaderName::To,
|
|
Property::Cc => HeaderName::Cc,
|
|
Property::Bcc => HeaderName::Bcc,
|
|
Property::ReplyTo => HeaderName::ReplyTo,
|
|
_ => unreachable!(),
|
|
})
|
|
.map(|value| value.into_form(&HeaderForm::Addresses))
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
Property::Header(_) => {
|
|
email.append(
|
|
property.clone(),
|
|
metadata.contents.parts[0]
|
|
.headers
|
|
.header_to_value(property, &raw_message),
|
|
);
|
|
}
|
|
Property::Headers => {
|
|
email.append(
|
|
Property::Headers,
|
|
metadata.contents.parts[0]
|
|
.headers
|
|
.headers_to_value(&raw_message),
|
|
);
|
|
}
|
|
Property::TextBody | Property::HtmlBody | Property::Attachments => {
|
|
let list = match property {
|
|
Property::TextBody => &metadata.contents.text_body,
|
|
Property::HtmlBody => &metadata.contents.html_body,
|
|
Property::Attachments => &metadata.contents.attachments,
|
|
_ => unreachable!(),
|
|
}
|
|
.iter();
|
|
email.append(
|
|
property.clone(),
|
|
list.map(|part_id| {
|
|
metadata.contents.to_body_part(
|
|
*part_id,
|
|
&body_properties,
|
|
&raw_message,
|
|
&blob_id,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
}
|
|
Property::BodyStructure => {
|
|
email.append(
|
|
Property::BodyStructure,
|
|
metadata.contents.to_body_part(
|
|
0,
|
|
&body_properties,
|
|
&raw_message,
|
|
&blob_id,
|
|
),
|
|
);
|
|
}
|
|
Property::BodyValues => {
|
|
let mut body_values = Object::with_capacity(metadata.contents.parts.len());
|
|
for (part_id, part) in metadata.contents.parts.iter().enumerate() {
|
|
if ((metadata.contents.html_body.contains(&part_id)
|
|
&& (fetch_all_body_values || fetch_html_body_values))
|
|
|| (metadata.contents.text_body.contains(&part_id)
|
|
&& (fetch_all_body_values || fetch_text_body_values)))
|
|
&& matches!(
|
|
part.body,
|
|
MetadataPartType::Text | MetadataPartType::Html
|
|
)
|
|
{
|
|
let (is_truncated, value) = part
|
|
.decode_contents(&raw_message)
|
|
.truncate(max_body_value_bytes);
|
|
body_values.append(
|
|
Property::_T(part_id.to_string()),
|
|
Object::with_capacity(3)
|
|
.with_property(
|
|
Property::IsEncodingProblem,
|
|
part.is_encoding_problem,
|
|
)
|
|
.with_property(Property::IsTruncated, is_truncated)
|
|
.with_property(Property::Value, value),
|
|
);
|
|
}
|
|
}
|
|
email.append(Property::BodyValues, body_values);
|
|
}
|
|
|
|
_ => {
|
|
return Err(MethodError::InvalidArguments(format!(
|
|
"Invalid property {property:?}"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
response.list.push(email);
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
}
|