Import upstream v0.16.22, stripped

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.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
changes::state::JmapCacheState,
email::{PatchResult, handle_email_patch, ingested_into_object},
};
use common::{Server, auth::AccessToken};
use email::{
cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess},
message::copy::{CopyMessageError, EmailCopy},
};
use http_proto::HttpSessionData;
use jmap_proto::{
error::set::SetError,
method::{
copy::{CopyRequest, CopyResponse},
set::SetRequest,
},
object::email::{Email, EmailProperty, EmailValue},
request::{
Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod,
method::{MethodFunction, MethodName, MethodObject},
reference::MaybeResultReference,
},
};
use jmap_tools::{Key, Value};
use std::future::Future;
use trc::AddContext;
use types::acl::Acl;
use utils::map::vec_map::VecMap;
pub trait JmapEmailCopy: Sync + Send {
fn email_copy<'x>(
&self,
request: CopyRequest<'x, Email>,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod<'x>>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<CopyResponse<Email>>> + Send;
}
impl JmapEmailCopy for Server {
async fn email_copy<'x>(
&self,
request: CopyRequest<'x, Email>,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod<'x>>>,
session: &HttpSessionData,
) -> trc::Result<CopyResponse<Email>> {
let account_id = request.account_id.document_id();
let from_account_id = request.from_account_id.document_id();
if account_id == from_account_id {
return Err(trc::JmapEvent::InvalidArguments
.into_err()
.details("From accountId is equal to fromAccountId"));
}
let cache = self.get_cached_messages(account_id).await?;
let old_state = cache.assert_state(false, &request.if_in_state)?;
let mut response = CopyResponse {
from_account_id: request.from_account_id,
account_id: request.account_id,
new_state: old_state.clone(),
old_state,
created: VecMap::with_capacity(request.create.len()),
not_created: VecMap::new(),
};
let from_cache = self
.get_cached_messages(from_account_id)
.await
.caused_by(trc::location!())?;
let from_message_ids = if access_token.is_member(from_account_id) {
from_cache.email_document_ids()
} else {
from_cache.shared_messages(access_token, Acl::ReadItems)
};
let can_add_mailbox_ids = if access_token.is_shared(account_id) {
cache.shared_mailboxes(access_token, Acl::AddItems).into()
} else {
None
};
let on_success_delete = request.on_success_destroy_original.unwrap_or(false);
let mut destroy_ids = Vec::new();
'create: for (id, create) in request.create.into_valid() {
let mut from_message_id = None;
let mut mailboxes = Vec::new();
let mut keywords = Vec::new();
let mut received_at = None;
for (property, value) in create.into_expanded_object() {
match (property, value) {
(Key::Property(EmailProperty::Id), Value::Element(EmailValue::Id(src))) => {
from_message_id = Some(src);
}
(Key::Property(EmailProperty::MailboxIds), Value::Object(ids)) => {
mailboxes = ids
.into_expanded_boolean_set()
.filter_map(|id| {
id.try_into_property()?.try_into_id()?.document_id().into()
})
.collect();
}
(Key::Property(EmailProperty::Keywords), Value::Object(keywords_)) => {
keywords = keywords_
.into_expanded_boolean_set()
.filter_map(|id| id.try_into_property()?.try_into_keyword())
.collect();
}
(Key::Property(EmailProperty::Pointer(pointer)), value) => {
match handle_email_patch(&pointer, value) {
PatchResult::SetKeyword(keyword) => {
if !keywords.contains(keyword) {
keywords.push(keyword.clone());
}
}
PatchResult::RemoveKeyword(keyword) => {
keywords.retain(|k| k != keyword);
}
PatchResult::AddMailbox(id) => {
if !mailboxes.contains(&id) {
mailboxes.push(id);
}
}
PatchResult::RemoveMailbox(id) => {
mailboxes.retain(|mid| mid != &id);
}
PatchResult::Invalid(set_error) => {
response.not_created.append(id, set_error);
continue 'create;
}
}
}
(
Key::Property(EmailProperty::ReceivedAt),
Value::Element(EmailValue::Date(value)),
) => {
received_at = value.into();
}
(property, _) => {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(property.into_owned())
.with_description("Invalid property or value.".to_string()),
);
continue 'create;
}
}
}
let Some(from_message_id) = from_message_id else {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::Id)
.with_description("Missing or invalid \"id\" property."),
);
continue 'create;
};
if !from_message_ids.contains(from_message_id.document_id()) {
response.not_created.append(
id,
SetError::not_found().with_description(format!(
"Item {} not found in account {}.",
id, response.from_account_id
)),
);
continue 'create;
}
// Make sure message belongs to at least one mailbox
if mailboxes.is_empty() {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::MailboxIds)
.with_description("Message has to belong to at least one mailbox."),
);
continue 'create;
}
// Verify that the mailboxIds are valid
for mailbox_id in &mailboxes {
if !cache.has_mailbox_id(mailbox_id) {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::MailboxIds)
.with_description(format!("mailboxId {mailbox_id} does not exist.")),
);
continue 'create;
} else if matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(*mailbox_id)) {
response.not_created.append(
id,
SetError::forbidden().with_description(format!(
"You are not allowed to add messages to mailbox {mailbox_id}."
)),
);
continue 'create;
}
}
// Add response
match self
.copy_message(
from_account_id,
from_message_id.document_id(),
account_id,
mailboxes,
keywords,
received_at.map(|dt| dt.timestamp() as u64),
session.session_id,
)
.await?
{
Ok(email) => {
response
.created
.append(id, ingested_into_object(email).into());
}
Err(err) => {
response.not_created.append(
id,
match err {
CopyMessageError::NotFound => SetError::not_found()
.with_description("Message not found in account."),
CopyMessageError::OverQuota => SetError::over_quota(),
CopyMessageError::AlreadyExists(existing) => SetError::already_exists()
.with_existing_id(types::id::Id::from(existing)),
},
);
}
}
// Add to destroy list
if on_success_delete {
destroy_ids.push(MaybeInvalid::Value(from_message_id));
}
}
// Update state
if !response.created.is_empty() {
response.new_state = self.get_cached_messages(account_id).await?.get_state(false);
}
// Destroy ids
if on_success_delete && !destroy_ids.is_empty() {
*next_call = Call {
id: String::new(),
name: MethodName::new(MethodObject::Email, MethodFunction::Set),
method: RequestMethod::Set(SetRequestMethod::Email(Box::new(SetRequest {
account_id: request.from_account_id,
if_in_state: request.destroy_from_if_in_state,
create: None,
update: None,
destroy: MaybeResultReference::Value(destroy_ids).into(),
arguments: Default::default(),
}))),
}
.into();
}
Ok(response)
}
}
+449
View File
@@ -0,0 +1,449 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::changes::state::JmapCacheState;
use common::{Server, auth::AccessToken};
use email::{
cache::{MessageCacheFetch, email::MessageCacheAccess},
message::{
body::{ToBodyPart, truncate_html, truncate_plain},
headers::{HeaderToValue, IntoForm},
metadata::{
ArchivedMetadataPartType, MESSAGE_HAS_ATTACHMENT, MESSAGE_RECEIVED_MASK,
MessageMetadata, MetadataHeaderName, PART_ENCODING_PROBLEM,
},
},
};
use jmap_proto::{
method::get::{GetRequest, GetResponse},
object::email::{Email, EmailProperty, EmailValue, HeaderForm},
request::IntoValid,
types::date::UTCDate,
};
use jmap_tools::{Key, Map, Value};
use mail_parser::HeaderValue;
use std::future::Future;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::{AddContext, StoreEvent};
use types::{
acl::Acl,
blob::{BlobClass, BlobId},
blob_hash::BlobHash,
collection::Collection,
field::EmailField,
id::Id,
};
use utils::chained_bytes::ChainedBytes;
pub trait EmailGet: Sync + Send {
fn email_get(
&self,
request: GetRequest<Email>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse<Email>>> + Send;
}
impl EmailGet for Server {
async fn email_get(
&self,
mut request: GetRequest<Email>,
access_token: &AccessToken,
) -> trc::Result<GetResponse<Email>> {
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[
EmailProperty::Id,
EmailProperty::BlobId,
EmailProperty::ThreadId,
EmailProperty::MailboxIds,
EmailProperty::Keywords,
EmailProperty::Size,
EmailProperty::ReceivedAt,
EmailProperty::MessageId,
EmailProperty::InReplyTo,
EmailProperty::References,
EmailProperty::Sender,
EmailProperty::From,
EmailProperty::To,
EmailProperty::Cc,
EmailProperty::Bcc,
EmailProperty::ReplyTo,
EmailProperty::Subject,
EmailProperty::SentAt,
EmailProperty::HasAttachment,
EmailProperty::Preview,
EmailProperty::BodyValues,
EmailProperty::TextBody,
EmailProperty::HtmlBody,
EmailProperty::Attachments,
]);
let body_properties = request
.arguments
.body_properties
.map(|v| v.into_valid().collect())
.unwrap_or_else(|| {
vec![
EmailProperty::PartId,
EmailProperty::BlobId,
EmailProperty::Size,
EmailProperty::Name,
EmailProperty::Type,
EmailProperty::Charset,
EmailProperty::Disposition,
EmailProperty::Cid,
EmailProperty::Language,
EmailProperty::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 cache = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
let message_ids = if access_token.is_member(account_id) {
cache.email_document_ids()
} else {
cache.shared_messages(access_token, Acl::ReadItems)
};
let ids = if let Some(ids) = ids {
ids
} else {
cache
.emails
.items
.iter()
.take(self.core.jmap.get_max_objects)
.map(|item| Id::from_parts(item.thread_id, item.document_id))
.collect()
};
let mut response = GetResponse {
account_id: request.account_id.into(),
state: cache.get_state(false).into(),
list: Vec::with_capacity(ids.len()),
not_found: not_found_ids,
};
// Check if we need to fetch the raw headers or body
let mut needs_body = false;
for property in &properties {
if matches!(
property,
EmailProperty::BodyValues
| EmailProperty::TextBody
| EmailProperty::HtmlBody
| EmailProperty::Attachments
| EmailProperty::BodyStructure
) {
needs_body = true;
break;
}
}
for id in ids {
// Obtain the email object
if !message_ids.contains(id.document_id()) {
response.push_not_found(id);
continue;
}
let metadata_ = match self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
account_id,
Collection::Email,
id.document_id(),
EmailField::Metadata,
))
.await?
{
Some(metadata) => metadata,
None => {
response.push_not_found(id);
continue;
}
};
let metadata = metadata_
.unarchive::<MessageMetadata>()
.caused_by(trc::location!())?;
// Obtain message data
let data = match cache.email_by_id(&id.document_id()) {
Some(data) => data,
None => {
response.push_not_found(id);
continue;
}
};
// Retrieve raw message if needed
let blob_hash = BlobHash::from(&metadata.blob_hash);
let raw_body;
let mut raw_message = ChainedBytes::new(metadata.raw_headers.as_ref());
if needs_body {
raw_body = self
.blob_store()
.get_blob(blob_hash.as_slice(), 0..usize::MAX)
.await?;
if let Some(raw_body) = &raw_body {
raw_message.append(
raw_body
.get(metadata.blob_body_offset.to_native() as usize..)
.unwrap_or_default(),
);
} else {
trc::event!(
Store(StoreEvent::NotFound),
AccountId = account_id,
DocumentId = id.document_id(),
Collection = Collection::Email,
BlobId = blob_hash.to_hex(),
Details = "Blob not found.",
CausedBy = trc::location!(),
);
response.push_not_found(id);
continue;
}
}
let blob_id = BlobId {
hash: blob_hash,
class: BlobClass::Linked {
account_id,
collection: Collection::Email.into(),
document_id: id.document_id(),
},
section: None,
};
// Prepare response
let mut email: Map<'_, EmailProperty, EmailValue> =
Map::with_capacity(properties.len());
let contents = &metadata.contents[0];
let root_part = &contents.parts[0];
let blob_body_offset = metadata.blob_body_offset.to_native() as isize
- root_part.offset_body.to_native() as isize;
for property in &properties {
match property {
EmailProperty::Id => {
email.insert_unchecked(EmailProperty::Id, Id::from(*id));
}
EmailProperty::ThreadId => {
email.insert_unchecked(EmailProperty::ThreadId, Id::from(id.prefix_id()));
}
EmailProperty::BlobId => {
email.insert_unchecked(EmailProperty::BlobId, blob_id.clone());
}
EmailProperty::MailboxIds => {
let mut obj = Map::with_capacity(data.mailboxes.len());
for id in data.mailboxes.iter() {
debug_assert!(id.uid != 0);
obj.insert_unchecked(
EmailProperty::IdValue(Id::from(id.mailbox_id)),
true,
);
}
email.insert_unchecked(property.clone(), Value::Object(obj));
}
EmailProperty::Keywords => {
let mut obj = Map::with_capacity(2);
for keyword in cache.expand_keywords(data) {
obj.insert_unchecked(EmailProperty::Keyword(keyword), true);
}
email.insert_unchecked(property.clone(), Value::Object(obj));
}
EmailProperty::Size => {
email.insert_unchecked(EmailProperty::Size, data.size);
}
EmailProperty::ReceivedAt => {
email.insert_unchecked(
EmailProperty::ReceivedAt,
EmailValue::Date(UTCDate::from_timestamp(
(metadata.rcvd_attach.to_native() & MESSAGE_RECEIVED_MASK) as i64,
)),
);
}
EmailProperty::Preview => {
if !metadata.preview.is_empty() {
email.insert_unchecked(
EmailProperty::Preview,
metadata.preview.to_string(),
);
}
}
EmailProperty::HasAttachment => {
email.insert_unchecked(
EmailProperty::HasAttachment,
(metadata.rcvd_attach.to_native() & MESSAGE_HAS_ATTACHMENT) != 0,
);
}
EmailProperty::Subject => {
email.insert_unchecked(
EmailProperty::Subject,
root_part
.header_value(&MetadataHeaderName::Subject)
.map(|value| HeaderValue::from(value).into_form(&HeaderForm::Text))
.unwrap_or_default(),
);
}
EmailProperty::SentAt => {
email.insert_unchecked(
EmailProperty::SentAt,
root_part
.header_value(&MetadataHeaderName::Date)
.map(|value| HeaderValue::from(value).into_form(&HeaderForm::Date))
.unwrap_or_default(),
);
}
EmailProperty::MessageId
| EmailProperty::InReplyTo
| EmailProperty::References => {
email.insert_unchecked(
property.clone(),
root_part
.header_value(&match property {
EmailProperty::MessageId => MetadataHeaderName::MessageId,
EmailProperty::InReplyTo => MetadataHeaderName::InReplyTo,
EmailProperty::References => MetadataHeaderName::References,
_ => unreachable!(),
})
.map(|value| {
HeaderValue::from(value).into_form(&HeaderForm::MessageIds)
})
.unwrap_or_default(),
);
}
EmailProperty::Sender
| EmailProperty::From
| EmailProperty::To
| EmailProperty::Cc
| EmailProperty::Bcc
| EmailProperty::ReplyTo => {
email.insert_unchecked(
property.clone(),
root_part
.header_value(&match property {
EmailProperty::Sender => MetadataHeaderName::Sender,
EmailProperty::From => MetadataHeaderName::From,
EmailProperty::To => MetadataHeaderName::To,
EmailProperty::Cc => MetadataHeaderName::Cc,
EmailProperty::Bcc => MetadataHeaderName::Bcc,
EmailProperty::ReplyTo => MetadataHeaderName::ReplyTo,
_ => unreachable!(),
})
.map(|value| {
HeaderValue::from(value).into_form(&HeaderForm::Addresses)
})
.unwrap_or_default(),
);
}
EmailProperty::Header(_) => {
email.insert_unchecked(
property.clone(),
root_part.header_to_value(property, &raw_message),
);
}
EmailProperty::Headers => {
email.insert_unchecked(
EmailProperty::Headers,
root_part.headers_to_value(&raw_message),
);
}
EmailProperty::TextBody
| EmailProperty::HtmlBody
| EmailProperty::Attachments => {
let list = match property {
EmailProperty::TextBody => &contents.text_body,
EmailProperty::HtmlBody => &contents.html_body,
EmailProperty::Attachments => &contents.attachments,
_ => unreachable!(),
}
.iter();
email.insert_unchecked(
property.clone(),
list.map(|part_id| {
contents.to_body_part(
u16::from(part_id) as u32,
&body_properties,
&raw_message,
&blob_id,
blob_body_offset,
)
})
.collect::<Vec<_>>(),
);
}
EmailProperty::BodyStructure => {
email.insert_unchecked(
EmailProperty::BodyStructure,
contents.to_body_part(
0,
&body_properties,
&raw_message,
&blob_id,
blob_body_offset,
),
);
}
EmailProperty::BodyValues => {
let mut body_values = Map::with_capacity(contents.parts.len());
for (part_id, part) in contents.parts.iter().enumerate() {
if part.is_text_mime_type()
&& (fetch_all_body_values
|| (fetch_html_body_values
&& contents.is_html_part(part_id as u16))
|| (fetch_text_body_values
&& contents.is_text_part(part_id as u16)))
{
let contents = part.decode_contents(&raw_message);
let (is_truncated, value) = match &part.body {
ArchivedMetadataPartType::Text => {
truncate_plain(contents.as_str(), max_body_value_bytes)
}
ArchivedMetadataPartType::Html => {
truncate_html(contents.as_str(), max_body_value_bytes)
}
_ => unreachable!(),
};
body_values.insert_unchecked(
Key::Owned(part_id.to_string()),
Map::with_capacity(3)
.with_key_value(
EmailProperty::IsEncodingProblem,
(part.flags & PART_ENCODING_PROBLEM) != 0,
)
.with_key_value(EmailProperty::IsTruncated, is_truncated)
.with_key_value(EmailProperty::Value, value),
);
}
}
email.insert_unchecked(EmailProperty::BodyValues, body_values);
}
_ => {
return Err(trc::JmapEvent::InvalidArguments
.into_err()
.details(format!("Invalid property {property:?}")));
}
}
}
response.list.push(email.into());
}
Ok(response)
}
}
+237
View File
@@ -0,0 +1,237 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
blob::download::BlobDownload, changes::state::JmapCacheState, email::ingested_into_object,
};
use common::{Server, auth::AccessToken, ipc::PushNotification};
use email::{
cache::{MessageCacheFetch, mailbox::MailboxCacheAccess},
mailbox::JUNK_ID,
message::ingest::{EmailIngest, IngestEmail, IngestSource},
};
use http_proto::HttpSessionData;
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::import::{ImportEmailRequest, ImportEmailResponse},
object::email::EmailProperty,
request::MaybeInvalid,
types::state::State,
};
use mail_parser::{HeaderName, MessageParser};
use std::future::Future;
use types::{
acl::Acl,
id::Id,
keyword::Keyword,
type_state::{DataType, StateChange},
};
use utils::map::vec_map::VecMap;
pub trait EmailImport: Sync + Send {
fn email_import(
&self,
request: ImportEmailRequest,
access_token: &AccessToken,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<ImportEmailResponse>> + Send;
}
impl EmailImport for Server {
async fn email_import(
&self,
request: ImportEmailRequest,
access_token: &AccessToken,
session: &HttpSessionData,
) -> trc::Result<ImportEmailResponse> {
// Validate state
let account_id = request.account_id.document_id();
let cache = self.get_cached_messages(account_id).await?;
let old_state: State = cache.assert_state(false, &request.if_in_state)?;
let can_add_mailbox_ids = if access_token.is_shared(account_id) {
cache.shared_mailboxes(access_token, Acl::AddItems).into()
} else {
None
};
// Obtain import access token
let import_access_token = if account_id != access_token.account_id() {
#[cfg(feature = "test_mode")]
{
AccessToken::from_id_maybe_invalid(account_id).into()
}
#[cfg(not(feature = "test_mode"))]
{
use common::auth::BuildAccessToken;
use trc::AddContext;
self.access_token(account_id)
.await
.caused_by(trc::location!())?
.build()
.into()
}
} else {
None
};
let mut response = ImportEmailResponse {
account_id: request.account_id,
new_state: old_state.clone(),
old_state: old_state.into(),
created: VecMap::with_capacity(request.emails.len()),
not_created: VecMap::new(),
};
let mut last_change_id = None;
'outer: for (id, email) in request.emails {
// Validate mailboxIds
let mailbox_ids = email
.mailbox_ids
.unwrap()
.into_iter()
.filter_map(|m| m.try_unwrap().map(|m| m.document_id()))
.collect::<Vec<_>>();
if mailbox_ids.is_empty() {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::MailboxIds)
.with_description("Message must belong to at least one mailbox."),
);
continue;
}
for mailbox_id in &mailbox_ids {
if !cache.has_mailbox_id(mailbox_id) {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::MailboxIds)
.with_description(format!(
"Mailbox {} does not exist.",
Id::from(*mailbox_id)
)),
);
continue 'outer;
} else if matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(*mailbox_id)) {
response.not_created.append(
id,
SetError::forbidden().with_description(format!(
"You are not allowed to add messages to mailbox {}.",
Id::from(*mailbox_id)
)),
);
continue 'outer;
}
}
let MaybeInvalid::Value(blob_id) = email.blob_id else {
response.not_created.append(
id,
SetError::invalid_properties()
.with_property(EmailProperty::BlobId)
.with_description("Invalid blob id."),
);
continue;
};
// Fetch raw message to import
let raw_message = match self.blob_download(&blob_id, access_token).await? {
Some(raw_message) => raw_message,
None => {
response.not_created.append(
id,
SetError::new(SetErrorType::BlobNotFound)
.with_description(format!("BlobId {} not found.", blob_id)),
);
continue;
}
};
// Import message
let parsed = MessageParser::new().parse(&raw_message);
let is_valid_message = parsed.as_ref().is_some_and(|message| {
message
.headers()
.iter()
.any(|header| !matches!(header.name, HeaderName::Other(_)))
});
if !is_valid_message {
response.not_created.append(
id,
SetError::new(SetErrorType::InvalidEmail)
.with_description("Blob does not contain a valid RFC 5322 message."),
);
continue;
}
match self
.email_ingest(IngestEmail {
raw_message: &raw_message,
message: parsed,
blob_hash: Some(&blob_id.hash),
access_token: import_access_token.as_ref().unwrap_or(access_token),
source: IngestSource::Jmap {
train_classifier: email
.keywords
.iter()
.any(|k| matches!(k, Keyword::Junk | Keyword::NotJunk))
|| mailbox_ids.contains(&JUNK_ID),
},
mailbox_ids,
keywords: email.keywords,
received_at: email.received_at.map(|r| r.into()),
session_id: session.session_id,
})
.await
{
Ok(email) => {
last_change_id = Some(email.change_id);
response
.created
.append(id, ingested_into_object(email).into());
}
Err(mut err) => match err.as_ref() {
trc::EventType::Limit(trc::LimitEvent::Quota) => {
response.not_created.append(
id,
SetError::new(SetErrorType::OverQuota)
.with_description("You have exceeded your disk quota."),
);
}
trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => {
response.not_created.append(
id,
SetError::new(SetErrorType::InvalidEmail).with_description(
err.take_value(trc::Key::Reason)
.and_then(|v| v.into_string())
.unwrap(),
),
);
}
_ => {
return Err(err);
}
},
}
}
// Update state
if let Some(change_id) = last_change_id {
self.broadcast_push_notification(PushNotification::StateChange(
StateChange::new(account_id)
.with_change_id(change_id)
.with_change(DataType::Email)
.with_change(DataType::Mailbox)
.with_change(DataType::Thread),
))
.await;
response.new_state = self.get_cached_messages(account_id).await?.get_state(false);
}
Ok(response)
}
}
+75
View File
@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use email::message::ingest::IngestedEmail;
use jmap_proto::{
error::set::SetError,
object::email::{EmailProperty, EmailValue},
};
use jmap_tools::{JsonPointer, JsonPointerItem, Key, Map, Value};
use types::{id::Id, keyword::Keyword};
pub mod copy;
pub mod get;
pub mod import;
pub mod parse;
pub mod query;
pub mod set;
pub mod snippet;
fn ingested_into_object(email: IngestedEmail) -> Map<'static, EmailProperty, EmailValue> {
Map::with_capacity(3)
.with_key_value(
EmailProperty::Id,
Id::from_parts(email.thread_id, email.document_id),
)
.with_key_value(EmailProperty::ThreadId, Id::from(email.thread_id))
.with_key_value(EmailProperty::BlobId, email.blob_id)
.with_key_value(EmailProperty::Size, email.size)
}
pub(crate) enum PatchResult<'x> {
SetKeyword(&'x Keyword),
RemoveKeyword(&'x Keyword),
AddMailbox(u32),
RemoveMailbox(u32),
Invalid(SetError<EmailProperty>),
}
pub(crate) fn handle_email_patch<'x>(
pointer: &'x JsonPointer<EmailProperty>,
value: Value<'_, EmailProperty, EmailValue>,
) -> PatchResult<'x> {
let mut pointer_iter = pointer.iter();
match (pointer_iter.next(), pointer_iter.next()) {
(
Some(JsonPointerItem::Key(Key::Property(EmailProperty::Keywords))),
Some(JsonPointerItem::Key(Key::Property(EmailProperty::Keyword(keyword)))),
) => match value {
Value::Bool(true) => return PatchResult::SetKeyword(keyword),
Value::Bool(false) | Value::Null => return PatchResult::RemoveKeyword(keyword),
_ => (),
},
(
Some(JsonPointerItem::Key(Key::Property(EmailProperty::MailboxIds))),
Some(JsonPointerItem::Key(Key::Property(EmailProperty::IdValue(id)))),
) => match value {
Value::Bool(true) => return PatchResult::AddMailbox(id.document_id()),
Value::Bool(false) | Value::Null => {
return PatchResult::RemoveMailbox(id.document_id());
}
_ => (),
},
_ => (),
}
PatchResult::Invalid(
SetError::invalid_properties()
.with_property(EmailProperty::Pointer(pointer.clone()))
.with_description("Invalid patch value".to_string()),
)
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::blob::download::BlobDownload;
use common::{Server, auth::AccessToken};
use email::message::index::PREVIEW_LENGTH;
use email::message::{
body::{ToBodyPart, TruncateBody},
headers::HeaderToValue,
};
use jmap_proto::{
method::parse::{ParseRequest, ParseResponse},
object::email::{Email, EmailProperty},
request::{IntoValid, MaybeInvalid, reference::MaybeIdReference},
};
use jmap_tools::{Key, Map, Value};
use mail_parser::{
HeaderName, MessageParser, MimeHeaders, PartType, decoders::html::html_to_text,
parsers::preview::preview_text,
};
use std::future::Future;
use utils::{chained_bytes::ChainedBytes, map::vec_map::VecMap};
pub trait EmailParse: Sync + Send {
fn email_parse(
&self,
request: ParseRequest<Email>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<ParseResponse<Email>>> + Send;
}
impl EmailParse for Server {
async fn email_parse(
&self,
request: ParseRequest<Email>,
access_token: &AccessToken,
) -> trc::Result<ParseResponse<Email>> {
if request.blob_ids.len() > self.core.jmap.mail_parse_max_items {
return Err(trc::JmapEvent::RequestTooLarge.into_err());
}
let properties = request
.properties
.map(|v| v.into_valid().collect())
.unwrap_or_else(|| {
vec![
EmailProperty::BlobId,
EmailProperty::Size,
EmailProperty::ReceivedAt,
EmailProperty::MessageId,
EmailProperty::InReplyTo,
EmailProperty::References,
EmailProperty::Sender,
EmailProperty::From,
EmailProperty::To,
EmailProperty::Cc,
EmailProperty::Bcc,
EmailProperty::ReplyTo,
EmailProperty::Subject,
EmailProperty::SentAt,
EmailProperty::HasAttachment,
EmailProperty::Preview,
EmailProperty::BodyValues,
EmailProperty::TextBody,
EmailProperty::HtmlBody,
EmailProperty::Attachments,
]
});
let body_properties = request
.arguments
.body_properties
.map(|v| v.into_valid().collect())
.unwrap_or_else(|| {
vec![
EmailProperty::PartId,
EmailProperty::BlobId,
EmailProperty::Size,
EmailProperty::Name,
EmailProperty::Type,
EmailProperty::Charset,
EmailProperty::Disposition,
EmailProperty::Cid,
EmailProperty::Language,
EmailProperty::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 mut response = ParseResponse {
account_id: request.account_id,
parsed: VecMap::with_capacity(request.blob_ids.len()),
not_parsable: vec![],
not_found: vec![],
};
for blob_id in request.blob_ids {
let blob_id = match blob_id {
MaybeIdReference::Id(blob_id) => blob_id,
MaybeIdReference::Invalid(s) | MaybeIdReference::Reference(s) => {
response.not_found.push(MaybeInvalid::Invalid(s));
continue;
}
};
// Fetch raw message to parse
let raw_message = match self.blob_download(&blob_id, access_token).await? {
Some(raw_message) => raw_message,
None => {
response.not_found.push(MaybeInvalid::Value(blob_id));
continue;
}
};
let message = match MessageParser::new().parse(&raw_message).filter(|message| {
message
.root_part()
.headers()
.iter()
.any(|header| !matches!(header.name, HeaderName::Other(_)))
}) {
Some(message) => message,
None => {
response.not_parsable.push(blob_id);
continue;
}
};
let raw_message = ChainedBytes::new(&raw_message);
// Prepare response
let mut email = Map::with_capacity(properties.len());
for property in &properties {
match property {
EmailProperty::BlobId => {
email.insert_unchecked(EmailProperty::BlobId, blob_id.clone());
}
EmailProperty::Size => {
email.insert_unchecked(
EmailProperty::Size,
Value::Number(raw_message.len().into()),
);
}
EmailProperty::HasAttachment => {
email.insert_unchecked(
EmailProperty::HasAttachment,
Value::Bool(message.parts.iter().enumerate().any(|(part_id, part)| {
let part_id = part_id as u32;
match &part.body {
PartType::Html(_) | PartType::Text(_) => {
!message.text_body.contains(&part_id)
&& !message.html_body.contains(&part_id)
}
PartType::Binary(_) | PartType::Message(_) => true,
_ => false,
}
})),
);
}
EmailProperty::Preview => {
email.insert_unchecked(
EmailProperty::Preview,
match message
.text_body
.first()
.or_else(|| message.html_body.first())
.and_then(|idx| message.parts.get(*idx as usize))
.map(|part| &part.body)
{
Some(PartType::Text(text)) => {
preview_text(text.replace('\r', "").into(), PREVIEW_LENGTH)
.into()
}
Some(PartType::Html(html)) => preview_text(
html_to_text(html).replace('\r', "").into(),
PREVIEW_LENGTH,
)
.into(),
_ => Value::Null,
},
);
}
EmailProperty::MessageId
| EmailProperty::InReplyTo
| EmailProperty::References
| EmailProperty::Sender
| EmailProperty::From
| EmailProperty::To
| EmailProperty::Cc
| EmailProperty::Bcc
| EmailProperty::ReplyTo
| EmailProperty::Subject
| EmailProperty::SentAt
| EmailProperty::Header(_) => {
email.insert_unchecked(
property.clone(),
message.parts[0]
.headers
.header_to_value(property, &raw_message),
);
}
EmailProperty::Headers => {
email.insert_unchecked(
EmailProperty::Headers,
message.parts[0].headers.headers_to_value(&raw_message),
);
}
EmailProperty::TextBody
| EmailProperty::HtmlBody
| EmailProperty::Attachments => {
let list = match property {
EmailProperty::TextBody => &message.text_body,
EmailProperty::HtmlBody => &message.html_body,
EmailProperty::Attachments => &message.attachments,
_ => unreachable!(),
}
.iter();
email.insert_unchecked(
property.clone(),
list.map(|part_id| {
message.parts.to_body_part(
*part_id,
&body_properties,
&raw_message,
&blob_id,
0,
)
})
.collect::<Vec<_>>(),
);
}
EmailProperty::BodyStructure => {
email.insert_unchecked(
EmailProperty::BodyStructure,
message.parts.to_body_part(
0,
&body_properties,
&raw_message,
&blob_id,
0,
),
);
}
EmailProperty::BodyValues => {
let mut body_values = Map::with_capacity(message.parts.len());
for (part_id, part) in message.parts.iter().enumerate() {
let part_id = part_id as u32;
if part.is_text()
&& part
.content_type()
.is_none_or(|ct| ct.ctype().eq_ignore_ascii_case("text"))
&& (fetch_all_body_values
|| (fetch_html_body_values
&& message.html_body.contains(&part_id))
|| (fetch_text_body_values
&& message.text_body.contains(&part_id)))
{
let (is_truncated, value) =
part.body.truncate(max_body_value_bytes);
body_values.insert_unchecked(
Key::Owned(part_id.to_string()),
Map::with_capacity(3)
.with_key_value(
EmailProperty::IsEncodingProblem,
part.is_encoding_problem,
)
.with_key_value(EmailProperty::IsTruncated, is_truncated)
.with_key_value(EmailProperty::Value, value),
);
}
}
email.insert_unchecked(EmailProperty::BodyValues, body_values);
}
EmailProperty::Id
| EmailProperty::ThreadId
| EmailProperty::Keywords
| EmailProperty::MailboxIds
| EmailProperty::ReceivedAt => {
email.insert_unchecked(property.clone(), Value::Null);
}
_ => {
return Err(trc::JmapEvent::InvalidArguments
.into_err()
.details(format!("Invalid property {property:?}")));
}
}
}
response.parsed.append(blob_id, email.into());
}
Ok(response)
}
}
+441
View File
@@ -0,0 +1,441 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use common::{MessageStoreCache, Server, auth::AccessToken};
use email::cache::{MessageCacheFetch, email::MessageCacheAccess};
use jmap_proto::{
method::query::{Filter, QueryRequest, QueryResponse},
object::email::{Email, EmailComparator, EmailFilter},
};
use mail_parser::HeaderName;
use nlp::language::Language;
use std::future::Future;
use store::{
ahash::{AHashMap, AHashSet},
roaring::RoaringBitmap,
search::{
EmailSearchField, SearchComparator, SearchFilter, SearchOperator, SearchQuery, SearchValue,
},
write::SearchIndex,
};
use trc::AddContext;
use types::{acl::Acl, keyword::Keyword};
use utils::map::vec_map::VecMap;
pub trait EmailQuery: Sync + Send {
fn email_query(
&self,
request: QueryRequest<Email>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
impl EmailQuery for Server {
async fn email_query(
&self,
mut request: QueryRequest<Email>,
access_token: &AccessToken,
) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len());
let cached_messages = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
for filter in std::mem::take(&mut request.filter) {
match filter {
Filter::Property(cond) => match cond {
EmailFilter::Text(text) => {
let (text, language) =
Language::detect(text, self.core.email.default_language);
filters.push(SearchFilter::Or);
filters.push(SearchFilter::has_text(
EmailSearchField::From,
&text,
Language::None,
));
filters.push(SearchFilter::has_text(
EmailSearchField::To,
&text,
Language::None,
));
filters.push(SearchFilter::has_text(
EmailSearchField::Cc,
&text,
Language::None,
));
filters.push(SearchFilter::has_text(
EmailSearchField::Bcc,
&text,
Language::None,
));
filters.push(SearchFilter::has_text(
EmailSearchField::Subject,
&text,
language,
));
filters.push(SearchFilter::has_text(
EmailSearchField::Body,
&text,
language,
));
filters.push(SearchFilter::has_text(
EmailSearchField::Attachment,
text,
language,
));
filters.push(SearchFilter::End);
}
EmailFilter::From(text) => filters.push(SearchFilter::has_text(
EmailSearchField::From,
text,
Language::None,
)),
EmailFilter::To(text) => filters.push(SearchFilter::has_text(
EmailSearchField::To,
text,
Language::None,
)),
EmailFilter::Cc(text) => filters.push(SearchFilter::has_text(
EmailSearchField::Cc,
text,
Language::None,
)),
EmailFilter::Bcc(text) => filters.push(SearchFilter::has_text(
EmailSearchField::Bcc,
text,
Language::None,
)),
EmailFilter::Subject(text) => filters.push(SearchFilter::has_text_detect(
EmailSearchField::Subject,
text,
self.core.email.default_language,
)),
EmailFilter::Body(text) => filters.push(SearchFilter::has_text_detect(
EmailSearchField::Body,
text,
self.core.email.default_language,
)),
EmailFilter::Header(header) => {
let mut header = header.into_iter();
let header_name = header.next().ok_or_else(|| {
trc::JmapEvent::InvalidArguments
.into_err()
.details("Header name is missing.".to_string())
})?;
if let Some(header_name) = HeaderName::parse(header_name) {
let value = header.next();
let op = if matches!(
header_name,
HeaderName::MessageId
| HeaderName::InReplyTo
| HeaderName::References
| HeaderName::ResentMessageId
) || value.is_none()
{
SearchOperator::Equal
} else {
SearchOperator::Contains
};
filters.push(SearchFilter::cond(
EmailSearchField::Headers,
op,
SearchValue::KeyValues(VecMap::with_capacity(1).with_append(
header_name.as_str().to_lowercase(),
value.unwrap_or_default(),
)),
));
}
}
EmailFilter::InMailbox(mailbox) => {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cached_messages
.in_mailbox(mailbox.document_id())
.map(|item| item.document_id),
)))
}
EmailFilter::InMailboxOtherThan(mailboxes) => {
let mailboxes = mailboxes
.into_iter()
.map(|m| m.document_id())
.collect::<AHashSet<_>>();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cached_messages.emails.items.iter().filter_map(|item| {
if item
.mailboxes
.iter()
.any(|mb| !mailboxes.contains(&mb.mailbox_id))
{
Some(item.document_id)
} else {
None
}
}),
)));
}
EmailFilter::Before(date) => filters.push(SearchFilter::lt(
EmailSearchField::ReceivedAt,
date.timestamp(),
)),
EmailFilter::After(date) => filters.push(SearchFilter::gt(
EmailSearchField::ReceivedAt,
date.timestamp(),
)),
EmailFilter::MinSize(size) => {
filters.push(SearchFilter::ge(EmailSearchField::Size, size))
}
EmailFilter::MaxSize(size) => {
filters.push(SearchFilter::lt(EmailSearchField::Size, size))
}
EmailFilter::AllInThreadHaveKeyword(keyword) => filters.push(
SearchFilter::is_in_set(thread_keywords(&cached_messages, keyword, true)),
),
EmailFilter::SomeInThreadHaveKeyword(keyword) => filters.push(
SearchFilter::is_in_set(thread_keywords(&cached_messages, keyword, false)),
),
EmailFilter::NoneInThreadHaveKeyword(keyword) => {
filters.push(SearchFilter::Not);
filters.push(SearchFilter::is_in_set(thread_keywords(
&cached_messages,
keyword,
false,
)));
filters.push(SearchFilter::End);
}
EmailFilter::HasKeyword(keyword) => {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cached_messages
.with_keyword(&keyword)
.map(|item| item.document_id),
)));
}
EmailFilter::NotKeyword(keyword) => {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cached_messages
.without_keyword(&keyword)
.map(|item| item.document_id),
)));
}
EmailFilter::HasAttachment(has_attach) => {
filters.push(SearchFilter::eq(
EmailSearchField::HasAttachment,
has_attach,
));
}
// Non-standard
EmailFilter::Id(ids) => {
let mut set = RoaringBitmap::new();
for id in ids {
set.insert(id.document_id());
}
filters.push(SearchFilter::is_in_set(set));
}
EmailFilter::SentBefore(date) => {
filters.push(SearchFilter::lt(EmailSearchField::SentAt, date.timestamp()))
}
EmailFilter::SentAfter(date) => {
filters.push(SearchFilter::gt(EmailSearchField::SentAt, date.timestamp()))
}
EmailFilter::InThread(id) => {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cached_messages
.in_thread(id.document_id())
.map(|item| item.document_id),
)))
}
other => {
return Err(trc::JmapEvent::UnsupportedFilter
.into_err()
.details(other.to_string()));
}
},
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
// Parse sort criteria
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request
.sort
.take()
.filter(|s| !s.is_empty())
.unwrap_or_default()
{
comparators.push(match comparator.property {
EmailComparator::ReceivedAt => {
SearchComparator::field(EmailSearchField::ReceivedAt, comparator.is_ascending)
}
EmailComparator::Size => {
SearchComparator::field(EmailSearchField::Size, comparator.is_ascending)
}
EmailComparator::From => {
SearchComparator::field(EmailSearchField::From, comparator.is_ascending)
}
EmailComparator::To => {
SearchComparator::field(EmailSearchField::To, comparator.is_ascending)
}
EmailComparator::Subject => {
SearchComparator::field(EmailSearchField::Subject, comparator.is_ascending)
}
EmailComparator::SentAt => {
SearchComparator::field(EmailSearchField::SentAt, comparator.is_ascending)
}
EmailComparator::HasKeyword(keyword) => SearchComparator::set(
RoaringBitmap::from_iter(
cached_messages
.with_keyword(&keyword)
.map(|item| item.document_id),
),
comparator.is_ascending,
),
EmailComparator::AllInThreadHaveKeyword(keyword) => SearchComparator::set(
thread_keywords(&cached_messages, keyword, true),
comparator.is_ascending,
),
EmailComparator::SomeInThreadHaveKeyword(keyword) => SearchComparator::set(
thread_keywords(&cached_messages, keyword, false),
comparator.is_ascending,
),
// Non-standard
EmailComparator::Cc => {
SearchComparator::field(EmailSearchField::Cc, comparator.is_ascending)
}
other => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(other.to_string()));
}
});
}
let results = self
.search_store()
.query_account(
SearchQuery::new(SearchIndex::Email)
.with_filters(filters)
.with_comparators(comparators)
.with_account_id(account_id)
.with_mask(if access_token.is_shared(account_id) {
cached_messages.shared_messages(access_token, Acl::ReadItems)
} else {
cached_messages
.emails
.items
.iter()
.map(|item| item.document_id)
.collect()
}),
)
.await?;
let collapse_threads = request.arguments.collapse_threads.unwrap_or(false);
let total_results = if collapse_threads {
let mut seen_thread_ids = AHashSet::new();
results
.iter()
.filter_map(|document_id| {
cached_messages
.email_by_id(document_id)
.map(|email| email.thread_id)
})
.filter(|thread_id| seen_thread_ids.insert(*thread_id))
.count()
} else {
results.len()
};
let mut response = QueryResponseBuilder::new(
total_results,
self.core.jmap.query_max_results,
cached_messages.get_state(false),
&request,
);
if !results.is_empty() {
let mut seen_thread_ids = AHashSet::new();
for document_id in results {
let Some(thread_id) = cached_messages
.email_by_id(&document_id)
.map(|email| email.thread_id)
else {
continue;
};
if collapse_threads && !seen_thread_ids.insert(thread_id) {
continue;
}
if !response.add(thread_id, document_id) {
break;
}
}
}
response.build()
}
}
fn thread_keywords(cache: &MessageStoreCache, keyword: Keyword, match_all: bool) -> RoaringBitmap {
let keyword_doc_ids =
RoaringBitmap::from_iter(cache.with_keyword(&keyword).map(|item| item.document_id));
if keyword_doc_ids.is_empty() {
return keyword_doc_ids;
}
let mut not_matched_ids = RoaringBitmap::new();
let mut matched_ids = RoaringBitmap::new();
let mut thread_map: AHashMap<u32, RoaringBitmap> = AHashMap::new();
for item in &cache.emails.items {
thread_map
.entry(item.thread_id)
.or_default()
.insert(item.document_id);
}
for item in &cache.emails.items {
let keyword_doc_id = item.document_id;
if !keyword_doc_ids.contains(keyword_doc_id)
|| matched_ids.contains(keyword_doc_id)
|| not_matched_ids.contains(keyword_doc_id)
{
continue;
}
if let Some(thread_doc_ids) = thread_map.get(&item.thread_id) {
let mut thread_tag_intersection = thread_doc_ids.clone();
thread_tag_intersection &= &keyword_doc_ids;
if (match_all && &thread_tag_intersection == thread_doc_ids)
|| (!match_all && !thread_tag_intersection.is_empty())
{
matched_ids |= thread_doc_ids;
} else if !thread_tag_intersection.is_empty() {
not_matched_ids |= &thread_tag_intersection;
}
}
}
matched_ids
}
File diff suppressed because it is too large Load Diff
+263
View File
@@ -0,0 +1,263 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken};
use email::{
cache::{MessageCacheFetch, email::MessageCacheAccess},
message::metadata::{
ArchivedMetadataPartType, DecodedPartContent, MessageMetadata, MetadataHeaderName,
},
};
use jmap_proto::{
method::{
query::Filter,
search_snippet::{GetSearchSnippetRequest, GetSearchSnippetResponse, SearchSnippet},
},
object::email::EmailFilter,
request::MaybeInvalid,
};
use mail_parser::decoders::html::html_to_text;
use nlp::language::{Language, search_snippet::generate_snippet, stemmer::Stemmer};
use std::future::Future;
use store::{
ValueKey,
backend::MAX_TOKEN_LENGTH,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{acl::Acl, collection::Collection, field::EmailField};
use utils::chained_bytes::ChainedBytes;
pub trait EmailSearchSnippet: Sync + Send {
fn email_search_snippet(
&self,
request: GetSearchSnippetRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetSearchSnippetResponse>> + Send;
}
impl EmailSearchSnippet for Server {
async fn email_search_snippet(
&self,
request: GetSearchSnippetRequest,
access_token: &AccessToken,
) -> trc::Result<GetSearchSnippetResponse> {
let mut filter_stack = vec![];
let mut include_term = true;
let mut terms = vec![];
let mut is_exact = false;
let mut language = self.core.email.default_language;
for cond in request.filter {
match cond {
Filter::Property(cond) => {
if let EmailFilter::Text(text)
| EmailFilter::Subject(text)
| EmailFilter::Body(text) = cond
&& include_term
{
let (text, language_) =
Language::detect(text, self.core.email.default_language);
language = language_;
if (text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\''))
{
for token in language.tokenize_text(&text, MAX_TOKEN_LENGTH) {
terms.push(token.word.into_owned());
}
is_exact = true;
} else {
for token in Stemmer::new(&text, language, MAX_TOKEN_LENGTH) {
terms.push(token.word.into_owned());
if let Some(stemmed_word) = token.stemmed_word {
terms.push(stemmed_word.into_owned());
}
}
}
}
}
Filter::And | Filter::Or => {
filter_stack.push(cond);
}
Filter::Not => {
filter_stack.push(cond);
include_term = !include_term;
}
Filter::Close => {
if matches!(filter_stack.pop(), Some(Filter::Not)) {
include_term = !include_term;
}
}
}
}
let account_id = request.account_id.document_id();
let cached_messages = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
let document_ids = if access_token.is_member(account_id) {
cached_messages.email_document_ids()
} else {
cached_messages.shared_messages(access_token, Acl::ReadItems)
};
let email_ids = request.email_ids.unwrap();
let mut response = GetSearchSnippetResponse {
account_id: request.account_id,
list: Vec::with_capacity(email_ids.len()),
not_found: None,
};
let mut not_found = Vec::new();
if email_ids.len() > self.core.jmap.snippet_max_results {
return Err(trc::JmapEvent::RequestTooLarge.into_err());
}
for email_id in email_ids {
let email_id = match email_id {
MaybeInvalid::Value(email_id) => email_id,
invalid => {
not_found.push(invalid);
continue;
}
};
let document_id = email_id.document_id();
let mut snippet = SearchSnippet {
email_id,
subject: None,
preview: None,
};
if !document_ids.contains(document_id) {
not_found.push(MaybeInvalid::Value(email_id));
continue;
} else if terms.is_empty() {
response.list.push(snippet);
continue;
}
let metadata_ = match self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
account_id,
Collection::Email,
document_id,
EmailField::Metadata,
))
.await?
{
Some(metadata) => metadata,
None => {
not_found.push(MaybeInvalid::Value(email_id));
continue;
}
};
let metadata = metadata_
.unarchive::<MessageMetadata>()
.caused_by(trc::location!())?;
// Add subject snippet
let contents = &metadata.contents[0];
if let Some(subject) = contents
.root_part()
.header_value(&MetadataHeaderName::Subject)
.and_then(|v| v.as_text())
.and_then(|v| generate_snippet(v, &terms, language, is_exact))
{
snippet.subject = subject.into();
}
// Download message
let raw_body = if let Some(raw_body) = self
.blob_store()
.get_blob(metadata.blob_hash.0.as_slice(), 0..usize::MAX)
.await?
{
raw_body
} else {
trc::event!(
Store(trc::StoreEvent::NotFound),
AccountId = account_id,
DocumentId = email_id.document_id(),
Collection = Collection::Email,
BlobId = metadata.blob_hash.0.as_slice(),
Details = "Blob not found.",
CausedBy = trc::location!(),
);
not_found.push(MaybeInvalid::Value(email_id));
continue;
};
let raw_message = ChainedBytes::new(metadata.raw_headers.as_ref()).with_last(
raw_body
.get(metadata.blob_body_offset.to_native() as usize..)
.unwrap_or_default(),
);
// Find a matching part
'outer: for part in contents.parts.iter() {
match &part.body {
ArchivedMetadataPartType::Text => {
let text = match part.decode_contents(&raw_message) {
DecodedPartContent::Text(text) => text,
_ => unreachable!(),
};
if let Some(body) = generate_snippet(&text, &terms, language, is_exact) {
snippet.preview = body.into();
break;
}
}
ArchivedMetadataPartType::Html => {
let text = match part.decode_contents(&raw_message) {
DecodedPartContent::Text(html) => html_to_text(&html),
_ => unreachable!(),
};
if let Some(body) = generate_snippet(&text, &terms, language, is_exact) {
snippet.preview = body.into();
break;
}
}
ArchivedMetadataPartType::Message(message) => {
for part in metadata.contents[u16::from(message) as usize].parts.iter() {
if let ArchivedMetadataPartType::Text | ArchivedMetadataPartType::Html =
part.body
{
let text = match (part.decode_contents(&raw_message), &part.body) {
(
DecodedPartContent::Text(text),
ArchivedMetadataPartType::Text,
) => text,
(
DecodedPartContent::Text(html),
ArchivedMetadataPartType::Html,
) => html_to_text(&html).into(),
_ => unreachable!(),
};
if let Some(body) =
generate_snippet(&text, &terms, language, is_exact)
{
snippet.preview = body.into();
break 'outer;
}
}
}
}
_ => (),
}
}
//}
response.list.push(snippet);
}
if !not_found.is_empty() {
response.not_found = Some(not_found);
}
Ok(response)
}
}