/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::queue::{ MessageSource, quota::HasQueueQuota, spool::{QueueParams, SmtpSpool}, }; use common::{Server, config::smtp::queue::QueueExpiry, scripts::plugins::PluginContext}; use mail_parser::{Encoding, Message, MessagePart, PartType}; use sieve::{ Event, Input, MatchAs, Recipient, Sieve, compiler::grammar::actions::action_redirect::{ByMode, ByTime, Notify, NotifyItem, Ret}, }; use smtp_proto::{ MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, }; use std::{future::Future, sync::Arc, time::Instant}; use trc::SieveEvent; use super::{ScriptModification, ScriptParameters, ScriptResult}; pub trait RunScript: Sync + Send { fn run_script( &self, script_id: String, script: Arc, params: ScriptParameters<'_>, ) -> impl Future + Send; } impl RunScript for Server { async fn run_script( &self, script_id: String, script: Arc, params: ScriptParameters<'_>, ) -> ScriptResult { // Create filter instance let time = Instant::now(); let mut instance = self .core .sieve .trusted_runtime .filter_parsed(params.message.unwrap_or_else(|| Message { parts: vec![MessagePart { headers: vec![], is_encoding_problem: false, body: PartType::Text("".into()), encoding: Encoding::None, offset_header: 0, offset_body: 0, offset_end: 0, }], raw_message: b""[..].into(), ..Default::default() })) .with_vars_env(params.variables) .with_envelope_list(params.envelope) .with_user_address(¶ms.from_addr) .with_user_full_name(¶ms.from_name); if let Some(spam_status) = params.spam_status { instance.set_spam_status(spam_status); } let mut input = Input::script("__script", script); let mut messages: Vec> = Vec::new(); let session_id = params.session_id; let mut reject_reason = None; let mut modifications = vec![]; let mut keep_id = usize::MAX; // Start event loop while let Some(result) = instance.run(input) { match result { Ok(event) => match event { Event::IncludeScript { name, optional } => { if let Some(script) = self.core.sieve.trusted_script(name.as_str()) { input = Input::script(name, script.clone()); } else if optional { input = false.into(); } else { trc::event!( Sieve(SieveEvent::ScriptNotFound), Id = script_id.clone(), SpanId = session_id, Details = name.as_str().to_string(), ); break; } } Event::ListContains { lists, values, match_as, } => { input = false.into(); 'outer: for list in lists { if let Some(store) = self.get_lookup_store(&list) { for value in &values { if let Ok(true) = store .key_exists(if !matches!(match_as, MatchAs::Lowercase) { value.clone() } else { value.to_lowercase() }) .await { input = true.into(); break 'outer; } } } else { trc::event!( Sieve(SieveEvent::ListNotFound), Id = script_id.clone(), SpanId = session_id, Details = list, ); } } } Event::Function { id, arguments } => { input = self .core .run_plugin( id, PluginContext { session_id, server: self, message: instance.message(), modifications: &mut modifications, access_token: params.access_token, arguments, }, ) .await; } Event::Keep { message_id, .. } => { keep_id = message_id; input = true.into(); } Event::Discard => { keep_id = usize::MAX - 1; input = true.into(); } Event::Reject { reason, .. } => { reject_reason = reason.into(); input = true.into(); } Event::SendMessage { recipient, notify, return_of_content, by_time, message_id, } => { // Build message let mut message = self.new_message( params.return_path.as_str(), MessageSource::Autogenerated, session_id, ); match recipient { Recipient::Address(rcpt) => { message.expand_and_add_recipient(rcpt, self).await; } Recipient::Group(rcpt_list) => { for rcpt in rcpt_list { message.expand_and_add_recipient(rcpt, self).await; } } Recipient::List(list) => { trc::event!( Sieve(SieveEvent::NotSupported), Id = script_id.clone(), SpanId = session_id, Details = list, Reason = "Sending to lists is not supported.", ); } } // Set notify flags let mut flags = 0; match notify { Notify::Never => { flags = RCPT_NOTIFY_NEVER; } Notify::Items(items) => { for item in items { flags |= match item { NotifyItem::Success => RCPT_NOTIFY_SUCCESS, NotifyItem::Failure => RCPT_NOTIFY_FAILURE, NotifyItem::Delay => RCPT_NOTIFY_DELAY, }; } } Notify::Default => (), } if flags > 0 { for rcpt in &mut message.message.recipients { rcpt.flags |= flags; } } // Set ByTime flags match by_time { ByTime::Relative { rlimit, mode, trace, } => { if trace { message.message.flags |= MAIL_BY_TRACE; } match mode { ByMode::Notify => { for domain in &mut message.message.recipients { domain.notify.due += rlimit; } } ByMode::Return => { for domain in &mut message.message.recipients { domain.notify.due += rlimit; } } ByMode::Default => (), } } ByTime::Absolute { alimit, mode, trace, } => { if trace { message.message.flags |= MAIL_BY_TRACE; } match mode { ByMode::Notify => { for domain in &mut message.message.recipients { domain.notify.due = alimit as u64; } } ByMode::Return => { let expires = (alimit as u64).saturating_sub(message.message.created); if expires > 0 { for domain in &mut message.message.recipients { domain.expires = QueueExpiry::Ttl(expires); } } } ByMode::Default => (), } } ByTime::None => (), }; // Set ret match return_of_content { Ret::Full => { message.message.flags |= MAIL_RET_FULL; } Ret::Hdrs => { message.message.flags |= MAIL_RET_HDRS; } Ret::Default => (), } // Queue message let is_forward = message_id == 0; let raw_message = if !is_forward { messages.get(message_id - 1).map(|m| m.as_slice()) } else { instance.message().raw_message().into() }; if let Some(raw_message) = raw_message.filter(|m| !m.is_empty()) { if let Some(metadata) = self.has_quota(&mut message).await { let dkim_signers = if let Some(sign_domain) = ¶ms.sign_domain { match self.dkim_signers(sign_domain).await { Ok(signers) => signers, Err(err) => { trc::error!( err.details("Failed to obtain DKIM signers") .caused_by(trc::location!()) ); None } } } else { None }; let _ = message .queue( QueueParams::new(raw_message, session_id, self) .with_dkim_signers(dkim_signers) .with_raw_headers_opt( params.headers.filter(|_| is_forward), ) .with_original_raw_message( instance.message().raw_message(), ) .with_metadata(metadata), ) .await; } else { trc::event!( Sieve(SieveEvent::QuotaExceeded), SpanId = session_id, Id = script_id.clone(), From = message.message.return_path, To = message .message .recipients .into_iter() .map(|r| trc::Value::from(r.address().to_string())) .collect::>(), ); } } input = true.into(); } Event::CreatedMessage { message, .. } => { messages.push(message); input = true.into(); } Event::SetEnvelope { envelope, value } => { modifications.push(ScriptModification::SetEnvelope { name: envelope, value, }); input = true.into(); } unsupported => { trc::event!( Sieve(SieveEvent::NotSupported), Id = script_id.clone(), SpanId = session_id, Reason = "Unsupported event", Details = format!("{unsupported:?}"), ); break; } }, Err(err) => { trc::event!( Sieve(SieveEvent::RuntimeError), Id = script_id.clone(), SpanId = session_id, Reason = err.to_string(), ); break; } } } // Keep id // 0 = use original message // MAX = implicit keep // MAX - 1 = discard message if keep_id == 0 { trc::event!( Sieve(SieveEvent::ActionAccept), SpanId = session_id, Id = script_id, Elapsed = time.elapsed(), ); ScriptResult::Accept { modifications } } else if let Some(mut reject_reason) = reject_reason { trc::event!( Sieve(SieveEvent::ActionReject), Id = script_id, SpanId = session_id, Details = reject_reason.clone(), Elapsed = time.elapsed(), ); if !reject_reason.ends_with('\n') { reject_reason.push_str("\r\n"); } let mut reject_bytes = reject_reason.as_bytes().iter(); if matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit()) && matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit()) && matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit()) && matches!(reject_bytes.next(), Some(ch) if ch == &b' ' ) { ScriptResult::Reject(reject_reason) } else { ScriptResult::Reject(format!("503 5.5.3 {reject_reason}")) } } else if keep_id != usize::MAX - 1 { if let Some(message) = messages.into_iter().nth(keep_id - 1) { trc::event!( Sieve(SieveEvent::ActionAccept), SpanId = session_id, Id = script_id, Elapsed = time.elapsed(), ); ScriptResult::Replace { message, modifications, } } else { trc::event!( Sieve(SieveEvent::ActionAcceptReplace), SpanId = session_id, Id = script_id, Elapsed = time.elapsed(), ); ScriptResult::Accept { modifications } } } else { trc::event!( Sieve(SieveEvent::ActionDiscard), SpanId = session_id, Id = script_id, Elapsed = time.elapsed() ); ScriptResult::Discard } } }