From 903f08575ac7cbd1258ecb6a98e208a411b8ca92 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Sun, 21 May 2023 10:50:25 +0000 Subject: [PATCH] SieveScripts and VacationResponse tests passing --- Cargo.lock | 2 +- crates/jmap/src/api/config.rs | 3 + crates/jmap/src/lib.rs | 25 +++-- crates/jmap/src/sieve/ingest.rs | 91 ++++++++++------ crates/jmap/src/sieve/set.rs | 72 +++++++++---- crates/jmap/src/vacation/set.rs | 22 ++-- crates/smtp/Cargo.toml | 4 +- crates/smtp/src/config/condition.rs | 40 +++---- crates/smtp/src/config/mod.rs | 12 +++ crates/smtp/src/core/mod.rs | 81 ++++++++++++-- crates/smtp/src/outbound/delivery.rs | 2 +- crates/smtp/src/outbound/mod.rs | 18 ++-- crates/store/src/blob/write.rs | 7 -- crates/store/src/write/batch.rs | 2 +- .../jmap_sieve/test_notify_fcc.sieve | 2 +- .../jmap_sieve/test_redirect_enclose.sieve | 2 +- tests/src/jmap/email_submission.rs | 4 +- tests/src/jmap/mod.rs | 13 ++- tests/src/jmap/sieve_script.rs | 102 ++++++++++-------- tests/src/jmap/vacation_response.rs | 67 +++++------- tests/src/lib.rs | 2 +- 21 files changed, 370 insertions(+), 203 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 197ae579..93bed1ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3380,6 +3380,7 @@ dependencies = [ "form_urlencoded", "http-body-util", "hyper 1.0.0-rc.3", + "lazy_static", "lru-cache", "mail-auth", "mail-builder", @@ -3387,7 +3388,6 @@ dependencies = [ "mail-send", "num_cpus", "parking_lot", - "privdrop", "rand", "rayon", "regex", diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs index a1d704f7..0dfcb7d6 100644 --- a/crates/jmap/src/api/config.rs +++ b/crates/jmap/src/api/config.rs @@ -48,6 +48,9 @@ impl crate::Config { mail_attachments_max_size: settings .property("jmap.email.max-attachment-size")? .unwrap_or(50000000), + mail_max_size: settings + .property("jmap.email.max-size")? + .unwrap_or(75000000), mail_parse_max_items: settings .property("jmap.email.parse.max-items")? .unwrap_or(50000000), diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index c6935b13..04d681b4 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -20,7 +20,7 @@ use services::{ delivery::spawn_delivery_manager, state::{self, init_state_manager, spawn_state_manager}, }; -use smtp::{core::SMTP, queue}; +use smtp::core::SMTP; use sqlx::{mysql::MySqlPoolOptions, postgres::PgPoolOptions, sqlite::SqlitePoolOptions}; use store::{ fts::Language, @@ -87,6 +87,7 @@ pub struct Config { pub mailbox_name_max_len: usize, pub mail_attachments_max_size: usize, pub mail_parse_max_items: usize, + pub mail_max_size: usize, pub sieve_max_script_name: usize, pub sieve_max_scripts: usize, @@ -667,11 +668,23 @@ impl JMAP { pub async fn write_batch(&self, batch: BatchBuilder) -> Result<(), MethodError> { self.store.write(batch.build()).await.map_err(|err| { - tracing::error!( - event = "error", - context = "write_batch", - error = ?err, - "Failed to write batch."); + match err { + store::Error::InternalError(err) => { + tracing::error!( + event = "error", + context = "write_batch", + error = ?err, + "Failed to write batch."); + } + store::Error::AssertValueFailed => { + tracing::debug!( + event = "assert_failed", + context = "write_batch", + "Failed to assert value." + ); + } + } + MethodError::ServerPartialFail }) } diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index fdbb2c1d..4a058ff6 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::Message; use sieve::{Envelope, Event, Input, Mailbox, Recipient}; +use smtp::core::{NullIo, Session, SessionAddress}; use store::{ ahash::AHashSet, write::{now, BatchBuilder, F_VALUE}, @@ -17,12 +18,6 @@ use crate::{ use super::ActiveScript; -struct OutgoingMessage { - pub mail_from: String, - pub rcpt_to: Vec, - pub message: Vec, -} - struct SieveMessage<'x> { pub raw_message: Cow<'x, [u8]>, pub file_into: Vec, @@ -94,7 +89,6 @@ impl JMAP { file_into: Vec::new(), flags: Vec::new(), }]; - let mut outgoing_messages = Vec::new(); let now = now(); let mut ingested_message = IngestedEmail { id: Id::default(), @@ -217,7 +211,9 @@ impl JMAP { do_deliver = true; } else { tracing::error!( - "Sieve filter failed: Unknown message id {}.", + context = "sieve_script_ingest", + event = "error", + "Unknown message id {}.", message_id ); } @@ -294,7 +290,9 @@ impl JMAP { do_deliver = true; } else { tracing::error!( - "Sieve filter failed: Unknown message id {}.", + context = "sieve_script_ingest", + event = "error", + "Unknown message id {}.", message_id ); } @@ -306,27 +304,49 @@ impl JMAP { .. } => { input = true.into(); + if let Some(message) = messages.get(message_id) { + if message.raw_message.len() <= self.config.mail_max_size { + let result = Session::::sieve( + self.smtp.clone(), + SessionAddress::new(mail_from.clone()), + match recipient { + Recipient::Address(rcpt) => vec![SessionAddress::new(rcpt)], + Recipient::Group(rcpts) => { + rcpts.into_iter().map(SessionAddress::new).collect() + } + Recipient::List(_) => { + // Not yet implemented + continue; + } + }, + message.raw_message.to_vec(), + ) + .queue_message() + .await; - outgoing_messages.push(OutgoingMessage { - mail_from: mail_from.clone(), - rcpt_to: match recipient { - Recipient::Address(rcpt) => vec![rcpt], - Recipient::Group(rcpts) => rcpts, - Recipient::List(_) => { - // Not yet implemented - continue; - } - }, - message: if let Some(message) = messages.get(message_id) { - message.raw_message.to_vec() - } else { - tracing::error!( - "Sieve filter failed: Unknown message id {}.", - message_id + tracing::debug!( + context = "sieve_script_ingest", + event = "send_message", + smtp_response = std::str::from_utf8(&result).unwrap() ); - continue; - }, - }); + } else { + tracing::warn!( + context = "sieve_script_ingest", + event = "message_too_large", + from = mail_from.as_str(), + size = message.raw_message.len(), + max_size = self.config.mail_max_size + ); + } + } else { + tracing::error!( + context = "sieve_script_ingest", + event = "error", + "Unknown message id {}.", + message_id + ); + continue; + } } Event::ListContains { .. } | Event::Execute { .. } | Event::Notify { .. } => { // Not allowed @@ -350,14 +370,17 @@ impl JMAP { } Err(err) => { - tracing::debug!("Sieve script runtime error: {}", err); + tracing::debug!( + context = "sieve_script_ingest", + event = "error", + reason = %err, + "Runtime error", + ); input = true.into(); } } } - let coco = "send outgoing"; - // Fail-safe, no discard and no keep seen, assume that something went wrong and file anyway. if !do_deliver && !do_discard { messages[0].file_into.push(INBOX_ID); @@ -374,7 +397,11 @@ impl JMAP { } else if let Some(message) = Message::parse(&sieve_message.raw_message) { message } else { - tracing::debug!("Failed to parse Sieve generated message."); + tracing::error!( + context = "sieve_script_ingest", + event = "error", + "Failed to parse Sieve generated message.", + ); continue; }; diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 7dc34e50..1623e549 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -204,28 +204,19 @@ impl JMAP { for id in will_destroy { let document_id = id.document_id(); if sieve_ids.contains(document_id) { - // Make sure the script is not active - if matches!( - self.get_property::>( - account_id, - Collection::SieveScript, - document_id, - Property::Value, - ) + if self + .sieve_script_delete(account_id, document_id, true) .await? - .and_then(|mut obj| obj.properties.remove(&Property::IsActive)), - Some(Value::Bool(true)) - ) { + { + changes.log_delete(Collection::SieveScript, document_id); + ctx.response.destroyed.push(id); + } else { ctx.response.not_destroyed.append( id, SetError::new(SetErrorType::ScriptIsActive) .with_description("Deactivate Sieve script before deletion."), ); - continue; } - self.sieve_script_delete(account_id, document_id).await?; - changes.log_delete(Collection::SieveScript, document_id); - ctx.response.destroyed.push(id); } else { ctx.response.not_destroyed.append(id, SetError::not_found()); } @@ -277,15 +268,46 @@ impl JMAP { &self, account_id: u32, document_id: u32, - ) -> Result<(), MethodError> { + fail_if_active: bool, + ) -> Result { + // Fetch record + let obj = self + .get_property::>( + account_id, + Collection::SieveScript, + document_id, + Property::Value, + ) + .await? + .ok_or_else(|| { + tracing::warn!( + event = "error", + context = "sieve_script_delete", + account_id = account_id, + document_id = document_id, + "Sieve script not found." + ); + MethodError::ServerPartialFail + })?; + + // Make sure the script is not active + if fail_if_active + && matches!( + obj.properties.get(&Property::IsActive), + Some(Value::Bool(true)) + ) + { + return Ok(false); + } + // Delete record let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) .with_collection(Collection::SieveScript) .delete_document(document_id) - .value(Property::Value, (), F_VALUE | F_CLEAR) - .value(Property::EmailIds, (), F_VALUE | F_CLEAR); + .value(Property::EmailIds, (), F_VALUE | F_CLEAR) + .custom(ObjectIndexBuilder::new(SCHEMA).with_current(obj)); self.write_batch(batch).await?; let _ = self .delete_blob(&BlobKind::Linked { @@ -294,7 +316,7 @@ impl JMAP { document_id, }) .await; - Ok(()) + Ok(true) } #[allow(clippy::blocks_in_if_conditions)] @@ -452,11 +474,11 @@ impl JMAP { pub async fn sieve_activate_script( &self, account_id: u32, - activate_id: Option, + mut activate_id: Option, ) -> Result, MethodError> { let mut changed_ids = Vec::new(); // Find the currently active script - let active_ids = self + let mut active_ids = self .filter( account_id, Collection::SieveScript, @@ -466,8 +488,12 @@ impl JMAP { .results; // Check if script is already active - if activate_id.map_or(false, |id| active_ids.contains(id)) { - return Ok(changed_ids); + if activate_id.map_or(false, |id| active_ids.remove(id)) { + if active_ids.is_empty() { + return Ok(changed_ids); + } else { + activate_id = None; + } } // Prepare batch diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 591d1238..e4a58a96 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -85,7 +85,9 @@ impl JMAP { } } _ => { - return Ok(response); + if will_destroy.is_empty() { + return Ok(response); + } } } @@ -176,8 +178,9 @@ impl JMAP { // Obtain current script let document_id = self.get_vacation_sieve_script_id(account_id).await?; let mut was_active = false; - let mut obj = ObjectIndexBuilder::new(SCHEMA).with_current_opt( - if let Some(document_id) = document_id { + + let mut obj = ObjectIndexBuilder::new(SCHEMA) + .with_current_opt(if let Some(document_id) = document_id { self.get_property::>>( account_id, Collection::SieveScript, @@ -186,7 +189,9 @@ impl JMAP { ) .await? .map(|value| { - batch.assert_value(Property::Value, &value); + batch + .update_document(document_id) + .assert_value(Property::Value, &value); was_active = value.inner.properties.get(&Property::IsActive) == Some(&Value::Bool(true)); value.inner @@ -195,8 +200,8 @@ impl JMAP { .into() } else { None - }, - ); + }) + .with_changes(changes); // Create sieve script only if there are changes let script_blob = if build_script { @@ -208,7 +213,6 @@ impl JMAP { // Write changes let document_id = if let Some(document_id) = document_id { batch - .update_document(document_id) .value(Property::EmailIds, (), F_VALUE | F_CLEAR) .custom(obj); change_log.log_insert(Collection::SieveScript, document_id); @@ -258,7 +262,8 @@ impl JMAP { if id.is_singleton() { if let Some(document_id) = self.get_vacation_sieve_script_id(account_id).await? { - self.sieve_script_delete(account_id, document_id).await?; + self.sieve_script_delete(account_id, document_id, false) + .await?; change_log.log_delete(Collection::SieveScript, document_id); response.destroyed.push(id); continue; @@ -364,7 +369,6 @@ impl JMAP { script.extend_from_slice(b"}\r\n"); } - // Compile script match self.sieve_compiler.compile(&script) { Ok(compiled_script) => { // Update blob length diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 9166efed..9804222b 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -44,9 +44,7 @@ reqwest = { version = "0.11", default-features = false, features = ["rustls-tls" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" num_cpus = "1.15.0" - -[target.'cfg(unix)'.dependencies] -privdrop = "0.5.3" +lazy_static = "1.4" [features] test_mode = [] diff --git a/crates/smtp/src/config/condition.rs b/crates/smtp/src/config/condition.rs index f798bd50..02555b08 100644 --- a/crates/smtp/src/config/condition.rs +++ b/crates/smtp/src/config/condition.rs @@ -156,24 +156,28 @@ impl ConfigCondition for Config { let value_str = self.value_require((&prefix, op_str))?; let value = match (key, &op) { - (EnvelopeKey::Listener, MatchType::Equal) => ConditionMatch::UInt( - ctx.servers - .iter() - .find_map(|s| { - if s.id == value_str { - s.internal_id.into() - } else { - None - } - }) - .ok_or_else(|| { - format!( - "Listener {:?} does not exist for property {:?}.", - value_str, - (&prefix, op_str).as_key() - ) - })?, - ), + (EnvelopeKey::Listener, MatchType::Equal) => { + ConditionMatch::UInt(if value_str != "sieve" { + ctx.servers + .iter() + .find_map(|s| { + if s.id == value_str { + s.internal_id.into() + } else { + None + } + }) + .ok_or_else(|| { + format!( + "Listener {:?} does not exist for property {:?}.", + value_str, + (&prefix, op_str).as_key() + ) + })? + } else { + u16::MAX + }) + } (EnvelopeKey::LocalIp | EnvelopeKey::RemoteIp, MatchType::Equal) => { ConditionMatch::IpAddrMask(value_str.parse_key((&prefix, op_str))?) } diff --git a/crates/smtp/src/config/mod.rs b/crates/smtp/src/config/mod.rs index d2a1c744..d92d513f 100644 --- a/crates/smtp/src/config/mod.rs +++ b/crates/smtp/src/config/mod.rs @@ -540,4 +540,16 @@ impl<'x> ConfigContext<'x> { } } +impl std::fmt::Debug for RelayHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RelayHost") + .field("address", &self.address) + .field("port", &self.port) + .field("protocol", &self.protocol) + .field("tls_implicit", &self.tls_implicit) + .field("tls_allow_invalid_certs", &self.tls_allow_invalid_certs) + .finish() + } +} + pub type Result = std::result::Result; diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index bc7c1113..4650a991 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -58,7 +58,7 @@ use crate::{ dane::{DnssecResolver, Tlsa}, mta_sts, }, - queue::{self, QuotaLimiter}, + queue::{self, DomainPart, QuotaLimiter}, reporting, }; @@ -393,12 +393,38 @@ impl AsyncRead for NullIo { } } +#[cfg(feature = "local_delivery")] +impl crate::inbound::IsTls for NullIo { + fn is_tls(&self) -> bool { + true + } + + fn write_tls_header(&self, _headers: &mut Vec) {} +} + +#[cfg(feature = "local_delivery")] +lazy_static::lazy_static! { +static ref SIEVE: Arc = Arc::new(utils::listener::ServerInstance { + id: "sieve".to_string(), + listener_id: u16::MAX, + protocol: utils::config::ServerProtocol::Lmtp, + hostname: "localhost".to_string(), + data: "localhost".to_string(), + tls_acceptor: None, + is_tls_implicit: true, + limiter: utils::listener::limiter::ConcurrencyLimiter::new(0), + shutdown_rx: tokio::sync::watch::channel(false).1, +}); +} + #[cfg(feature = "local_delivery")] impl Session { pub fn local( core: std::sync::Arc, instance: std::sync::Arc, - data: SessionData, + mail_from: SessionAddress, + rcpt_to: Vec, + message: Vec, ) -> Self { Session { state: State::None, @@ -406,16 +432,36 @@ impl Session { core, span: tracing::info_span!( "local_delivery", - "return_path" = if let Some(mail_from) = &data.mail_from { + "return_path" = if !mail_from.address_lcase.is_empty() { mail_from.address_lcase.as_str() } else { "<>" }, - "nrcpt" = data.rcpt_to.len(), - "size" = data.message.len(), + "nrcpt" = rcpt_to.len(), + "size" = message.len(), ), stream: NullIo(), - data, + data: SessionData { + local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + helo_domain: "localhost".into(), + mail_from: mail_from.into(), + rcpt_to, + rcpt_errors: 0, + message, + authenticated_as: "".into(), + auth_errors: 0, + priority: 0, + delivery_by: 0, + future_release: 0, + valid_until: Instant::now(), + bytes_left: 0, + messages_sent: 0, + iprev: None, + spf_ehlo: None, + spf_mail_from: None, + dnsbl_error: None, + }, params: SessionParameters { timeout: Default::default(), ehlo_require: Default::default(), @@ -443,4 +489,27 @@ impl Session { in_flight: vec![], } } + + pub fn sieve( + core: std::sync::Arc, + mail_from: SessionAddress, + rcpt_to: Vec, + message: Vec, + ) -> Self { + Self::local(core, SIEVE.clone(), mail_from, rcpt_to, message) + } +} + +#[cfg(feature = "local_delivery")] +impl SessionAddress { + pub fn new(address: String) -> Self { + let address_lcase = address.to_lowercase(); + SessionAddress { + domain: address_lcase.domain_part().to_string(), + address_lcase, + address, + flags: 0, + dsn_info: None, + } + } } diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index 5a800e6f..80c7ba40 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -313,7 +313,7 @@ impl DeliveryAttempt { // Obtain remote hosts list let mx_list; - if is_smtp { + if is_smtp && remote_hosts.is_empty() { // Lookup MX mx_list = match core.resolvers.dns.mx_lookup(&domain.domain).await { Ok(mx) => mx, diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index 43fc3b85..ebd66380 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -231,6 +231,7 @@ impl From> for DeliveryAttempt { } } +#[derive(Debug)] enum NextHop<'x> { Relay(&'x RelayHost), MX(&'x str), @@ -253,14 +254,15 @@ impl<'x> NextHop<'x> { #[inline(always)] fn fqdn_hostname(&self) -> Cow<'_, str> { - let host = match self { - NextHop::MX(host) => host, - NextHop::Relay(host) => host.address.as_str(), - }; - if !host.ends_with('.') { - format!("{host}.").into() - } else { - (*host).into() + match self { + NextHop::MX(host) => { + if !host.ends_with('.') { + format!("{host}.").into() + } else { + (*host).into() + } + } + NextHop::Relay(host) => host.address.as_str().into(), } } diff --git a/crates/store/src/blob/write.rs b/crates/store/src/blob/write.rs index f2c1b875..1bf677e4 100644 --- a/crates/store/src/blob/write.rs +++ b/crates/store/src/blob/write.rs @@ -15,13 +15,6 @@ impl Store { BlobStore::Local(base_path) => { let blob_path = get_path(base_path, kind)?; - let metadata = fs::metadata(&blob_path).await; - if let Ok(metadata) = metadata { - if metadata.len() as usize == data.len() { - return Ok(false); - } - } - fs::create_dir_all(blob_path.parent().unwrap()).await?; let mut blob_file = File::create(&blob_path).await?; blob_file.write_all(data).await?; diff --git a/crates/store/src/write/batch.rs b/crates/store/src/write/batch.rs index 102a16cd..a013a9c5 100644 --- a/crates/store/src/write/batch.rs +++ b/crates/store/src/write/batch.rs @@ -133,7 +133,7 @@ impl BatchBuilder { pub fn is_empty(&self) -> bool { self.ops.is_empty() - || self.ops.iter().any(|op| { + || !self.ops.iter().any(|op| { !matches!( op, Operation::AccountId { .. } diff --git a/tests/resources/jmap_sieve/test_notify_fcc.sieve b/tests/resources/jmap_sieve/test_notify_fcc.sieve index 905c1d88..5285fe35 100644 --- a/tests/resources/jmap_sieve/test_notify_fcc.sieve +++ b/tests/resources/jmap_sieve/test_notify_fcc.sieve @@ -3,7 +3,7 @@ require ["enotify", "fcc", "mailbox", "editheader", "imap4flags"]; if header :matches "Subject" "*TPS*" { notify :message "It's time to file your TPS report." :fcc "Notifications" :create - "mailto:sms_gateway@example.com?subject=It's%20TPS-o-clock"; + "mailto:sms_gateway@remote.org?subject=It's%20TPS-o-clock"; deleteheader "Subject"; addheader "Subject" "${1}**censored**${2}"; diff --git a/tests/resources/jmap_sieve/test_redirect_enclose.sieve b/tests/resources/jmap_sieve/test_redirect_enclose.sieve index 4a8726b2..c9c9e6d2 100644 --- a/tests/resources/jmap_sieve/test_redirect_enclose.sieve +++ b/tests/resources/jmap_sieve/test_redirect_enclose.sieve @@ -1,5 +1,5 @@ require ["enclose"]; enclose :subject "Check this out" "Attached you'll find a message I just received."; -redirect "jane@example.com"; +redirect "jane@remote.org"; discard; diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index 09568a14..359d4bc7 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -580,11 +580,11 @@ pub async fn assert_message_delivery( ) { match tokio::time::timeout(Duration::from_millis(3000), event_rx.recv()).await { Ok(Some(message)) => { + println!("Got message [{}]", message.message); + assert_eq!(message.mail_from, expected_message.mail_from); assert_eq!(message.rcpt_to, expected_message.rcpt_to); - println!("Got message [{}]", message.message); - if let Some(needle) = expected_message.message.strip_prefix('@') { assert!( message.message.contains(needle), diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index ca13be54..b54a1cbe 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -83,8 +83,18 @@ type = "system" [queue.outbound] next-hop = [ { if = "rcpt-domain", in-list = "list/domains", then = "local" }, + { if = "rcpt-domain", eq = "remote.org", then = "mock-smtp" }, { else = false } ] +[remote."mock-smtp"] +address = "localhost" +port = 9999 +protocol = "smtp" + +[remote."mock-smtp".tls] +implicit = false +allow-invalid-certs = true + [store] db.path = "{TMP}/sqlite.db" blob.path = "{TMP}" @@ -172,7 +182,8 @@ pub async fn jmap_tests() { //auth_oauth::test(params.server.clone(), &mut params.client).await; //event_source::test(params.server.clone(), &mut params.client).await; //push_subscription::test(params.server.clone(), &mut params.client).await; - sieve_script::test(params.server.clone(), &mut params.client).await; + //sieve_script::test(params.server.clone(), &mut params.client).await; + vacation_response::test(params.server.clone(), &mut params.client).await; let websockets = "todo"; diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index 45252e3f..a82abf20 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -1,6 +1,11 @@ -use std::{fs, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + fs, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; -use jmap::{JMAP, SUPERUSER_ID}; +use jmap::JMAP; use jmap_client::{ client::Client, core::set::{SetError, SetErrorType}, @@ -8,14 +13,13 @@ use jmap_client::{ sieve::query::{Comparator, Filter}, Error, }; -use jmap_proto::types::id::Id; use crate::jmap::{ delivery::SmtpConnection, email_submission::{assert_message_delivery, spawn_mock_smtp_server, MockMessage}, + mailbox::destroy_all_mailboxes, test_account_create, }; -use crate::smtp::session::VerifyResponse; pub async fn test(server: Arc, client: &mut Client) { println!("Running Sieve tests..."); @@ -134,10 +138,10 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap(); lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: TPS Report\r\n", "\r\n", @@ -222,10 +226,10 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap(); lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "Bcc: Undisclosed recipients;\r\n", "Message-ID: <1234@example.com>\r\n", "Subject: Holidays\r\n", @@ -249,13 +253,20 @@ pub async fn test(server: Arc, client: &mut Client) { // Let one sec duplicate ids expire tokio::time::sleep(Duration::from_millis(1100)).await; + // Start mock SMTP server + let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server(); + server.smtp.resolvers.dns.ipv4_add( + "localhost", + vec!["127.0.0.1".parse().unwrap()], + Instant::now() + Duration::from_secs(10), + ); + // Run reject and duplicate check tests - let test = "fd"; - /*lmtp.ingest_with_code( - "bill@example.com", + lmtp.ingest( + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "Bcc: Undisclosed recipients;\r\n", "Message-ID: <1234@example.com>\r\n", "Subject: Holidays\r\n", @@ -263,10 +274,9 @@ pub async fn test(server: Arc, client: &mut Client) { "Remember to file your T.P.S. reports before ", "going on holidays." ), - 5, ) - .await - .assert_contains("No soup for you"); + .await; + assert_eq!( client .email_query(None::, None::>) @@ -276,7 +286,14 @@ pub async fn test(server: Arc, client: &mut Client) { .len(), 1, "Reject failed." - );*/ + ); + + assert_message_delivery( + &mut smtp_rx, + MockMessage::new("<>", [""], "@No soup for you"), + false, + ) + .await; // Run include tests client @@ -287,11 +304,11 @@ pub async fn test(server: Arc, client: &mut Client) { .sieve_script_create("test_include", get_script("test_include"), true) .await .unwrap(); - lmtp.ingest_with_code( - "bill@example.com", + lmtp.ingest( + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "Bcc: Undisclosed recipients;\r\n", "Message-ID: <1234@example.com>\r\n", "Subject: Holidays\r\n", @@ -299,14 +316,19 @@ pub async fn test(server: Arc, client: &mut Client) { "Remember to file your T.P.S. reports before ", "going on holidays." ), - 5, ) - .await - .assert_contains("Rejected from an included script"); + .await; - // Start mock SMTP server - let coco = "fd"; - /*let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server(); + assert_message_delivery( + &mut smtp_rx, + MockMessage::new( + "<>", + [""], + "@Rejected from an included script", + ), + false, + ) + .await; // Run enclose + redirect tests client @@ -318,10 +340,10 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap(); lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: TPS Report\r\n", "\r\n", @@ -334,7 +356,7 @@ pub async fn test(server: Arc, client: &mut Client) { &mut smtp_rx, MockMessage::new( "", - [""], + [""], "@Attached you'll find", ), false, @@ -357,10 +379,10 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap(); lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: Urgently I need those TPS Reports\r\n", "\r\n", @@ -374,7 +396,7 @@ pub async fn test(server: Arc, client: &mut Client) { &mut smtp_rx, MockMessage::new( "", - [""], + [""], "@It's TPS-o-clock", ), false, @@ -436,19 +458,15 @@ pub async fn test(server: Arc, client: &mut Client) { smtp_settings.lock().do_stop = true; - */ - // Remove test data - let todo = "fix"; - /*for account_id in [&account_id, &domain_id] { - client - .set_default_account_id(Id::new(SUPERUSER_ID as u64)) - .principal_destroy(account_id) - .await - .unwrap(); + client.sieve_script_deactivate().await.unwrap(); + let mut request = client.build(); + request.query_sieve_script(); + for id in request.send_query_sieve_script().await.unwrap().take_ids() { + client.sieve_script_destroy(&id).await.unwrap(); } - server.store.principal_purge().unwrap(); - server.store.assert_is_empty();*/ + destroy_all_mailboxes(client).await; + server.store.assert_is_empty().await; } fn get_script(name: &str) -> Vec { diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 265fd760..53c14916 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -1,35 +1,34 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; use chrono::{Duration, Utc}; -use jmap::{JMAP, SUPERUSER_ID}; +use jmap::JMAP; use jmap_client::client::Client; -use jmap_proto::types::id::Id; use crate::jmap::{ delivery::SmtpConnection, email_submission::{ assert_message_delivery, expect_nothing, spawn_mock_smtp_server, MockMessage, }, + mailbox::destroy_all_mailboxes, + test_account_create, }; pub async fn test(server: Arc, client: &mut Client) { println!("Running Vacation Response tests..."); - // Create INBOX - let domain_id = client - .set_default_account_id(Id::new(SUPERUSER_ID as u64)) - .domain_create("example.com") + // Create test account + let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") .await - .unwrap() - .take_id(); - let account_id = client - .individual_create("jdoe@example.com", "12345", "John Doe") - .await - .unwrap() - .take_id(); + .to_string(); + client.set_default_account_id(&account_id); // Start mock SMTP server let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server(); + server.smtp.resolvers.dns.ipv4_add( + "localhost", + vec!["127.0.0.1".parse().unwrap()], + Instant::now() + std::time::Duration::from_secs(10), + ); // Let people know that we'll be down in Kokomo client @@ -47,10 +46,10 @@ pub async fn test(server: Arc, client: &mut Client) { // Send a message lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: TPS Report\r\n", "\r\n", @@ -63,7 +62,7 @@ pub async fn test(server: Arc, client: &mut Client) { // Await vacation response assert_message_delivery( &mut smtp_rx, - MockMessage::new("", [""], "@Kokomo"), + MockMessage::new("", [""], "@Kokomo"), false, ) .await; @@ -71,10 +70,10 @@ pub async fn test(server: Arc, client: &mut Client) { // Further messages from the same recipient should not // trigger a vacation response lmtp.ingest( - "bill@example.com", + "bill@remote.org", &["jdoe@example.com"], concat!( - "From: bill@example.com\r\n", + "From: bill@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: TPS Report -- friendly reminder\r\n", "\r\n", @@ -88,7 +87,7 @@ pub async fn test(server: Arc, client: &mut Client) { // Messages from MAILER-DAEMON should not // trigger a vacation response lmtp.ingest( - "MAILER-DAEMON@example.com", + "MAILER-DAEMON@remote.org", &["jdoe@example.com"], concat!( "From: MAILER-DAEMON@example.com\r\n", @@ -108,10 +107,10 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap(); lmtp.ingest( - "jane_smith@example.com", + "jane_smith@remote.org", &["jdoe@example.com"], concat!( - "From: jane_smith@example.com\r\n", + "From: jane_smith@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: When were you going on holidays?\r\n", "\r\n", @@ -128,10 +127,10 @@ pub async fn test(server: Arc, client: &mut Client) { .unwrap(); smtp_settings.lock().do_stop = true; lmtp.ingest( - "jane_smith@example.com", + "jane_smith@remote.org", &["jdoe@example.com"], concat!( - "From: jane_smith@example.com\r\n", + "From: jane_smith@remote.org\r\n", "To: jdoe@example.com\r\n", "Subject: When were you going on holidays?\r\n", "\r\n", @@ -143,25 +142,13 @@ pub async fn test(server: Arc, client: &mut Client) { assert_message_delivery( &mut smtp_rx, - MockMessage::new( - "", - [""], - "@Kokomo", - ), + MockMessage::new("", [""], "@Kokomo"), false, ) .await; // Remove test data - let implement = "true"; - /*for account_id in [&account_id, &domain_id] { - client - .set_default_account_id(Id::new(SUPERUSER_ID as u64)) - .principal_destroy(account_id) - .await - .unwrap(); - } - server.store.principal_purge().unwrap(); - server.store.assert_is_empty(); - */ + client.vacation_response_destroy().await.unwrap(); + destroy_all_mailboxes(client).await; + server.store.assert_is_empty().await; } diff --git a/tests/src/lib.rs b/tests/src/lib.rs index c820c0ba..db82b811 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; #[cfg(test)] pub mod jmap; #[cfg(test)] -pub mod smtp; +//pub mod smtp; #[cfg(test)] pub mod store;