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,279 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ingest::{EmailIngest, IngestEmail, IngestSource};
|
||||
use crate::{mailbox::INBOX_ID, sieve::ingest::SieveScriptIngest};
|
||||
use common::{
|
||||
Server,
|
||||
auth::BuildAccessToken,
|
||||
ipc::{EmailPush, PushNotification},
|
||||
};
|
||||
use mail_parser::MessageParser;
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
use store::ahash::AHashMap;
|
||||
use types::blob_hash::BlobHash;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IngestMessage {
|
||||
pub sender_address: String,
|
||||
pub sender_authenticated: bool,
|
||||
pub recipients: Vec<IngestRecipient>,
|
||||
pub message_blob: BlobHash,
|
||||
pub message_size: u64,
|
||||
pub session_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IngestRecipient {
|
||||
pub address: String,
|
||||
pub orcpt: Option<String>,
|
||||
pub spam_percentage: Option<u8>,
|
||||
}
|
||||
|
||||
impl IngestRecipient {
|
||||
pub fn is_spam(&self) -> bool {
|
||||
self.spam_percentage
|
||||
.is_some_and(|percentage| percentage >= 50)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LocalDeliveryStatus {
|
||||
Success,
|
||||
TemporaryFailure {
|
||||
reason: Cow<'static, str>,
|
||||
},
|
||||
PermanentFailure {
|
||||
code: [u8; 3],
|
||||
reason: Cow<'static, str>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct LocalDeliveryResult {
|
||||
pub status: Vec<LocalDeliveryStatus>,
|
||||
pub autogenerated: Vec<AutogeneratedMessage>,
|
||||
}
|
||||
|
||||
pub struct AutogeneratedMessage {
|
||||
pub sender_address: String,
|
||||
pub recipients: Vec<String>,
|
||||
pub message: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait MailDelivery: Sync + Send {
|
||||
fn deliver_message(
|
||||
&self,
|
||||
message: IngestMessage,
|
||||
) -> impl Future<Output = LocalDeliveryResult> + Send;
|
||||
}
|
||||
|
||||
impl MailDelivery for Server {
|
||||
async fn deliver_message(&self, message: IngestMessage) -> LocalDeliveryResult {
|
||||
// Read message
|
||||
let raw_message = match self
|
||||
.core
|
||||
.storage
|
||||
.blob
|
||||
.get_blob(message.message_blob.as_slice(), 0..usize::MAX)
|
||||
.await
|
||||
{
|
||||
Ok(Some(raw_message)) => raw_message,
|
||||
Ok(None) => {
|
||||
trc::event!(
|
||||
MessageIngest(trc::MessageIngestEvent::Error),
|
||||
Reason = "Blob not found.",
|
||||
SpanId = message.session_id,
|
||||
CausedBy = trc::location!()
|
||||
);
|
||||
|
||||
return LocalDeliveryResult {
|
||||
status: (0..message.recipients.len())
|
||||
.map(|_| LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Blob not found.".into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
autogenerated: vec![],
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.details("Failed to fetch message blob.")
|
||||
.span_id(message.session_id)
|
||||
.caused_by(trc::location!())
|
||||
);
|
||||
|
||||
return LocalDeliveryResult {
|
||||
status: (0..message.recipients.len())
|
||||
.map(|_| LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Temporary I/O error.".into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
autogenerated: vec![],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Obtain the account IDs for each recipient
|
||||
let mut account_ids: AHashMap<u32, usize> =
|
||||
AHashMap::with_capacity(message.recipients.len());
|
||||
let mut result = LocalDeliveryResult {
|
||||
status: Vec::with_capacity(message.recipients.len()),
|
||||
autogenerated: Vec::new(),
|
||||
};
|
||||
|
||||
for rcpt in message.recipients {
|
||||
let account_id = match self.account_id_from_email(&rcpt.address, false).await {
|
||||
Ok(Some(account_id)) => account_id,
|
||||
Ok(None) => {
|
||||
// Something went wrong
|
||||
result.status.push(LocalDeliveryStatus::PermanentFailure {
|
||||
code: [5, 5, 0],
|
||||
reason: "Mailbox not found.".into(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.details("Failed to lookup recipient.")
|
||||
.ctx(trc::Key::To, rcpt.address.to_string())
|
||||
.span_id(message.session_id)
|
||||
.caused_by(trc::location!())
|
||||
);
|
||||
result.status.push(LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Address lookup failed.".into(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(status) = account_ids
|
||||
.get(&account_id)
|
||||
.and_then(|pos| result.status.get(*pos))
|
||||
{
|
||||
result.status.push(status.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Obtain access token
|
||||
let status = match self.access_token(account_id).await.and_then(|token| {
|
||||
token
|
||||
.build()
|
||||
.assert_has_permission(Permission::EmailReceive)
|
||||
}) {
|
||||
Ok(access_token) => {
|
||||
// Check if there is an active sieve script
|
||||
match self.sieve_script_get_active(account_id).await {
|
||||
Ok(None) => {
|
||||
// Ingest message
|
||||
self.email_ingest(IngestEmail {
|
||||
raw_message: &raw_message,
|
||||
blob_hash: Some(&message.message_blob),
|
||||
message: MessageParser::new().parse(&raw_message),
|
||||
access_token: &access_token,
|
||||
mailbox_ids: vec![INBOX_ID],
|
||||
keywords: vec![],
|
||||
received_at: None,
|
||||
source: IngestSource::Smtp {
|
||||
deliver_to: &rcpt.address,
|
||||
is_sender_authenticated: message.sender_authenticated,
|
||||
is_spam: rcpt.is_spam(),
|
||||
},
|
||||
session_id: message.session_id,
|
||||
})
|
||||
.await
|
||||
}
|
||||
Ok(Some(active_script)) => {
|
||||
self.sieve_script_ingest(
|
||||
&access_token,
|
||||
&message.message_blob,
|
||||
&raw_message,
|
||||
&message.sender_address,
|
||||
message.sender_authenticated,
|
||||
&rcpt,
|
||||
message.session_id,
|
||||
active_script,
|
||||
&mut result.autogenerated,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
let status = match status {
|
||||
Ok(ingested_message) => {
|
||||
// Notify state change
|
||||
if ingested_message.change_id != u64::MAX {
|
||||
self.broadcast_push_notification(PushNotification::EmailPush(EmailPush {
|
||||
account_id,
|
||||
email_id: ingested_message.document_id,
|
||||
change_id: ingested_message.change_id,
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
LocalDeliveryStatus::Success
|
||||
}
|
||||
Err(err) => {
|
||||
let status = match err.as_ref() {
|
||||
trc::EventType::Limit(trc::LimitEvent::Quota) => {
|
||||
LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Mailbox over quota.".into(),
|
||||
}
|
||||
}
|
||||
trc::EventType::Limit(trc::LimitEvent::TenantQuota) => {
|
||||
LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Organization over quota.".into(),
|
||||
}
|
||||
}
|
||||
trc::EventType::Security(trc::SecurityEvent::Unauthorized) => {
|
||||
LocalDeliveryStatus::PermanentFailure {
|
||||
code: [5, 5, 0],
|
||||
reason: "This account is not authorized to receive email.".into(),
|
||||
}
|
||||
}
|
||||
trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => {
|
||||
LocalDeliveryStatus::PermanentFailure {
|
||||
code: err
|
||||
.value(trc::Key::Code)
|
||||
.and_then(|v| v.to_uint())
|
||||
.map(|n| {
|
||||
[(n / 100) as u8, ((n % 100) / 10) as u8, (n % 10) as u8]
|
||||
})
|
||||
.unwrap_or([5, 5, 0]),
|
||||
reason: err
|
||||
.value_as_str(trc::Key::Reason)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
_ => LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Transient server failure.".into(),
|
||||
},
|
||||
};
|
||||
|
||||
trc::error!(
|
||||
err.ctx(trc::Key::To, rcpt.address.to_string())
|
||||
.span_id(message.session_id)
|
||||
);
|
||||
|
||||
status
|
||||
}
|
||||
};
|
||||
|
||||
// Cache response for UID to avoid duplicate deliveries
|
||||
account_ids.insert(account_id, result.status.len());
|
||||
|
||||
result.status.push(status);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user