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.
238 lines
8.7 KiB
Rust
238 lines
8.7 KiB
Rust
/*
|
|
* 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)
|
|
}
|
|
}
|