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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user