SieveScrtipts partially tested.

This commit is contained in:
Mauro D
2023-05-20 18:50:24 +00:00
parent bc40256ac4
commit e7d57e84b0
35 changed files with 2512 additions and 68 deletions

View File

@@ -41,3 +41,4 @@ serial_test = "2.0.0"
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
num_cpus = "1.15.0"
async-trait = "0.1.68"
chrono = "0.4"

View File

@@ -47,7 +47,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) {
),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
server
.get_document_ids(
@@ -78,7 +78,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) {
),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
server
.get_document_ids(
@@ -120,7 +120,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) {
),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
for (account_id, num_messages) in [(&account_id_1, 3), (&account_id_2, 1), (&account_id_3, 1)] {
assert_eq!(
server
@@ -154,7 +154,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) {
10,
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
for (account_id, num_messages) in [(&account_id_1, 3), (&account_id_2, 2), (&account_id_3, 2)] {
assert_eq!(
server
@@ -192,7 +192,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) {
),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
for (account_id, num_messages) in [(&account_id_1, 4), (&account_id_2, 3), (&account_id_3, 3)] {
assert_eq!(
server
@@ -236,14 +236,16 @@ impl SmtpConnection {
self.rcpt_to(recipient, 2).await;
}
self.data(3).await;
self.data_bytes(message, recipients.len(), code).await
let result = self.data_bytes(message, recipients.len(), code).await;
tokio::time::sleep(Duration::from_millis(200)).await;
result
}
pub async fn ingest(&mut self, from: &str, recipients: &[&str], message: &str) {
self.ingest_with_code(from, recipients, message, 2).await;
}
pub async fn ingest_chunked(
async fn ingest_chunked(
&mut self,
from: &str,
recipients: &[&str],
@@ -258,6 +260,7 @@ impl SmtpConnection {
self.bdat(std::str::from_utf8(chunk).unwrap(), 2).await;
}
self.bdat_last("", recipients.len(), 2).await;
tokio::time::sleep(Duration::from_millis(200)).await;
}
pub async fn connect() -> Self {

View File

@@ -0,0 +1,628 @@
use std::{sync::Arc, time::Duration};
use ahash::AHashMap;
use jmap::{JMAP, SUPERUSER_ID};
use jmap_client::{
client::Client,
core::set::{SetError, SetErrorType, SetObject},
email_submission::{Address, Delivered, DeliveryStatus, Displayed, UndoStatus},
mailbox::Role,
Error,
};
use jmap_proto::types::id::Id;
use mail_parser::DateTime;
use store::parking_lot::Mutex;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::TcpListener,
sync::mpsc,
};
use crate::jmap::email_set::assert_email_properties;
#[derive(Default, Debug, PartialEq, Eq)]
pub struct MockMessage {
pub mail_from: String,
pub rcpt_to: Vec<String>,
pub message: String,
}
impl MockMessage {
pub fn new<T, U>(mail_from: T, rcpt_to: U, message: T) -> Self
where
T: Into<String>,
U: IntoIterator<Item = T>,
{
Self {
mail_from: mail_from.into(),
rcpt_to: rcpt_to.into_iter().map(|s| s.into()).collect(),
message: message.into(),
}
}
}
#[derive(Default)]
pub struct MockSMTPSettings {
pub fail_mail_from: bool,
pub fail_rcpt_to: bool,
pub fail_message: bool,
pub do_stop: bool,
}
const TEST_DKIM_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFC
jxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gb
to/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB
AoGBALmn+XwWk7akvkUlqb+dOxyLB9i5VBVfje89Teolwc9YJT36BGN/l4e0l6QX
/1//6DWUTB3KI6wFcm7TWJcxbS0tcKZX7FsJvUz1SbQnkS54DJck1EZO/BLa5ckJ
gAYIaqlA9C0ZwM6i58lLlPadX/rtHb7pWzeNcZHjKrjM461ZAkEA+itss2nRlmyO
n1/5yDyCluST4dQfO8kAB3toSEVc7DeFeDhnC1mZdjASZNvdHS4gbLIA1hUGEF9m
3hKsGUMMPwJBAPW5v/U+AWTADFCS22t72NUurgzeAbzb1HWMqO4y4+9Hpjk5wvL/
eVYizyuce3/fGke7aRYw/ADKygMJdW8H/OcCQQDz5OQb4j2QDpPZc0Nc4QlbvMsj
7p7otWRO5xRa6SzXqqV3+F0VpqvDmshEBkoCydaYwc2o6WQ5EBmExeV8124XAkEA
qZzGsIxVP+sEVRWZmW6KNFSdVUpk3qzK0Tz/WjQMe5z0UunY9Ax9/4PVhp/j61bf
eAYXunajbBSOLlx4D+TunwJBANkPI5S9iylsbLs6NkaMHV6k5ioHBBmgCak95JGX
GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
-----END RSA PRIVATE KEY-----"#;
#[allow(clippy::disallowed_types)]
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
println!("Running E-mail submissions tests...");
// Start mock SMTP server
let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
// Create an identity without using a valid address should fail
match client
.set_default_account_id(Id::new(1).to_string())
.identity_create("John Doe", "jdoe@example.com")
.await
.unwrap_err()
{
Error::Set(err) => assert_eq!(err.error(), &SetErrorType::InvalidProperties),
err => panic!("Unexpected error: {:?}", err),
}
// Create a domain and a test account
let domain_id = client
.set_default_account_id(Id::new(0))
.domain_create("example.com")
.await
.unwrap()
.take_id();
let account_id = client
.individual_create("jdoe@example.com", "12345", "John Doe")
.await
.unwrap()
.take_id();
let identity_id = client
.set_default_account_id(&account_id)
.identity_create("John Doe", "jdoe@example.com")
.await
.unwrap()
.take_id();
// Create test mailboxes
let mailbox_id = client
.mailbox_create("JMAP EmailSubmission", None::<String>, Role::None)
.await
.unwrap()
.take_id();
let mailbox_id_2 = client
.mailbox_create("JMAP EmailSubmission 2", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Import an email without any recipients
let email_id = client
.email_import(
b"From: jdoe@example.com\nSubject: hey\n\ntest".to_vec(),
[&mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
// Submission without a valid emailId or identityId should fail
assert!(matches!(
client
.email_submission_create(Id::new(123456).to_string(), &identity_id)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
assert!(matches!(
client
.email_submission_create(&email_id, Id::new(123456).to_string())
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Submissions of e-mails without any recipients should fail
assert!(matches!(
client
.email_submission_create(&email_id, &identity_id)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Submissions with an envelope that does not match
// the identity from address should fail
assert!(matches!(
client
.email_submission_create_envelope(
&email_id,
&identity_id,
"other_address@example.com",
Vec::<Address>::new(),
)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Submit a valid message submission
let email_body =
"From: jdoe@example.com\r\nTo: jane_smith@example.com\r\nSubject: hey\r\n\r\ntest";
let email_id = client
.email_import(
email_body.as_bytes().to_vec(),
[&mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
client
.email_submission_create(&email_id, &identity_id)
.await
.unwrap();
// Confirm that the message has been delivered
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com>",
["<jane_smith@example.com>"],
email_body,
),
false,
)
.await;
// Manually add recipients to the envelope and confirm submission
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
"jdoe@example.com",
[
"tim@foobar.com", // Should be de-duplicated
"tim@foobar.com",
"tim@foobar.com ",
" james@other_domain.com ", // Should be sanitized
" secret_rcpt@test.com ",
],
)
.await
.unwrap()
.take_id();
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com>",
[
"<james@other_domain.com>",
"<secret_rcpt@test.com>",
"<tim@foobar.com>",
],
email_body,
),
false,
)
.await;
// Confirm that the email submission status was updated
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(email_submission.undo_status().unwrap(), &UndoStatus::Final);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"tim@foobar.com".to_string(),
DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown)
),
(
"secret_rcpt@test.com".to_string(),
DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown)
),
(
"james@other_domain.com".to_string(),
DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown)
),
])
);
// SMTP rejects some of the recipients
smtp_settings.lock().fail_rcpt_to = true;
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
"jdoe@example.com",
["tim@foobar.com", "james@other_domain.com", "jane@test.com"],
)
.await
.unwrap()
.take_id();
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<jdoe@example.com>", ["<tim@foobar.com>"], email_body),
false,
)
.await;
// Confirm that all delivery failures were included
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(email_submission.undo_status().unwrap(), &UndoStatus::Final);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"james@other_domain.com".to_string(),
DeliveryStatus::new(
"550 I refuse to accept that recipient.",
Delivered::No,
Displayed::Unknown
)
),
(
"jane@test.com".to_string(),
DeliveryStatus::new(
"550 I refuse to accept that recipient.",
Delivered::No,
Displayed::Unknown
)
),
(
"tim@foobar.com".to_string(),
DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown)
),
])
);
smtp_settings.lock().fail_rcpt_to = false;
// SMTP rejects the message
smtp_settings.lock().fail_message = true;
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
"jdoe@example.com",
["tim@foobar.com", "james@other_domain.com", "jane@test.com"],
)
.await
.unwrap()
.take_id();
expect_nothing(&mut smtp_rx).await;
// Confirm that all delivery failures were included
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(
email_submission.undo_status().unwrap(),
&UndoStatus::Canceled
);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"james@other_domain.com".to_string(),
DeliveryStatus::new(
"503 Thank you but I am saving myself for dessert.",
Delivered::No,
Displayed::Unknown
)
),
(
"jane@test.com".to_string(),
DeliveryStatus::new(
"503 Thank you but I am saving myself for dessert.",
Delivered::No,
Displayed::Unknown
)
),
(
"tim@foobar.com".to_string(),
DeliveryStatus::new(
"503 Thank you but I am saving myself for dessert.",
Delivered::No,
Displayed::Unknown
)
),
])
);
smtp_settings.lock().fail_message = false;
// Enable DKIM for the domain
client
.set_default_account_id(Id::from(SUPERUSER_ID))
.domain_enable_dkim(&domain_id, TEST_DKIM_KEY, "my-selector", None)
.await
.unwrap();
client.set_default_account_id(&account_id);
// Confirm that the sendAt property is updated when using FUTURERELEASE
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
Address::new("jdoe@example.com").parameter("HOLDUNTIL", Some("2079-11-20T05:00:00Z")),
["jane_smith@example.com"],
)
.await
.unwrap()
.take_id();
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com> HOLDUNTIL=2079-11-20T05:00:00Z",
["<jane_smith@example.com>"],
email_body,
),
true,
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(
email_submission.send_at().unwrap(),
DateTime::parse_rfc3339("2079-11-20T05:00:00Z")
.unwrap()
.to_timestamp()
);
// Verify onSuccessUpdateEmail action
let mut request = client.build();
let set_request = request.set_email_submission();
let create_id = set_request
.create()
.email_id(&email_id)
.identity_id(&identity_id)
.create_id()
.unwrap();
set_request
.arguments()
.on_success_update_email(&create_id)
.keyword("$draft", true)
.mailbox_id(&mailbox_id, false)
.mailbox_id(&mailbox_id_2, true);
request.send().await.unwrap().unwrap_method_responses();
assert_email_properties(client, &email_id, &[&mailbox_id_2], &["$draft"]).await;
// Verify onSuccessDestroyEmail action
smtp_settings.lock().do_stop = true;
let mut request = client.build();
let set_request = request.set_email_submission();
let create_id = set_request
.create()
.email_id(&email_id)
.identity_id(&identity_id)
.create_id()
.unwrap();
set_request.arguments().on_success_destroy_email(&create_id);
request.send().await.unwrap().unwrap_method_responses();
assert!(client
.email_get(&email_id, None::<Vec<_>>)
.await
.unwrap()
.is_none());
// Destroy the created mailbox, identity and all submissions
let todo = "true";
/*client
.set_default_account_id(Id::from(SUPERUSER_ID))
.principal_destroy(&account_id)
.await
.unwrap();
client.principal_destroy(&domain_id).await.unwrap();
server.store.principal_purge().unwrap();
server.store.assert_is_empty();*/
}
pub fn spawn_mock_smtp_server() -> (mpsc::Receiver<MockMessage>, Arc<Mutex<MockSMTPSettings>>) {
// Create channels
let (event_tx, event_rx) = mpsc::channel::<MockMessage>(100);
let _settings = Arc::new(Mutex::new(MockSMTPSettings::default()));
let settings = _settings.clone();
// Start mock SMTP server
tokio::spawn(async move {
let listener = TcpListener::bind("127.0.0.1:9999")
.await
.unwrap_or_else(|e| {
panic!("Failed to bind mock SMTP server to 127.0.0.1:9999: {}", e);
});
while let Ok((mut stream, _)) = listener.accept().await {
let (rx, mut tx) = stream.split();
let mut rx = BufReader::new(rx);
let mut buf = String::with_capacity(128);
let mut message = MockMessage::default();
tx.write_all(b"220 [127.0.0.1] Clueless host service ready\r\n")
.await
.unwrap();
while rx.read_line(&mut buf).await.is_ok() {
print!("-> {}", buf);
if buf.starts_with("EHLO") {
tx.write_all(b"250 Hi there, but I have no extensions to offer :-(\r\n")
.await
.unwrap();
} else if buf.starts_with("MAIL FROM") {
if settings.lock().fail_mail_from {
tx.write_all("552-I do not\r\n552 like that MAIL FROM.\r\n".as_bytes())
.await
.unwrap();
} else {
message.mail_from = buf.split_once(':').unwrap().1.trim().to_string();
tx.write_all(b"250 OK\r\n").await.unwrap();
}
} else if buf.starts_with("RCPT TO") {
if settings.lock().fail_rcpt_to && !buf.contains("foobar.com") {
tx.write_all(
"550-I refuse to\r\n550 accept that recipient.\r\n".as_bytes(),
)
.await
.unwrap();
} else {
message
.rcpt_to
.push(buf.split(':').nth(1).unwrap().trim().to_string());
tx.write_all(b"250 OK\r\n").await.unwrap();
}
} else if buf.starts_with("DATA") {
if settings.lock().fail_message {
tx.write_all(
"503-Thank you but I am\r\n503 saving myself for dessert.\r\n"
.as_bytes(),
)
.await
.unwrap();
} else if !message.mail_from.is_empty() && !message.rcpt_to.is_empty() {
tx.write_all(b"354 Start feeding me now some quality content please\r\n")
.await
.unwrap();
buf.clear();
while rx.read_line(&mut buf).await.is_ok() {
if buf.starts_with('.') {
message.message = message.message.trim().to_string();
break;
} else {
message.message += &buf;
buf.clear();
}
}
tx.write_all(b"250 Great success!\r\n").await.unwrap();
message.rcpt_to.sort_unstable();
event_tx.send(message).await.unwrap();
message = MockMessage::default();
} else {
tx.write_all("554 You forgot to tell me a few things.\r\n".as_bytes())
.await
.unwrap();
}
} else if buf.starts_with("QUIT") {
tx.write_all("250 Arrivederci!\r\n".as_bytes())
.await
.unwrap();
break;
} else if buf.starts_with("RSET") {
tx.write_all("250 Your wish is my command.\r\n".as_bytes())
.await
.unwrap();
message = MockMessage::default();
} else {
println!("Unknown command: {}", buf.trim());
}
buf.clear();
}
if settings.lock().do_stop {
println!("Mock SMTP server stopped.");
break;
}
}
});
(event_rx, _settings)
}
pub async fn assert_message_delivery(
event_rx: &mut mpsc::Receiver<MockMessage>,
expected_message: MockMessage,
expect_dkim: bool,
) {
match tokio::time::timeout(Duration::from_millis(3000), event_rx.recv()).await {
Ok(Some(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),
"[{}] needle = {:?}",
message.message,
needle
);
} else {
let message = if expect_dkim {
if message.message.starts_with("DKIM-Signature:") {
message.message.split_once('\n').unwrap().1
} else {
panic!(
"Expected DKIM-Signature header but got: {}",
message.message
);
}
} else {
&message.message
};
assert_eq!(message, expected_message.message);
}
}
result => {
panic!(
"Timeout waiting for message {:?}: {:?}",
expected_message, result
);
}
}
}
pub async fn expect_nothing(event_rx: &mut mpsc::Receiver<MockMessage>) {
match tokio::time::timeout(Duration::from_millis(500), event_rx.recv()).await {
Err(_) => {}
message => {
panic!("Received a message when expecting nothing: {:?}", message);
}
}
}

View File

@@ -21,11 +21,14 @@ pub mod email_query;
pub mod email_query_changes;
pub mod email_search_snippet;
pub mod email_set;
pub mod email_submission;
pub mod event_source;
pub mod mailbox;
pub mod push_subscription;
pub mod sieve_script;
pub mod thread_get;
pub mod thread_merge;
pub mod vacation_response;
const SERVER: &str = r#"
[server]
@@ -123,6 +126,7 @@ address = "sqlite::memory:"
uid-by-login = "SELECT ROWID - 1 FROM users WHERE login = ?"
login-by-uid = "SELECT login FROM users WHERE ROWID - 1 = ?"
secret-by-uid = "SELECT secret FROM users WHERE ROWID - 1 = ?"
name-by-uid = "SELECT name FROM users WHERE ROWID - 1 = ?"
gids-by-uid = "SELECT gid FROM groups WHERE uid = ?"
uids-by-address = "SELECT uid FROM emails WHERE email = ?"
addresses-by-uid = "SELECT email FROM emails WHERE uid = ?"
@@ -166,8 +170,11 @@ pub async fn jmap_tests() {
//auth_acl::test(params.server.clone(), &mut params.client).await;
//auth_limits::test(params.server.clone(), &mut params.client).await;
//auth_oauth::test(params.server.clone(), &mut params.client).await;
event_source::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;
let websockets = "todo";
if delete {
params.temp_dir.delete();
@@ -197,7 +204,7 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest {
let smtp = SMTP::init(&config, &servers, delivery_tx)
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(&config, delivery_rx)
let jmap = JMAP::init(&config, delivery_rx, smtp.clone())
.await
.failed("Invalid configuration file");
let shutdown_tx = servers.spawn(|server, shutdown_rx| {

View File

@@ -0,0 +1,460 @@
use std::{fs, path::PathBuf, sync::Arc, time::Duration};
use jmap::{JMAP, SUPERUSER_ID};
use jmap_client::{
client::Client,
core::set::{SetError, SetErrorType},
email, mailbox,
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},
test_account_create,
};
use crate::smtp::session::VerifyResponse;
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
println!("Running Sieve tests...");
// Create test account
let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe")
.await
.to_string();
client.set_default_account_id(&account_id);
// Validate scripts
client
.sieve_script_validate(get_script("validate_ok"))
.await
.unwrap();
assert!(matches!(
client
.sieve_script_validate(get_script("validate_error"))
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidScript,
..
}))
));
// Create 5 Sieve scripts, all deactivated.
let mut script_ids = Vec::new();
for i in 0..5 {
script_ids.push(
client
.sieve_script_create(
format!("script_{}", i + 1),
format!("require \"fileinto\"; fileinto \"{}\";", i + 1).into_bytes(),
false,
)
.await
.unwrap()
.take_id(),
);
}
let response = client
.sieve_script_query(Filter::is_active(false).into(), [Comparator::name()].into())
.await
.unwrap();
assert_eq!(response.ids().len(), 5);
for (pos, id) in response.ids().iter().enumerate() {
let script = client
.sieve_script_get(id, None::<Vec<_>>)
.await
.unwrap()
.unwrap();
assert_eq!(script.name().unwrap(), format!("script_{}", pos + 1));
assert_eq!(
String::from_utf8(client.download(script.blob_id().unwrap()).await.unwrap()).unwrap(),
format!("require \"fileinto\"; fileinto \"{}\";", pos + 1)
);
}
// Activate last script twice and then the first script
for _ in 0..2 {
client
.sieve_script_activate(script_ids.last().unwrap())
.await
.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
vec![script_ids.last().unwrap().to_string()]
);
}
client
.sieve_script_activate(script_ids.first().unwrap())
.await
.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
vec![script_ids.first().unwrap().to_string()]
);
// Destroying an active script should not work
assert!(matches!(
client
.sieve_script_destroy(script_ids.first().unwrap())
.await,
Err(Error::Set(SetError {
type_: SetErrorType::ScriptIsActive,
..
}))
));
// Deactivate all scripts
client.sieve_script_deactivate().await.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
Vec::<String>::new()
);
// Connect to LMTP service
let mut lmtp = SmtpConnection::connect().await;
// Run mailbox, fileinto, flags tests
client
.sieve_script_create("test_mailbox", get_script("test_mailbox"), true)
.await
.unwrap();
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
// Make sure all folders were created
let mailbox_names = "My/Nested/Mailbox/with/multiple/levels/Folder"
.split('/')
.collect::<Vec<_>>();
let mut mailbox_ids = Vec::new();
for &mailbox in &mailbox_names {
let mut response = client
.mailbox_query(mailbox::query::Filter::name(mailbox).into(), None::<Vec<_>>)
.await
.unwrap();
assert!(
!response.ids().is_empty(),
"Mailbox {} was not created.",
mailbox
);
mailbox_ids.extend(response.take_ids());
}
assert_eq!(mailbox_ids.len(), mailbox_names.len());
// Make sure the message was delivered to the right folders
let message_ids = client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.take_ids();
assert_eq!(message_ids.len(), 1, "too many messages {:?}", message_ids);
let email = client
.email_get(
message_ids.last().unwrap(),
[email::Property::MailboxIds, email::Property::Keywords].into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(
email.keywords().len(),
2,
"Expected 2 keywords, found {:?}.",
email.keywords()
);
for keyword in ["$important", "$seen"] {
if !email.keywords().contains(&keyword) {
panic!("Keyword {} not found in {:?}.", keyword, email.keywords());
}
}
assert_eq!(
email.mailbox_ids().len(),
2,
"Expected 2 mailbox ids, found {:?}.",
email.mailbox_ids()
);
for mailbox_pos in [mailbox_ids.len() - 1, mailbox_ids.len() - 2] {
if !email
.mailbox_ids()
.contains(&mailbox_ids[mailbox_pos].as_str())
{
panic!(
"Mailbox {} ({}) not found in {:?}.",
mailbox_names[mailbox_pos],
mailbox_ids[mailbox_pos],
email.keywords()
);
}
}
// Run discard and duplicate tests
client
.sieve_script_create(
"test_discard_reject",
get_script("test_discard_reject"),
true,
)
.await
.unwrap();
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <1234@example.com>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your TPS reports before ",
"going on holidays."
),
)
.await;
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Discard failed."
);
// Let one sec duplicate ids expire
tokio::time::sleep(Duration::from_millis(1100)).await;
// Run reject and duplicate check tests
let test = "fd";
/*lmtp.ingest_with_code(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <1234@example.com>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
5,
)
.await
.assert_contains("No soup for you");
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Reject failed."
);*/
// Run include tests
client
.sieve_script_create("test_include_this", get_script("test_include_this"), false)
.await
.unwrap();
client
.sieve_script_create("test_include", get_script("test_include"), true)
.await
.unwrap();
lmtp.ingest_with_code(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <1234@example.com>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
5,
)
.await
.assert_contains("Rejected from an included script");
// Start mock SMTP server
let coco = "fd";
/*let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
// Run enclose + redirect tests
client
.sieve_script_create(
"test_redirect_enclose",
get_script("test_redirect_enclose"),
true,
)
.await
.unwrap();
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com>",
["<jane@example.com>"],
"@Attached you'll find",
),
false,
)
.await;
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Redirected message was stored."
);
// Run notify + editheader + notify + fcc tests
client
.sieve_script_create("test_notify_fcc", get_script("test_notify_fcc"), true)
.await
.unwrap();
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: Urgently I need those TPS Reports\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com>",
["<sms_gateway@example.com>"],
"@It's TPS-o-clock",
),
false,
)
.await;
let mut request = client.build();
request.get_email().properties([
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::Subject,
]);
let emails = request.send_get_email().await.unwrap().take_list();
assert_eq!(
emails.len(),
3,
"Two new messages were expected: {:#?}.",
emails
);
'outer: for (subject, folder, keywords) in [
("It's TPS-o-clock", "Notifications", ""),
(
"Urgently I need those **censored** Reports",
"Inbox",
"$seen",
),
] {
for email in &emails {
if email.subject().unwrap().eq(subject) {
if !keywords.is_empty() && !email.keywords().contains(&keywords) {
panic!("Keyword {:?} not found in: {:#?}", keywords, email);
}
let mailbox_id = client
.mailbox_query(
mailbox::query::Filter::name(folder.to_string()).into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap_or_else(|| panic!("Mailbox {:?} not found", folder));
if !email.mailbox_ids().contains(&mailbox_id.as_str()) {
panic!(
"Mailbox {:?} ({}) not found in: {:#?}",
folder, mailbox_id, email
);
}
continue 'outer;
}
}
panic!("Email {:?} not found in: {:#?}", subject, emails);
}
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();
}
server.store.principal_purge().unwrap();
server.store.assert_is_empty();*/
}
fn get_script(name: &str) -> Vec<u8> {
let mut script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
script_path.push("resources");
script_path.push("jmap_sieve");
script_path.push(format!("{}.sieve", name));
fs::read(script_path).unwrap()
}

View File

@@ -0,0 +1,167 @@
use std::sync::Arc;
use chrono::{Duration, Utc};
use jmap::{JMAP, SUPERUSER_ID};
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,
},
};
pub async fn test(server: Arc<JMAP>, 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")
.await
.unwrap()
.take_id();
let account_id = client
.individual_create("jdoe@example.com", "12345", "John Doe")
.await
.unwrap()
.take_id();
// Start mock SMTP server
let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
// Let people know that we'll be down in Kokomo
client
.set_default_account_id(&account_id)
.vacation_response_create(
"Off the Florida Keys there's a place called Kokomo",
"That's where you wanna go to get away from it all".into(),
"That's where <b>you wanna go</b> to get away from it all".into(),
)
.await
.unwrap();
// Connect to LMTP service
let mut lmtp = SmtpConnection::connect().await;
// Send a message
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
// Await vacation response
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<jdoe@example.com>", ["<bill@example.com>"], "@Kokomo"),
false,
)
.await;
// Further messages from the same recipient should not
// trigger a vacation response
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: TPS Report -- friendly reminder\r\n",
"\r\n",
"Listen, are you gonna have those TPS reports for us this afternoon?",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
// Messages from MAILER-DAEMON should not
// trigger a vacation response
lmtp.ingest(
"MAILER-DAEMON@example.com",
&["jdoe@example.com"],
concat!(
"From: MAILER-DAEMON@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: Delivery Failure\r\n",
"\r\n",
"I tried so hard and got so far but in the end it wasn't delivered.",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
// Vacation responses should honor the configured date ranges
client
.vacation_response_set_dates((Utc::now() + Duration::days(1)).timestamp().into(), None)
.await
.unwrap();
lmtp.ingest(
"jane_smith@example.com",
&["jdoe@example.com"],
concat!(
"From: jane_smith@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: When were you going on holidays?\r\n",
"\r\n",
"I'm asking because Bill really wants those TPS reports.",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
client
.vacation_response_set_dates((Utc::now() - Duration::days(1)).timestamp().into(), None)
.await
.unwrap();
smtp_settings.lock().do_stop = true;
lmtp.ingest(
"jane_smith@example.com",
&["jdoe@example.com"],
concat!(
"From: jane_smith@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: When were you going on holidays?\r\n",
"\r\n",
"I'm asking because Bill really wants those TPS reports.",
),
)
.await;
lmtp.quit().await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<jdoe@example.com>",
["<jane_smith@example.com>"],
"@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();
*/
}