/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ingest::{EmailIngest, IngestEmail, IngestSource}; use crate::{ mailbox::{INBOX_ID, TRASH_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; pub const ORCPT_ADDR_TYPE: &str = "rfc822;"; #[derive(Debug)] pub struct IngestMessage { pub sender_address: String, pub sender_authenticated: bool, pub recipients: Vec, pub message_blob: BlobHash, pub message_size: u64, pub session_id: u64, } #[derive(Debug)] pub struct IngestRecipient { pub address: String, pub orcpt: Option, pub spam_percentage: Option, } impl IngestRecipient { pub fn orcpt_parameter(&self) -> Option { self.orcpt .as_deref() .map(|orcpt| format!("{ORCPT_ADDR_TYPE}{orcpt}")) } 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, pub autogenerated: Vec, } pub struct AutogeneratedMessage { pub sender_address: String, pub recipients: Vec, pub message: Vec, } pub trait MailDelivery: Sync + Send { fn deliver_message( &self, message: IngestMessage, ) -> impl Future + 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::>(), 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::>(), autogenerated: vec![], }; } }; // Obtain the account IDs for each recipient let mut account_ids: AHashMap = 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 { // inbuxa: ME-4, ME-10: a masked address delivers to its owner let mut mask = match inbuxa_features::masked_email::ops::resolve_recipient( &self.core.storage.data, self.registry(), &rcpt.address, ) .await { Ok(mask) => mask, Err(err) => { trc::error!( err.details("Failed to look up masked address.") .ctx(trc::Key::To, rcpt.address.to_string()) .span_id(message.session_id) ); result.status.push(LocalDeliveryStatus::TemporaryFailure { reason: "Address lookup failed.".into(), }); continue; } }; let account_lookup = match &mask { Some(mask) => Ok(Some(mask.object.account_id.document_id())), None => self.account_id_from_email(&rcpt.address, false).await, }; let account_id = match account_lookup { 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; } }; // inbuxa: ME-9: rewritten at RCPT TO, the mask is the original recipient if mask.is_none() { match inbuxa_features::masked_email::ops::resolve_original( &self.core.storage.data, self.registry(), rcpt.orcpt.as_deref(), account_id, ) .await { Ok(original) => mask = original, Err(err) => { trc::error!(err.span_id(message.session_id)); } } } if let Some(status) = account_ids .get(&account_id) .and_then(|pos| result.status.get(*pos)) { result.status.push(status.clone()); continue; } // inbuxa: ME-9: the message names the mask it came through let masked = match &mask { Some(mask) => { let raw = inbuxa_features::masked_email::ops::with_header( &mask.object.email, &raw_message, ); match self.put_temporary_blob(account_id, &raw, 600).await { Ok((hash, _)) => Some((raw, hash)), Err(err) => { trc::error!(err.span_id(message.session_id)); result.status.push(LocalDeliveryStatus::TemporaryFailure { reason: "Temporary I/O error.".into(), }); continue; } } } None => None, }; let (raw_message, message_blob) = masked .as_ref() .map(|(raw, hash)| (raw.as_slice(), hash)) .unwrap_or((raw_message.as_slice(), &message.message_blob)); // inbuxa: ME-5: a disabled mask files straight to Trash let to_trash = mask.as_ref().is_some_and(|mask| { mask.state == inbuxa_features::masked_email::State::Disabled }); // 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 let active_script = if to_trash { Ok(None) } else { self.sieve_script_get_active(account_id).await }; match active_script { Ok(None) => { // Ingest message self.email_ingest(IngestEmail { raw_message, blob_hash: Some(message_blob), message: MessageParser::new().parse(raw_message), access_token: &access_token, mailbox_ids: vec![if to_trash { TRASH_ID } else { 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_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) => { // inbuxa: ME-7: the mask saw mail, and a pending one is now enabled if let Some(mask) = &mask && let Err(err) = inbuxa_features::masked_email::ops::delivered( &self.core.storage.data, self.registry(), mask, ) .await { trc::error!(err.span_id(message.session_id)); } // 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 } }