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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+391
View File
@@ -0,0 +1,391 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type, append::assert_append_message};
use crate::utils::{server::TestServer, smtp::SmtpConnection};
use imap_proto::ResponseType;
pub async fn test(
mut imap_john: &mut ImapConnection,
_imap_check: &mut ImapConnection,
test: &TestServer,
) {
// Delivery to support account
println!("Running ACL tests...");
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\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;
// Connect to all test accounts
let mut imap_jane = test.account("[email protected]").imap_client().await;
let mut imap_bill = test.account("[email protected]").imap_client().await;
// Jane should see the Support account
imap_jane.send("LIST \"\" \"*\"").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("Shared Folders/[email protected]/INBOX");
imap_jane
.send("SELECT \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("FETCH 1 (PREVIEW)").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("TPS reports ASAP");
imap_jane.send("UNSELECT").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
// Jane should be able to create folders under the Support account
imap_jane
.send("CREATE \"Shared Folders/[email protected]/inbox/Jane's Folder\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("LIST \"\" \"*\"").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals(
"* LIST () \"/\" \"Shared Folders/[email protected]/INBOX/Jane's Folder\"",
);
imap_jane
.send("DELETE \"Shared Folders/[email protected]/INBOX/Jane's Folder\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
// John should have no shared folders
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 0);
imap_john.send("NAMESPACE").await;
imap_john.assert_read(Type::Tagged, ResponseType::Ok).await;
// List rights
imap_jane.send("LISTRIGHTS INBOX [email protected]").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* LISTRIGHTS \"INBOX\" \"[email protected]\" r l ws i et k x p a");
// Jane shares her Inbox to John, expect a Shared Folders item in John's list
imap_jane.send("SETACL INBOX [email protected] lr").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders\"")
.assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders/[email protected]\"")
.assert_equals("* LIST () \"/\" \"Shared Folders/[email protected]/INBOX\"");
// Grant access to Bill and check ACLs
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl");
imap_jane
.send("SETACL INBOX [email protected] lrxtws")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl")
.assert_contains("\"[email protected]\" tewsrxl");
imap_bill.send("LIST \"\" \"*\"").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("Shared Folders/[email protected]/INBOX");
// Namespace should now return the Shared Folders namespace
imap_john.send("NAMESPACE").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* NAMESPACE ((\"\" \"/\")) ((\"Shared Folders\" \"/\")) NIL");
// List John's right on Jane's Inbox
imap_john
.send("MYRIGHTS \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* MYRIGHTS \"Shared Folders/[email protected]/INBOX\" rl");
// John should not be able to append messages
assert_append_message(
imap_john,
"Shared Folders/[email protected]/INBOX",
"From: john\n\ncontents",
ResponseType::No,
)
.await;
// Grant insert access to John on Jane's Inbox, and try inserting the
// message again.
imap_jane.send("SETACL INBOX [email protected] +i").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john
.send("MYRIGHTS \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* MYRIGHTS \"Shared Folders/[email protected]/INBOX\" rli");
assert_append_message(
imap_john,
"Shared Folders/[email protected]/INBOX",
"From: john\n\ncontents",
ResponseType::Ok,
)
.await;
// Only Bill should be allowed to delete messages on Jane's Inbox
for imap in [&mut imap_john, &mut imap_bill] {
imap.send("SELECT \"Shared Folders/[email protected]/INBOX\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
}
imap_john.send("UID STORE 1 +FLAGS (\\Deleted)").await;
imap_john.assert_read(Type::Tagged, ResponseType::No).await;
imap_bill.send("UID STORE 1 +FLAGS (\\Deleted)").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john.send("UID EXPUNGE").await;
imap_john.assert_read(Type::Tagged, ResponseType::No).await;
imap_john.send("UID FETCH 1 (PREVIEW)").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("contents");
imap_bill.send("UID EXPUNGE").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill.send("UID FETCH 1 (PREVIEW)").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("contents", 0);
imap_bill
.send("STATUS \"Shared Folders/[email protected]/INBOX\" (MESSAGES)")
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(MESSAGES 0)");
// Test copying and moving between shared mailboxes
let uid = assert_append_message(
imap_john,
"INBOX",
"From: john\n\ncopy test",
ResponseType::Ok,
)
.await
.into_append_uid();
imap_john.send("SELECT INBOX").await;
imap_john.assert_read(Type::Tagged, ResponseType::Ok).await;
// Copy from John's Inbox to Jane's Inbox
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid
))
.await;
let uid = imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
// Check that both Bill and Jane can see the message
imap_bill.send("NOOP").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
imap_jane.send("SELECT INBOX").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
// Bill now moves the message to his own Inbox
imap_bill.send(&format!("UID MOVE {} INBOX", uid)).await;
let uid_moved = imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
// Both Jane and Bill should not see the message on Jane's Inbox anymore
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("copy test", 0);
imap_jane
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("copy test", 0);
// Check that the message has been moved to Bill's Inbox
imap_bill.send("SELECT INBOX").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid_moved))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
// Repeating a cross-account copy returns the existing destination UID
// instead of creating a duplicate, and moving a message that the destination
// account already holds still expunges the source.
let uid_dedup = assert_append_message(
imap_john,
"INBOX",
concat!(
"Message-ID: <[email protected]>\n",
"From: john\n",
"Subject: dedup\n",
"\n",
"dedup test"
),
ResponseType::Ok,
)
.await
.into_append_uid();
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
let uid_dedup_dest = imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
assert_eq!(
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid(),
uid_dedup_dest
);
imap_jane.send("NOOP").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("UID FETCH 1:* (PREVIEW)").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("dedup test", 1);
imap_john
.send(&format!(
"UID MOVE {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
assert_eq!(
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid(),
uid_dedup_dest
);
imap_john
.send(&format!("UID FETCH {} (PREVIEW)", uid_dedup))
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("dedup test", 0);
// Jane stops sharing with Bill, and removes Insert access to John
imap_jane.send("DELETEACL INBOX [email protected]").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("SETACL INBOX [email protected] -i").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl")
.assert_count("[email protected]", 0);
// Bill should not have access to Jane's Inbox anymore
imap_bill.send("LIST \"\" \"*\"").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 0);
// And John should still have access
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 3);
}
+151
View File
@@ -0,0 +1,151 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
imap::Type,
system::antispam::{HAM, SPAM, TEST},
utils::{imap::AssertResult, server::TestServer, smtp::SmtpConnection},
};
use common::{Server, manager::SPAM_TRAINER_KEY};
use imap_proto::ResponseType;
use registry::schema::{
enums::TaskSpamFilterMaintenanceType,
prelude::ObjectType,
structs::{Task, TaskSpamFilterMaintenance, TaskStatus},
};
use spam_filter::modules::classifier::SpamTrainer;
use store::{
Deserialize,
write::{AlignedBytes, Archive},
};
pub async fn test(test: &TestServer) {
println!("Running Spam classifier tests...");
let admin = test.account("[email protected]");
let account = test.account("[email protected]");
let mut imap = account.imap_client().await;
let account_id = account.id();
// Make sure there are no training samples
admin
.registry_destroy_all(ObjectType::SpamTrainingSample)
.await;
assert_eq!(admin.spam_training_samples().await, vec![]);
// Train the classifier via APPEND
imap.append("INBOX", HAM[0]).await;
imap.append("Junk Mail", SPAM[0]).await;
let samples = account.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 1);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 1);
// Append two spam samples to "Drafts", then train the classifier via STORE and MOVE
imap.append("Drafts", SPAM[1]).await;
imap.append("Drafts", SPAM[2]).await;
imap.send_ok("SELECT Drafts").await;
imap.send_ok("STORE 1 +FLAGS ($Junk)").await;
imap.send_ok("MOVE 2 \"Junk Mail\"").await;
let samples = account.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 1);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 3);
// Add the remaining messages via APPEND
for message in HAM.iter().skip(1) {
imap.append("INBOX", message).await;
}
for message in SPAM.iter().skip(3) {
imap.append("Junk Mail", message).await;
}
let samples = account.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 10);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10);
assert_eq!(samples.len(), 20);
assert!(samples.iter().all(
|(_, s)| s.blob_id.class.account_id() == account_id.document_id() && !s.delete_after_use
));
// Train the classifier
admin
.registry_create_object(Task::SpamFilterMaintenance(TaskSpamFilterMaintenance {
maintenance_type: TaskSpamFilterMaintenanceType::Train,
status: TaskStatus::now(),
}))
.await;
test.wait_for_tasks().await;
let model = spam_classifier_model(&test.server).await;
assert_eq!(model.reservoir.ham.total_seen, 10);
assert_eq!(model.reservoir.spam.total_seen, 10);
assert_eq!(
model.last_id,
samples.iter().map(|(id, _)| id.id()).max().unwrap()
);
assert_eq!(account.spam_training_samples().await.len(), 20);
assert!(test.server.inner.data.spam_classifier.load().is_active());
// Send 3 test emails
for message in TEST {
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest("[email protected]", &["[email protected]"], message)
.await;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
imap.send_ok("SELECT INBOX").await;
imap.send("FETCH 11 (FLAGS RFC822.TEXT)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("FLAGS ($Junk")
.assert_contains("Subject: can someone explain")
.assert_contains("X-Spam-Status: No")
.assert_contains("PROB_HAM_HIGH");
imap.send("FETCH 12 (FLAGS RFC822.TEXT)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("FLAGS ($Junk")
.assert_contains("Subject: classifier test")
.assert_contains("X-Spam-Status: No")
.assert_contains_any(&["PROB_SPAM_UNCERTAIN", "PROB_HAM_LOW"]);
imap.send_ok("SELECT \"Junk Mail\"").await;
imap.send("FETCH 10 (FLAGS RFC822.TEXT)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FLAGS ($Junk")
.assert_contains("Subject: save up to")
.assert_contains("X-Spam-Status: Yes")
.assert_contains("PROB_SPAM_HIGH");
imap.send_ok("MOVE 10 INBOX").await;
let samples = account.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10);
// Make sure spam traps trigger spam classification
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest("[email protected]", &["[email protected]"], SPAM[4])
.await;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
let samples = admin.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 11);
// Global spam samples should not appear in the account
let samples = account.spam_training_samples().await;
assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11);
assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10);
}
pub async fn spam_classifier_model(server: &Server) -> SpamTrainer {
server
.blob_store()
.get_blob(SPAM_TRAINER_KEY, 0..usize::MAX)
.await
.and_then(|archive| match archive {
Some(archive) => <Archive<AlignedBytes> as Deserialize>::deserialize(&archive)
.and_then(|archive| archive.deserialize_untrusted::<SpamTrainer>())
.map(Some),
None => Ok(None),
})
.unwrap()
.unwrap()
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type, resources_dir};
use crate::utils::server::TestServer;
use imap_proto::ResponseType;
use std::{fs, io};
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) {
println!("Running APPEND tests...");
// Invalid APPEND commands
imap.send("APPEND \"Does not exist\" {1+}\r\na").await;
imap.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_response_code("TRYCREATE");
// Import test messages
let mut entries = fs::read_dir(resources_dir())
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()
.unwrap();
entries.sort();
let mut expected_uid = 1;
for file_name in entries.into_iter().take(20) {
if file_name.extension().is_none_or(|e| e != "txt") {
continue;
}
let raw_message = fs::read(&file_name).unwrap();
imap.send(&format!(
"APPEND INBOX (Flag_{}) {{{}}}",
file_name
.file_name()
.unwrap()
.to_str()
.unwrap()
.split_once('.')
.unwrap()
.0,
raw_message.len()
))
.await;
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(std::str::from_utf8(&raw_message).unwrap())
.await;
let result = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_response_code();
let mut code = result.split(' ');
assert_eq!(code.next(), Some("APPENDUID"));
assert_ne!(code.next(), Some("0"));
assert_eq!(code.next(), Some(expected_uid.to_string().as_str()));
expected_uid += 1;
}
test.wait_for_tasks().await;
}
pub async fn assert_append_message(
imap: &mut ImapConnection,
folder: &str,
message: &str,
expected_response: ResponseType,
) -> Vec<String> {
imap.send(&format!("APPEND \"{}\" {{{}}}", folder, message.len()))
.await;
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(message).await;
imap.assert_read(Type::Tagged, expected_response).await
}
fn build_message(message: usize, in_reply_to: Option<usize>, thread_num: usize) -> String {
if let Some(in_reply_to) = in_reply_to {
format!(
"Message-ID: <{}@domain>\nReferences: <{}@domain>\nSubject: re: T{}\n\nreply\n",
message, in_reply_to, thread_num
)
} else {
format!(
"Message-ID: <{}@domain>\nSubject: T{}\n\nmsg\n",
message, thread_num
)
}
}
pub fn build_messages() -> Vec<String> {
let mut messages = Vec::new();
for parent in 0..3 {
messages.push(build_message(parent, None, parent));
for child in 0..3 {
messages.push(build_message(
((parent + 1) * 10) + child,
parent.into(),
parent,
));
}
}
messages
}
+59
View File
@@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use directory::Credentials;
use imap_proto::ResponseType;
use mail_parser::decoders::base64::base64_decode;
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
println!("Running basic tests...");
// Test OAuth Bearer decoding
assert_eq!(
Credentials::Bearer {
token: "vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==".to_string(),
username: Some("[email protected]".to_string()),
},
Credentials::decode_sasl_challenge_oauth(
&base64_decode(
concat!(
"bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv",
"c3Q9c2VydmVyLmV4YW1wbGUuY29tAXBvcnQ9MTQzAWF1dGg9QmVhcmVyI",
"HZGOWRmdDRxbVRjMk52YjNSbGNrQmhiSFJoZG1semRHRXVZMjl0Q2c9PQ",
"EB"
)
.as_bytes(),
)
.unwrap(),
)
.unwrap()
);
// Test CAPABILITY
imap.send("CAPABILITY").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Test NOOP
imap.send("NOOP").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Test ID
imap.send("ID").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* ID (\"name\" \"Stalwart\" \"version\" ");
// Login should be disabled
imap.send("LOGIN [email protected] secret").await;
imap.assert_read(Type::Tagged, ResponseType::No).await;
// Try logging in with wrong password
imap.send("AUTHENTICATE PLAIN {24}").await;
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged("AGJvYXR5AG1jYm9hdGZhY2U=").await;
imap.assert_read(Type::Tagged, ResponseType::No).await;
}
+242
View File
@@ -0,0 +1,242 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::resources_dir;
use email::message::metadata::{MessageMetadata, build_metadata_contents};
use imap::op::fetch::AsImapDataItem;
use imap_proto::{
ResponseCode, StatusResponse,
protocol::fetch::{BodyContents, DataItem, Section},
};
use mail_parser::MessageParser;
use std::fs;
use store::{
Deserialize, Serialize,
write::{Archive, Archiver},
};
use utils::chained_bytes::ChainedBytes;
pub fn test() {
println!("Running BODYSTRUCTURE...");
for file_name in fs::read_dir(resources_dir()).unwrap() {
let mut file_name = file_name.as_ref().unwrap().path();
if file_name.extension().is_none_or(|e| e != "txt") {
continue;
}
let mut buf = Vec::new();
let raw_message = fs::read(&file_name).unwrap();
let message_ = MessageParser::new().parse(&raw_message).unwrap();
let metadata = MessageMetadata {
preview: Default::default(),
raw_headers: message_
.raw_message
.as_ref()
.get(
message_.root_part().offset_header as usize
..message_.root_part().offset_body as usize,
)
.unwrap_or_default()
.into(),
blob_hash: Default::default(),
blob_body_offset: message_.root_part().offset_body as u32,
contents: build_metadata_contents(message_),
rcvd_attach: 0,
};
let metadata_ =
Archive::deserialize_owned(Archiver::new(metadata).serialize().unwrap()).unwrap();
let metadata = metadata_.unarchive::<MessageMetadata>().unwrap();
let raw_message = ChainedBytes::new(metadata.raw_headers.as_ref()).with_last(
raw_message
.get(metadata.blob_body_offset.to_native() as usize..)
.unwrap_or_default(),
);
let decoded = metadata.decode_contents(raw_message);
// Serialize body and bodystructure
for (is_extended, is_utf8) in [(false, false), (true, false), (true, true)] {
let mut buf_ = Vec::new();
metadata.body_structure(&decoded, is_extended).serialize(
&mut buf_,
is_extended,
is_utf8,
);
if !is_extended {
buf.extend_from_slice(b"BODY ");
} else if !is_utf8 {
buf.extend_from_slice(b"BODYSTRUCTURE ");
} else {
buf.extend_from_slice(b"BODYSTRUCTURE UTF8=ACCEPT ");
}
// Poor man's indentation
let mut indent_count = 0;
let mut in_quote = false;
for ch in buf_ {
if ch == b'(' && !in_quote {
buf.extend_from_slice(b"(\n");
indent_count += 1;
for _ in 0..indent_count {
buf.extend_from_slice(b" ");
}
} else if ch == b')' && !in_quote {
buf.push(b'\n');
indent_count -= 1;
for _ in 0..indent_count {
buf.extend_from_slice(b" ");
}
buf.push(b')');
} else {
if ch == b'"' {
in_quote = !in_quote;
}
buf.push(ch);
}
}
buf.extend_from_slice(b"\n\n");
}
// Serialize body parts
let mut iter = 1..9;
let mut stack = Vec::new();
let mut sections = Vec::new();
loop {
'inner: while let Some(part_id) = iter.next() {
if part_id == 1 {
for section in [
None,
Some(Section::Header),
Some(Section::Text),
Some(Section::Mime),
] {
let mut body_sections = sections
.iter()
.map(|id| Section::Part { num: *id })
.collect::<Vec<_>>();
let is_first = if let Some(section) = section {
body_sections.push(section);
false
} else {
true
};
if let Some(contents) =
metadata.body_section(&decoded, &body_sections, None)
{
DataItem::BodySection {
sections: body_sections,
origin_octet: None,
contents,
}
.serialize(&mut buf, false);
if is_first {
match metadata.binary(&decoded, &sections, None) {
Ok(Some(contents)) => {
buf.push(b'\n');
DataItem::Binary {
sections: sections.clone(),
offset: None,
contents: match contents {
BodyContents::Bytes(bytes) => BodyContents::Text(
std::str::from_utf8(bytes.as_ref())
.unwrap_or("[binary content]")
.to_string()
.into(),
),
text => text,
},
}
.serialize(&mut buf, false);
}
Ok(None) => (),
Err(_) => {
buf.push(b'\n');
buf.extend_from_slice(
&StatusResponse::no(format!(
"Failed to decode part {} of message {}.",
sections
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join("."),
0
))
.with_code(ResponseCode::UnknownCte)
.serialize(Vec::new()),
);
}
}
if let Some(size) = metadata.binary_size(&decoded, &sections) {
buf.push(b'\n');
DataItem::BinarySize {
sections: sections.clone(),
size,
}
.serialize(&mut buf, false);
}
}
buf.extend_from_slice(b"\n----------------------------------\n");
} else {
break 'inner;
}
}
}
sections.push(part_id);
stack.push(iter);
iter = 1..9;
}
if let Some(prev_iter) = stack.pop() {
sections.pop();
iter = prev_iter;
} else {
break;
}
}
// Check header fields and partial sections
for sections in [
vec![Section::HeaderFields {
not: false,
fields: vec!["From".into(), "To".into()],
}],
vec![Section::HeaderFields {
not: true,
fields: vec!["Subject".into(), "Cc".into()],
}],
] {
DataItem::BodySection {
contents: metadata.body_section(&decoded, &sections, None).unwrap(),
sections: sections.clone(),
origin_octet: None,
}
.serialize(&mut buf, false);
buf.extend_from_slice(b"\n----------------------------------\n");
DataItem::BodySection {
contents: metadata
.body_section(&decoded, &sections, (10, 25).into())
.unwrap(),
sections,
origin_octet: 10.into(),
}
.serialize(&mut buf, false);
buf.extend_from_slice(b"\n----------------------------------\n");
}
file_name.set_extension("imap");
let expected_result = fs::read(&file_name).unwrap();
if buf != expected_result {
file_name.set_extension("imap_failed");
fs::write(&file_name, buf).unwrap();
panic!("Failed test, written output to {}", file_name.display());
}
}
}
+280
View File
@@ -0,0 +1,280 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use imap_proto::ResponseType;
use crate::imap::{
AssertResult,
append::{assert_append_message, build_messages},
};
use super::{ImapConnection, Type};
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
println!("Running CONDSTORE...");
// Test CONDSTORE parameter
imap.send("SELECT INBOX (CONDSTORE)").await;
let hms = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq();
// Unselect
imap.send("UNSELECT").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Create test folders
imap.send("CREATE Pecorino").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Enable CONDSTORE and QRESYNC
imap.send("ENABLE CONDSTORE QRESYNC").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Make sure modseq did not change after creating a mailbox
imap.send("SELECT Pecorino").await;
assert_eq!(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
hms
);
imap_check.send("LIST \"\" \"*\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("SELECT Pecorino (CONDSTORE)").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
// SEQ 0: Init
let mut messages = build_messages();
let mut modseqs = vec![hms];
// SEQ 1: Append a message and make sure the modseq increased
assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await;
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
assert_ne!(modseqs[modseqs.len() - 1], modseqs[modseqs.len() - 2]);
// SEQ 2: Move out the message and make sure the modseq increased
imap.send("UID MOVE 1 \"Deleted Items\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* VANISHED 1");
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
assert_ne!(modseqs[modseqs.len() - 1], modseqs[modseqs.len() - 2]);
// SEQ 3: Insert message
assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await;
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
// SEQ 4: Insert message
assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await;
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
// SEQ 5: Insert message
assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await;
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
// SEQ 6: Change a message flag
imap.send("UID STORE 4 +FLAGS.SILENT (\\Answered)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_modseq(),
);
// SEQ 7: Insert message
assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await;
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
// SEQ 8: Delete a message
imap.send("UID STORE 2 +FLAGS.SILENT (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("VANISHED 2")
.assert_contains("* 3 EXISTS");
imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await;
modseqs.push(
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_highest_modseq(),
);
// Fetch changes since SEQ 0
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[0]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FETCH (", 3)
.assert_count("VANISHED", 0);
// Fetch changes since SEQ 1, UID MOVE should count as a deletion
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[1]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 1)
.assert_contains("VANISHED (EARLIER) 1")
.assert_count("FETCH (", 3);
// Fetch changes since SEQ 3
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[3]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 1)
.assert_contains("VANISHED (EARLIER) 2")
.assert_count("FETCH (", 3);
// Fetch changes since SEQ 4
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[4]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 1)
.assert_contains("VANISHED (EARLIER) 2")
.assert_count("FETCH (", 2);
// Fetch changes since SEQ 6
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[6]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 1)
.assert_contains("VANISHED (EARLIER) 2")
.assert_count("FETCH (", 1);
// Fetch changes since SEQ 7
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[7]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 1)
.assert_contains("VANISHED (EARLIER) 2")
.assert_count("FETCH (", 0);
// Fetch changes since SEQ 8
imap.send(&format!(
"UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)",
modseqs[8]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("VANISHED", 0)
.assert_count("FETCH (", 0);
// Search since MODSEQ
imap.send(&format!("SEARCH RETURN (ALL) MODSEQ {}", modseqs[3]))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("ALL 1:3 MODSEQ");
imap_check.send("NOOP").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("3 EXISTS");
imap_check
.send(&format!("SEARCH MODSEQ {}", modseqs[4]))
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("SEARCH 2 3 (MODSEQ");
// Store unchanged since
imap.send(&format!(
"UID STORE 2:5 (UNCHANGEDSINCE {}) +FLAGS.SILENT (\\Junk)",
modseqs[5]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 1 FETCH")
.assert_contains("(UID 3 MODSEQ")
.assert_count("FETCH (", 1)
.assert_contains("[MODIFIED 2,4:5]");
imap.send(&format!(
"UID STORE 4,5 (UNCHANGEDSINCE {}) -FLAGS.SILENT (\\Answered)",
modseqs[6]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 2 FETCH")
.assert_contains("(UID 4 MODSEQ")
.assert_count("FETCH (", 1)
.assert_contains("[MODIFIED 5]");
// QResync
imap.send("STATUS Pecorino (UIDVALIDITY)").await;
let uid_validity = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_uid_validity();
imap.send(&format!(
"SELECT Pecorino (QRESYNC ({} {} 1:5)) ",
uid_validity, modseqs[6]
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FETCH (", 3)
.assert_contains("VANISHED (EARLIER) 2");
}
+177
View File
@@ -0,0 +1,177 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
println!("Running COPY/MOVE tests...");
// Check status
imap_check
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE RECENT))")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 RECENT 0 SIZE 12193)");
// Select INBOX
imap_check.send("SELECT INBOX").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
// Copying to the same mailbox should fail
imap_check.send("COPY 1:* INBOX").await;
imap_check
.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_response_code("CANNOT");
// Copying to a non-existent mailbox should fail
imap_check.send("COPY 1:* \"/dev/null\"").await;
imap_check
.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_response_code("TRYCREATE");
// Create test folders
imap_check.send("CREATE \"Scamorza Affumicata\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("CREATE \"Burrata al Tartufo\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
// Copy messages
imap_check
.send("COPY 1,3,5,7 \"Scamorza Affumicata\"")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("COPYUID")
.assert_contains("1:4");
// Check status
imap_check
.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MESSAGES 4")
//.assert_contains("RECENT 4")
.assert_contains("UNSEEN 4")
.assert_contains("UIDNEXT 5")
.assert_contains("SIZE 5851");
// Check \Recent flag
/*imap_check.send("SELECT \"Scamorza Affumicata\"").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 4 RECENT");
imap_check.send("FETCH 1:* (UID FLAGS)").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("\\Recent", 4);
imap_check.send("UNSELECT").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check
.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MESSAGES 4")
.assert_contains("RECENT 0")
.assert_contains("UNSEEN 4")
.assert_contains("UIDNEXT 5")
.assert_contains("SIZE 5851");
imap_check.send("SELECT \"Scamorza Affumicata\"").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 0 RECENT");
imap_check.send("FETCH 1:* (UID FLAGS)").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("\\Recent", 0);*/
// Move all messages to Burrata
imap_check.send("SELECT \"Scamorza Affumicata\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("MOVE 1:* \"Burrata al Tartufo\"").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* OK [COPYUID")
.assert_contains("1:4")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE");
// Check status
imap_check
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 4 UNSEEN 4 SIZE 5851)")
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)");
// Move the messages back to Scamorza, UIDNEXT should increase.
imap_check.send("SELECT \"Burrata al Tartufo\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("MOVE 1:* \"Scamorza Affumicata\"").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* OK [COPYUID")
.assert_contains("5:8")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 1 EXPUNGE");
// Check status
imap_check
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 9 MESSAGES 4 UNSEEN 4 SIZE 5851)")
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)");
imap_check.send("SELECT \"Burrata al Tartufo\"").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("SELECT \"Scamorza Affumicata\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID MOVE 5 \"Burrata al Tartufo\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("COPYUID");
imap_check.send("UID FETCH 1:* (UID)").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("UID 5");
imap.send("SELECT \"Burrata al Tartufo\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID MOVE 5 \"Scamorza Affumicata\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("COPYUID");
}
+183
View File
@@ -0,0 +1,183 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
println!("Running FETCH tests...");
// Examine INBOX
imap.send("EXAMINE INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("10 EXISTS")
.assert_contains("[UIDNEXT 11]");
// Fetch all properties available from JMAP
imap.send(concat!(
"FETCH 10 (FLAGS INTERNALDATE PREVIEW OBJECTID ",
"RFC822.SIZE UID ENVELOPE BODYSTRUCTURE)"
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FLAGS (Flag_009)")
.assert_contains("RFC822.SIZE 1457")
.assert_contains("UID 10")
.assert_contains("INTERNALDATE")
.assert_contains("OBJECTID (")
.assert_contains("EMAILID ")
.assert_contains("THREADID ")
.assert_contains("but then I thought, why not do both?")
.assert_contains(concat!(
"ENVELOPE (\"Sat, 20 Nov 2021 14:22:01 -0800\" ",
"\"Why not both importing AND exporting? ☺\" ",
"((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ",
"((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ",
"((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ",
"((NIL NIL \"Colleagues\" NIL)",
"(\"James Smythe\" NIL \"james\" \"vandelay.com\")",
"(NIL NIL NIL NIL)(NIL NIL \"Friends\" NIL)",
"(NIL NIL \"jane\" \"example.com\")",
"(\"John Smîth\" NIL \"john\" \"example.com\")",
"(NIL NIL NIL NIL)) NIL NIL NIL NIL)"
))
.assert_contains(concat!(
"BODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL ",
"\"base64\" 239 3 \"07aab44e51c5f1833a5d19f2e1804c4b\" NIL NIL NIL)",
"(\"message\" \"rfc822\" NIL NIL NIL NIL 723 ",
"(NIL \"Exporting my book about coffee tables\" ",
"((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ",
"((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ",
"((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ",
"NIL NIL NIL NIL NIL) ",
"((\"text\" \"plain\" (\"charset\" \"utf-16\") NIL NIL ",
"\"quoted-printable\" 228 3 \"3a942a99cdd8a099ae107d3867ec20fb\" NIL NIL NIL)",
"(\"image\" \"gif\" (\"name\" \"Book about ☕ tables.gif\") ",
"NIL NIL \"Base64\" 56 \"d40fa7f401e9dc2df56cbb740d65ff52\" ",
"(\"attachment\" NIL) NIL NIL) \"mixed\" (\"boundary\" \"giddyup\") NIL NIL NIL)",
" 0 \"cdb0382a03a15601fb1b3c7422521620\" NIL NIL NIL) ",
"\"mixed\" (\"boundary\" \"festivus\") NIL NIL NIL)"
));
imap_check.send("EXAMINE INBOX").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("FETCH 10 (ENVELOPE BODYSTRUCTURE)").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(
"\"=?utf-8?B?V2h5IG5vdCBib3RoIGltcG9ydGluZyBBTkQgZXhwb3J0aW5nPyDimLo=?=\" ",
)
.assert_contains("(\"=?utf-8?B?Sm9obiBTbcOudGg=?=\" NIL \"john\" \"example.com\")")
.assert_contains("(\"name\" \"=?utf-8?B?Qm9vayBhYm91dCDimJUgdGFibGVzLmdpZg==?=\")");
// Fetch bodyparts
imap.send(concat!(
"UID FETCH 10 (BINARY[1] BINARY.SIZE[1] BODY[1.TEXT] BODY[2.1.HEADER] ",
"BINARY[2.1] BODY[MIME] BODY[HEADER.FIELDS (From)]<10.8>)"
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("BINARY[1] ~{175}")
.assert_contains("BINARY.SIZE[1] 175")
.assert_contains("BODY[1.TEXT] {239}")
.assert_contains("BODY[2.1.HEADER] {88}")
.assert_contains("BINARY[2.1] ~{108}")
.assert_contains("BODY[MIME] {54}")
.assert_contains("BODY[HEADER.FIELDS (FROM)]<10> {8}")
.assert_contains("&ldquo;exporting&rdquo;")
.assert_contains("PGh0bWw+PHA+")
.assert_contains("Content-Transfer-Encoding: quoted-printable")
.assert_contains("Vandelay");
let fraktur_utf16_le: Vec<u8> = "ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨"
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
imap.assert_last_contains_bytes(&fraktur_utf16_le);
// We are in EXAMINE mode, fetching body should not set \Seen
imap.send("UID FETCH 10 (FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FLAGS (Flag_009)");
// Switch to SELECT mode
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Peek bodyparts
imap.send("UID FETCH 10 (BINARY.PEEK[1] BINARY.SIZE[1] BODY.PEEK[1.TEXT])")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("BINARY[1] ~{175}")
.assert_contains("BINARY.SIZE[1] 175")
.assert_contains("BODY[1.TEXT] {239}");
// PEEK was used, \Seen should not be set
imap.send("UID FETCH 10 (FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FLAGS (Flag_009)");
// Fetching a body section should set the \Seen flag
imap.send("UID FETCH 10 (BODY[1.TEXT])").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FLAGS")
.assert_contains("\\Seen");
// Fetch a sequence
imap.send("FETCH 1:5,7:10 (UID FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 1 FETCH (UID 1 ")
.assert_contains("* 2 FETCH (UID 2 ")
.assert_contains("* 3 FETCH (UID 3 ")
.assert_contains("* 4 FETCH (UID 4 ")
.assert_contains("* 5 FETCH (UID 5 ")
.assert_contains("* 7 FETCH (UID 7 ")
.assert_contains("* 8 FETCH (UID 8 ")
.assert_contains("* 9 FETCH (UID 9 ")
.assert_contains("* 10 FETCH (UID 10 ")
.assert_count("\\Recent", 0);
imap.send("FETCH 7:* (UID FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 7 FETCH (UID 7 ")
.assert_contains("* 8 FETCH (UID 8 ")
.assert_contains("* 9 FETCH (UID 9 ")
.assert_contains("* 10 FETCH (UID 10 ");
// Fetch using a saved search
imap.send("UID SEARCH RETURN (SAVE) FROM \"nathaniel\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("FETCH $ (UID PREVIEW)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 1 FETCH (UID 1 ")
.assert_contains("* 4 FETCH (UID 4 ")
.assert_contains("* 6 FETCH (UID 6 ")
.assert_contains("Some text appears here")
.assert_contains("plain text version of message goes here")
.assert_contains("This is implicitly typed plain US-ASCII text.");
// A failing command in a pipelined batch does not swallow the tagged completion of the commands queued behind it
imap.send_raw(concat!(
"_p UID FETCH 1:* (UID) (CHANGEDSINCE 1 VANISHED)\r\n",
"_x FETCH 1 (UID)\r\n"
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("_p BAD")
.assert_contains("* 1 FETCH (UID 1");
}
+200
View File
@@ -0,0 +1,200 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::smtp::SmtpConnection;
use super::{AssertResult, ImapConnection, Type};
use imap_proto::ResponseType;
use std::time::Duration;
const SLEEP: Duration = Duration::from_millis(200);
pub async fn test(
imap: &mut ImapConnection,
imap_check: &mut ImapConnection,
is_cluster_test: bool,
) {
println!("Running IDLE tests...");
// Switch connection to IDLE mode
imap_check.send("CREATE Parmeggiano").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("SELECT Parmeggiano").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("NOOP").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("IDLE").await;
imap_check
.assert_read(Type::Continuation, ResponseType::Ok)
.await;
// Expect a new mailbox update
imap.send("CREATE Provolone").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("LIST () \"/\" \"Provolone\"");
// Insert a message in the new folder and expect an update
let message = "From: [email protected]\nSubject: Test\n\nTest message\n";
imap.send(&format!("APPEND Provolone {{{}}}", message.len()))
.await;
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(message).await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("STATUS \"Provolone\"")
.assert_contains("MESSAGES 1")
.assert_contains("UNSEEN 1")
.assert_contains("UIDNEXT 2");
// Change message to Seen and expect an update
imap.send("SELECT Provolone").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("STORE 1:* +FLAGS (\\Seen)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("STATUS \"Provolone\"")
.assert_contains("MESSAGES 1")
.assert_contains("UNSEEN 0")
.assert_contains("UIDNEXT 2");
// Delete message and expect an update
imap.send("STORE 1:* +FLAGS (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CLOSE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("STATUS \"Provolone\"")
.assert_contains("MESSAGES 0")
.assert_contains("UNSEEN 0")
.assert_contains("UIDNEXT 2");
// Delete folder and expect an update
imap.send("DELETE Provolone").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("LIST (\\NonExistent) \"/\" \"Provolone\"");
// Add a message to Inbox and expect an update
imap.send(&format!("APPEND Parmeggiano {{{}}}", message.len()))
.await;
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(message).await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("MESSAGES 1")
.assert_contains("UNSEEN 1");
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("* 1 EXISTS");
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("* 1 FETCH (FLAGS () UID 1)");
// Delete message and expect an update
imap.send("SELECT Parmeggiano").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("STORE 1 +FLAGS (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("* 1 FETCH (FLAGS (\\Deleted) UID 1)");
imap.send("UID EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 0 EXISTS");
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("MESSAGES 0")
.assert_contains("UNSEEN 0");
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("* 1 EXPUNGE");
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("* 0 EXISTS");
// Test SMTP delivery notifications
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"X-Spam-Status: No\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("STATUS \"INBOX\"")
.assert_contains(if is_cluster_test {
"MESSAGES 1"
} else {
"MESSAGES 11"
});
// Stop IDLE mode
imap_check.send_raw("DONE").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_check.send("NOOP").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
}
+455
View File
@@ -0,0 +1,455 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use imap::op::list::matches_pattern;
use imap_proto::ResponseType;
use crate::utils::server::TestServer;
use super::{AssertResult, ImapConnection, Type};
pub async fn test(
mut imap: &mut ImapConnection,
mut imap_check: &mut ImapConnection,
test: &TestServer,
) {
println!("Running mailbox tests...");
// Pattern matching tests
mailbox_matches_pattern();
// Create third connection for testing
let mut other_conn = test.account("[email protected]").imap_client().await;
// List folders
imap.send("LIST \"\" \"*\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders([("INBOX", [""]), ("Deleted Items", [""])], true);
// Create folders
imap.send("CREATE \"Tofu\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CREATE \"Fruit\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CREATE \"Fruit/Apple\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CREATE \"Fruit/Apple/Green\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CREATE \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Select folder from another connection
other_conn.send("SELECT \"Tofu\"").await;
other_conn.assert_read(Type::Tagged, ResponseType::Ok).await;
other_conn.send("SELECT \"L&APg-bende opgaver\"").await;
other_conn.assert_read(Type::Tagged, ResponseType::Ok).await;
// Make sure folders are visible
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST \"\" \"*\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", [""]),
("Deleted Items", [""]),
("Fruit", [""]),
("Fruit/Apple", [""]),
("Fruit/Apple/Green", [""]),
("Tofu", [""]),
("L&APg-bende opgaver", [""]),
],
true,
);
}
imap.send("DELETE \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Special use folders that already exist should not be allowed
imap.send("CREATE \"Second trash\" (USE (\\Trash))").await;
imap.assert_read(Type::Tagged, ResponseType::No).await;
// Every command in a pipelined batch is answered, even after a failure
imap.send_raw(concat!(
"_p1 STATUS \"Tofu\" (MESSAGES)\r\n",
"_p2 STATUS \"Does Not Exist\" (MESSAGES)\r\n",
"_x STATUS \"Fruit/Apple\" (MESSAGES)\r\n"
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("_p1 OK")
.assert_contains("_p2 NO [NONEXISTENT]")
.assert_contains("* STATUS \"Fruit/Apple\"");
// Enable IMAP4rev2
imap.send("ENABLE IMAP4rev2").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Create and delete using IMAP4rev2
imap.send("CREATE \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("SELECT \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UNSELECT \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("DELETE \"L&APg-bende opgaver\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Create missing parent folders
imap.send("CREATE \"/Vegetable/Broccoli\" (USE (\\Important))")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CREATE \" Cars/Electric /4 doors/ Red/\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["HasNoChildren", ""]),
("Deleted Items", ["HasNoChildren", "Trash"]),
("Cars/Electric/4 doors/Red", ["HasNoChildren", ""]),
("Cars/Electric/4 doors", ["HasChildren", ""]),
("Cars/Electric", ["HasChildren", ""]),
("Cars", ["HasChildren", ""]),
("Fruit", ["HasChildren", ""]),
("Fruit/Apple", ["HasChildren", ""]),
("Fruit/Apple/Green", ["HasNoChildren", ""]),
("Vegetable", ["HasChildren", ""]),
("Vegetable/Broccoli", ["HasNoChildren", "\\Important"]),
("Tofu", ["HasNoChildren", ""]),
],
true,
);
}
// Rename folders
imap.send("RENAME \"Fruit/Apple/Green\" \"Fruit/Apple/Red\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("RENAME \"Cars\" \"Vehicles\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("RENAME \"Vegetable/Broccoli\" \"Veggies/Green/Broccoli\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("RENAME \"Tofu\" \"INBOX\"").await;
imap.assert_read(Type::Tagged, ResponseType::No).await;
imap.send("RENAME \"Tofu\" \"Inbox/Tofu\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("RENAME \"Deleted Items\" \"Recycle Bin\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["HasChildren", ""]),
("INBOX/Tofu", ["HasNoChildren", ""]),
("Recycle Bin", ["HasNoChildren", "Trash"]),
("Vehicles/Electric/4 doors/Red", ["HasNoChildren", ""]),
("Vehicles/Electric/4 doors", ["HasChildren", ""]),
("Vehicles/Electric", ["HasChildren", ""]),
("Vehicles", ["HasChildren", ""]),
("Fruit", ["HasChildren", ""]),
("Fruit/Apple", ["HasChildren", ""]),
("Fruit/Apple/Red", ["HasNoChildren", ""]),
("Vegetable", ["HasNoChildren", ""]),
("Veggies", ["HasChildren", ""]),
("Veggies/Green", ["HasChildren", ""]),
("Veggies/Green/Broccoli", ["HasNoChildren", ""]),
],
true,
);
}
// Delete folders
imap.send("DELETE \"INBOX/Tofu\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("DELETE \"Vegetable\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("DELETE \"Vehicles\"").await;
imap.assert_read(Type::Tagged, ResponseType::No).await;
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["HasNoChildren", ""]),
("Recycle Bin", ["HasNoChildren", "Trash"]),
("Vehicles/Electric/4 doors/Red", ["HasNoChildren", ""]),
("Vehicles/Electric/4 doors", ["HasChildren", ""]),
("Vehicles/Electric", ["HasChildren", ""]),
("Vehicles", ["HasChildren", ""]),
("Fruit", ["HasChildren", ""]),
("Fruit/Apple", ["HasChildren", ""]),
("Fruit/Apple/Red", ["HasNoChildren", ""]),
("Veggies", ["HasChildren", ""]),
("Veggies/Green", ["HasChildren", ""]),
("Veggies/Green/Broccoli", ["HasNoChildren", ""]),
],
true,
);
}
// Subscribe
imap.send("SUBSCRIBE \"INBOX\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("SUBSCRIBE \"Vehicles/Electric/4 doors/Red\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST \"\" \"*\" RETURN (SUBSCRIBED SPECIAL-USE)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["Subscribed", ""]),
("Recycle Bin", ["", "Trash"]),
("Vehicles/Electric/4 doors/Red", ["Subscribed", ""]),
("Vehicles/Electric/4 doors", ["", ""]),
("Vehicles/Electric", ["", ""]),
("Vehicles", ["", ""]),
("Fruit", ["", ""]),
("Fruit/Apple", ["", ""]),
("Fruit/Apple/Red", ["", ""]),
("Veggies", ["", ""]),
("Veggies/Green", ["", ""]),
("Veggies/Green/Broccoli", ["", ""]),
],
true,
);
}
// Filter by subscribed including children
imap.send("LIST (SUBSCRIBED) \"\" \"*\" RETURN (CHILDREN)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["Subscribed", "HasNoChildren"]),
(
"Vehicles/Electric/4 doors/Red",
["Subscribed", "HasNoChildren"],
),
],
true,
);
// Recursive match including children
imap.send("LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"*\" RETURN (CHILDREN)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", ["Subscribed", "HasNoChildren"]),
(
"Vehicles/Electric/4 doors/Red",
["Subscribed", "HasNoChildren"],
),
(
"Vehicles/Electric/4 doors",
["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"],
),
(
"Vehicles/Electric",
["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"],
),
(
"Vehicles",
["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"],
),
],
true,
);
// Imap4rev1 LSUB
imap.send("LSUB \"\" \"*\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[("INBOX", [""]), ("Vehicles/Electric/4 doors/Red", [""])],
true,
);
// Unsubscribe
imap.send("UNSUBSCRIBE \"Vehicles/Electric/4 doors/Red\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
for imap in [&mut imap, &mut imap_check] {
imap.send("LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"*\" RETURN (CHILDREN)")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders([("INBOX", ["Subscribed", "HasNoChildren"])], true);
}
// LIST Filters
imap.send("LIST \"\" \"%\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", [""]),
("Recycle Bin", [""]),
("Vehicles", [""]),
("Fruit", [""]),
("Veggies", [""]),
],
true,
);
imap.send("LIST \"\" \"*/Red\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("Vehicles/Electric/4 doors/Red", [""]),
("Fruit/Apple/Red", [""]),
],
true,
);
imap.send("LIST \"\" \"Fruit/*\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders([("Fruit/Apple/Red", [""]), ("Fruit/Apple", [""])], true);
imap.send("LIST \"\" \"Fruit/%\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders([("Fruit/Apple", [""])], true);
// Restore Trash folder's original name
imap.send("RENAME \"Recycle Bin\" \"Deleted Items\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Shared folder creation tests
let mut imap_jane = test.account("[email protected]").imap_client().await;
imap_jane
.send("CREATE \"Shared Folders/[email protected]/INBOX/Test\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane
.send("CREATE \"Shared Folders/[email protected]/Test\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane
.send("CREATE \"Shared Folders/[email protected]/Test/TestSubfolder\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("LIST \"\" \"*\"").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_folders(
[
("INBOX", [""]),
("Deleted Items", [""]),
("Drafts", [""]),
("Junk Mail", [""]),
("Sent Items", [""]),
("Shared Folders", [""]),
("Shared Folders/[email protected]", [""]),
("Shared Folders/[email protected]/Deleted Items", [""]),
("Shared Folders/[email protected]/Drafts", [""]),
("Shared Folders/[email protected]/INBOX", [""]),
("Shared Folders/[email protected]/INBOX/Test", [""]),
("Shared Folders/[email protected]/Junk Mail", [""]),
("Shared Folders/[email protected]/Sent Items", [""]),
("Shared Folders/[email protected]/Test", [""]),
(
"Shared Folders/[email protected]/Test/TestSubfolder",
[""],
),
],
true,
);
}
fn mailbox_matches_pattern() {
let mailboxes = [
"imaptest",
"imaptest/test",
"imaptest/test2",
"imaptest/test3",
"imaptest/test3/test4",
"imaptest/test3/test4/test5",
"foobar/test",
"foobar/test/test",
"foobar/test1/test1",
];
for (pattern, expected_match) in [
(
"imaptest/%",
vec!["imaptest/test", "imaptest/test2", "imaptest/test3"],
),
("imaptest/%/%", vec!["imaptest/test3/test4"]),
(
"imaptest/*",
vec![
"imaptest/test",
"imaptest/test2",
"imaptest/test3",
"imaptest/test3/test4",
"imaptest/test3/test4/test5",
],
),
("imaptest/*test4", vec!["imaptest/test3/test4"]),
(
"imaptest/*test*",
vec![
"imaptest/test",
"imaptest/test2",
"imaptest/test3",
"imaptest/test3/test4",
"imaptest/test3/test4/test5",
],
),
("imaptest/%3/%", vec!["imaptest/test3/test4"]),
("imaptest/%3/%4", vec!["imaptest/test3/test4"]),
("imaptest/%t*4", vec!["imaptest/test3/test4"]),
("*st/%3/%4/%5", vec!["imaptest/test3/test4/test5"]),
(
"*%*%*%",
vec![
"imaptest",
"imaptest/test",
"imaptest/test2",
"imaptest/test3",
"imaptest/test3/test4",
"imaptest/test3/test4/test5",
"foobar/test",
"foobar/test/test",
"foobar/test1/test1",
],
),
("foobar*test", vec!["foobar/test", "foobar/test/test"]),
] {
let patterns = vec![pattern.into()];
let mut matched_mailboxes = Vec::new();
for mailbox in mailboxes {
if matches_pattern(&patterns, mailbox) {
matched_mailboxes.push(mailbox);
}
}
assert_eq!(matched_mailboxes, expected_match, "for pattern {}", pattern);
}
}
+126
View File
@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::AssertResult;
use crate::utils::{server::TestServer, sieve::SieveConnection};
use imap_proto::ResponseType;
pub async fn test(test: &TestServer) {
println!("Running ManageSieve tests...");
// Connect to ManageSieve
let mut sieve = SieveConnection::connect().await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("IMPLEMENTATION");
// Authenticate
let account = test.account("[email protected]");
sieve.authenticate(account.name(), account.secret()).await;
// CheckScript
sieve.send("CHECKSCRIPT \"if true { keep; }\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve.send("CHECKSCRIPT \"keep :invalidtag;\"").await;
sieve.assert_read(ResponseType::No).await;
// PutScript
sieve
.send_literal("PUTSCRIPT \"simple script\" ", "if true { keep; }\r\n")
.await;
sieve.assert_read(ResponseType::Ok).await;
// PutScript should overwrite existing scripts
sieve.send("PUTSCRIPT \"holidays\" \"discard;\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve
.send_literal(
"PUTSCRIPT \"holidays\" ",
"require \"vacation\"; vacation \"Gone fishin'\";\r\n",
)
.await;
sieve.assert_read(ResponseType::Ok).await;
// GetScript
sieve.send("GETSCRIPT \"simple script\"").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("if true");
sieve.send("GETSCRIPT \"holidays\"").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("Gone fishin'");
sieve.send("GETSCRIPT \"dummy\"").await;
sieve.assert_read(ResponseType::No).await;
// ListScripts
sieve.send("LISTSCRIPTS").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("simple script")
.assert_contains("holidays")
.assert_count("ACTIVE", 0);
// RenameScript
sieve
.send("RENAMESCRIPT \"simple script\" \"minimalist script\"")
.await;
sieve.assert_read(ResponseType::Ok).await;
sieve
.send("RENAMESCRIPT \"holidays\" \"minimalist script\"")
.await;
sieve
.assert_read(ResponseType::No)
.await
.assert_contains("ALREADYEXISTS");
// SetActive
sieve.send("SETACTIVE \"holidays\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve.send("LISTSCRIPTS").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("minimalist script")
.assert_contains("holidays\" ACTIVE");
// Deleting an active script should not be allowed
sieve.send("DELETESCRIPT \"holidays\"").await;
sieve
.assert_read(ResponseType::No)
.await
.assert_contains("ACTIVE");
// Deactivate all
sieve.send("SETACTIVE \"\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve.send("LISTSCRIPTS").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_contains("minimalist script")
.assert_contains("holidays")
.assert_count("ACTIVE", 0);
// DeleteScript
sieve.send("DELETESCRIPT \"holidays\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve.send("DELETESCRIPT \"minimalist script\"").await;
sieve.assert_read(ResponseType::Ok).await;
sieve.send("LISTSCRIPTS").await;
sieve
.assert_read(ResponseType::Ok)
.await
.assert_count("minimalist script", 0)
.assert_count("holidays", 0);
}
+144
View File
@@ -0,0 +1,144 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type, expand_uid_list};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
println!("Running MESSAGELIMIT tests...");
// Both limits are advertised, and SAVELIMIT must not be the stricter of the
// two or MESSAGELIMIT-only clients would hit unexpected COPY rejections
imap.send("CAPABILITY").await;
let capabilities = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MESSAGELIMIT=")
.assert_contains("SAVELIMIT=");
assert!(
advertised_limit(&capabilities, "SAVELIMIT=")
>= advertised_limit(&capabilities, "MESSAGELIMIT="),
"SAVELIMIT must be at least MESSAGELIMIT, got {capabilities:?}"
);
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// The test mailboxes hold far fewer messages than the limit, so no
// command should be truncated and no MESSAGELIMIT code should appear
for command in [
"UID FETCH 1:* (UID)",
"UID SEARCH ALL",
"UID STORE 1:* +FLAGS.SILENT (\\Seen)",
"UID STORE 1:* -FLAGS.SILENT (\\Seen)",
] {
imap.send(command).await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("MESSAGELIMIT");
}
// EXPUNGE and CLOSE are never limited
imap.send("EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("MESSAGELIMIT");
// COPY is governed by SAVELIMIT and stays well under it here
imap.send("CREATE Savelimit").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID COPY 1:* Savelimit").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("[COPYUID ")
.assert_not_contains("MESSAGELIMIT");
imap.send("DELETE Savelimit").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// UIDAFTER and UIDBEFORE are the search criteria added by RFC 9738
imap.send("UID SEARCH UIDAFTER 0").await;
let all = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID SEARCH ALL").await;
let expected = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert_eq!(
search_results(&all),
search_results(&expected),
"UIDAFTER 0 must match every message"
);
// UIDBEFORE 1 can never match anything
imap.send("UID SEARCH UIDBEFORE 1").await;
let none = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
search_results(&none).is_empty(),
"UIDBEFORE 1 must match nothing, got {none:?}"
);
// The two criteria partition the mailbox around a pivot UID
let pivot = search_results(&expected)
.into_iter()
.max()
.expect("INBOX should not be empty");
imap.send(&format!("UID SEARCH UIDBEFORE {pivot}")).await;
let before = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
search_results(&before).iter().all(|uid| *uid < pivot),
"UIDBEFORE {pivot} returned a UID at or above the pivot"
);
imap.send(&format!("UID SEARCH UIDAFTER {pivot}")).await;
let after = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
search_results(&after).is_empty(),
"UIDAFTER on the highest UID must match nothing, got {after:?}"
);
// Both criteria are still usable inside a boolean expression
imap.send(&format!("UID SEARCH UIDAFTER 0 UIDBEFORE {pivot}"))
.await;
let between = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert_eq!(search_results(&between), search_results(&before));
}
fn advertised_limit(response: &[String], capability: &str) -> u32 {
response
.iter()
.find_map(|line| {
line.split_whitespace()
.find_map(|token| token.strip_prefix(capability))
.map(|limit| limit.trim_end_matches(']').parse().unwrap())
})
.unwrap_or_else(|| panic!("No {capability} capability in {response:?}"))
}
fn search_results(response: &[String]) -> Vec<u32> {
let mut uids: Vec<u32> = response
.iter()
.find_map(|line| {
let line = line.trim_end();
if let Some(list) = line.strip_prefix("* SEARCH") {
Some(
list.split_whitespace()
.filter_map(|uid| uid.parse().ok())
.collect(),
)
} else if line.starts_with("* ESEARCH") {
// * ESEARCH (TAG "_x") UID ALL 1:12
Some(
line.split_once(" ALL ")
.map(|(_, list)| expand_uid_list(list).into_iter().collect())
.unwrap_or_default(),
)
} else {
None
}
})
.unwrap_or_default();
uids.sort_unstable();
uids
}
+323
View File
@@ -0,0 +1,323 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acl;
pub mod antispam;
pub mod append;
pub mod basic;
pub mod body_structure;
pub mod condstore;
pub mod copy_move;
pub mod fetch;
pub mod idle;
pub mod mailbox;
pub mod managesieve;
pub mod messagelimit;
pub mod objectid;
pub mod pop;
pub mod search;
pub mod store;
pub mod thread;
pub mod uidbatches;
pub mod uidonly;
use crate::utils::{
imap::{AssertResult, ImapConnection, Type},
server::TestServerBuilder,
};
use ahash::AHashSet;
use imap_proto::ResponseType;
use registry::{
schema::{
enums::{Permission, SpecialUse},
prelude::ObjectType,
structs::{
Email, EmailFolder, Expression, Imap, MemoryLookupKey, MtaStageAuth, MtaStageData,
SpamClassifier, SpamTag, SpamTagScore,
},
},
types::float::Float,
};
use serde_json::json;
use std::{path::PathBuf, time::Instant};
use utils::map::vec_map::VecMap;
#[tokio::test(flavor = "multi_thread")]
pub async fn imap_tests() {
let mut test = TestServerBuilder::new("imap_tests")
.await
.with_default_listeners()
.await
.build()
.await;
// Create admin account
let admin = test.create_admin_account("[email protected]").await;
// Create test users
for (name, secret, description, aliases) in [
(
"[email protected]",
"12345 + extra safety",
"John Doe",
&["[email protected]"][..],
),
(
"[email protected]",
"abcde + extra safety",
"Jane Smith",
&["[email protected]"][..],
),
(
"[email protected]",
"098765 + extra safety",
"Bill Foobar",
&["[email protected]"][..],
),
(
"[email protected]",
"a_pop3_safe_secret_with_extra_safety",
"Karl Popper",
&["[email protected]"][..],
),
(
"[email protected]",
"secret2 + extra safety",
"Sigmund Gudmund Dudmundsson",
&[][..],
),
(
"[email protected]",
"secret3 + extra safety",
"Spam Trap",
&[][..],
),
] {
let account = admin
.create_user_account(
name,
secret,
description,
aliases,
vec![Permission::UnlimitedRequests, Permission::UnlimitedUploads],
)
.await;
test.insert_account(account);
}
// Create test group
test.insert_account(
admin
.create_group_account("[email protected]", "Support Group", &[])
.await,
);
// Add Jane to the Support group
let support_id = test.account("[email protected]").id();
admin
.registry_update_object(
ObjectType::Account,
test.account("[email protected]").id(),
json!({
"memberGroupIds": { support_id: true },
}),
)
.await;
// Add test settings
admin
.registry_create_object(Imap {
allow_plain_text_auth: true,
..Default::default()
})
.await;
admin
.registry_create_object(MtaStageAuth {
require: Expression {
else_: "false".to_string(),
..Default::default()
},
..Default::default()
})
.await;
admin
.registry_create_object(SpamClassifier {
min_ham_samples: 10,
min_spam_samples: 10,
..Default::default()
})
.await;
admin
.registry_create_object(Email {
default_folders: VecMap::from_iter(
[
(SpecialUse::Inbox, "Inbox"),
(SpecialUse::Sent, "Sent Items"),
(SpecialUse::Trash, "Deleted Items"),
(SpecialUse::Junk, "Junk Mail"),
(SpecialUse::Drafts, "Drafts"),
]
.into_iter()
.map(|(use_, name)| {
(
use_,
EmailFolder {
name: name.into(),
subscribe: false,
..Default::default()
},
)
}),
),
..Default::default()
})
.await;
admin
.registry_create_object(MtaStageData {
add_delivered_to_header: false,
enable_spam_filter: Expression {
else_: "recipients[0] != '[email protected]'".into(),
..Default::default()
},
..Default::default()
})
.await;
admin
.registry_create_object(SpamTag::Score(SpamTagScore {
score: Float::new(10.0),
tag: "PROB_SPAM_LOW".into(),
}))
.await;
admin
.registry_create_object(SpamTag::Score(SpamTagScore {
score: Float::new(10.0),
tag: "PROB_SPAM_HIGH".into(),
}))
.await;
admin
.registry_create_object(SpamTag::Score(SpamTagScore {
score: Float::new(100.0),
tag: "SPAM_TRAP".into(),
}))
.await;
admin
.registry_create_object(MemoryLookupKey {
is_glob_pattern: true,
key: "spamtrap@*".into(),
namespace: "spam-traps".into(),
})
.await;
admin.reload_settings().await;
admin.reload_lookup_stores().await;
test.insert_account(admin);
let start_time = Instant::now();
// Body structure tests
body_structure::test();
// Connect to IMAP server
let mut imap_check = ImapConnection::connect(b"_y ").await;
let mut imap = ImapConnection::connect(b"_x ").await;
for imap in [&mut imap, &mut imap_check] {
imap.assert_read(Type::Untagged, ResponseType::Ok).await;
}
// Unauthenticated tests
basic::test(&mut imap, &mut imap_check).await;
// Login
let account = test.account("[email protected]");
for imap in [&mut imap, &mut imap_check] {
imap.authenticate(account.name(), account.secret()).await;
}
// Test GETJMAPACCESS (RFC 9698)
imap.send("GETJMAPACCESS").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* JMAPACCESS \"")
.assert_contains("/.well-known/jmap\"");
// Delete folders
for mailbox in ["Drafts", "Junk Mail", "Sent Items"] {
imap.send(&format!("DELETE \"{}\"", mailbox)).await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
}
mailbox::test(&mut imap, &mut imap_check, &test).await;
append::test(&mut imap, &mut imap_check, &test).await;
search::test(&mut imap, &mut imap_check, &test).await;
fetch::test(&mut imap, &mut imap_check).await;
objectid::test(&test).await;
store::test(&mut imap, &mut imap_check, &test).await;
copy_move::test(&mut imap, &mut imap_check).await;
thread::test(&mut imap, &mut imap_check, &test).await;
idle::test(&mut imap, &mut imap_check, false).await;
condstore::test(&mut imap, &mut imap_check).await;
acl::test(&mut imap, &mut imap_check, &test).await;
uidbatches::test(&mut imap, &mut imap_check).await;
messagelimit::test(&mut imap, &mut imap_check).await;
// UIDONLY cannot be disabled once enabled, so it uses its own connection
uidonly::test(&test).await;
// Logout
for imap in [&mut imap, &mut imap_check] {
imap.send("UNAUTHENTICATE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("LOGOUT").await;
imap.assert_read(Type::Untagged, ResponseType::Bye).await;
}
// Antispam training
antispam::test(&test).await;
// Run ManageSieve tests
managesieve::test(&test).await;
// Run POP3 tests
pop::test(&test).await;
// Print elapsed time
let elapsed = start_time.elapsed();
println!(
"Elapsed: {}.{:03}s",
elapsed.as_secs(),
elapsed.subsec_millis()
);
// Remove test data
if test.is_reset() {
test.temp_dir.delete();
}
}
pub fn expand_uid_list(list: &str) -> AHashSet<u32> {
let mut items = AHashSet::new();
for uid in list.split(',') {
if let Some((start, end)) = uid.split_once(':') {
let start = start.parse::<u32>().unwrap();
let end = end.parse::<u32>().unwrap();
for uid in start..=end {
items.insert(uid);
}
} else {
items.insert(uid.parse::<u32>().unwrap());
}
}
items
}
fn resources_dir() -> PathBuf {
let mut resources = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
resources.push("resources");
resources.push("imap");
resources
}
+204
View File
@@ -0,0 +1,204 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, Type};
use crate::utils::server::TestServer;
use imap_proto::ResponseType;
pub async fn test(test: &TestServer) {
println!("Running OBJECTID+ tests...");
let account = test.account("[email protected]");
let account_id = account.id_string().to_string();
let mut imap = account.imap_client().await;
// OBJECTID+ is advertised
imap.send("CAPABILITY").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("OBJECTID+")
.assert_count("OBJECTID ", 0);
// Before activation no object identifiers are leaked
imap.send("SELECT INBOX").await;
let lines = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
!lines
.iter()
.any(|l| l.contains("OBJECTID") || l.contains("MAILBOXID")),
"Pre-activation SELECT leaked object identifiers: {lines:?}"
);
// Explicit activation via ENABLE
imap.send("ENABLE OBJECTID+").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("ENABLED OBJECTID+");
// SELECT now returns a compound OBJECTID with MAILBOXID and ACCOUNTID
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("[OBJECTID (")
.assert_contains("MAILBOXID ")
.assert_contains(&format!("ACCOUNTID {account_id}"));
// STATUS OBJECTID returns the compound for the queried mailbox, and an
// already-activated session is not sent a second ENABLED response
imap.send("STATUS INBOX (OBJECTID)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("OBJECTID (")
.assert_contains("MAILBOXID ")
.assert_contains(&format!("ACCOUNTID {account_id}"))
.assert_not_contains("ENABLED OBJECTID+");
// FETCH OBJECTID returns EMAILID and THREADID but never ACCOUNTID
imap.send("UID FETCH 1 (OBJECTID)").await;
let lines = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
lines
.iter()
.any(|l| l.contains("OBJECTID (") && l.contains("EMAILID ") && l.contains("THREADID ")),
"FETCH OBJECTID must include EMAILID and THREADID: {lines:?}"
);
assert!(
!lines.iter().any(|l| l.contains("ACCOUNTID")),
"FETCH OBJECTID must not include ACCOUNTID: {lines:?}"
);
// Commands that do not request the OBJECTID item never emit it, even once activated
imap.send("UID FETCH 1 (FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("OBJECTID");
imap.send("STATUS INBOX (MESSAGES)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("OBJECTID");
// EXAMINE returns the compound OBJECTID response code just like SELECT
imap.send("EXAMINE INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("[OBJECTID (")
.assert_contains("MAILBOXID ")
.assert_contains(&format!("ACCOUNTID {account_id}"));
// CREATE returns the compound OBJECTID response code
imap.send("CREATE \"ObjIdTest\"").await;
let lines = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
let mailbox_id = extract_value(&lines, "MAILBOXID ");
assert!(
lines
.iter()
.any(|l| l.contains(&format!("ACCOUNTID {account_id}"))),
"CREATE OBJECTID missing ACCOUNTID: {lines:?}"
);
// RENAME returns the compound OBJECTID response code and preserves the MAILBOXID
imap.send("RENAME \"ObjIdTest\" \"ObjIdRenamed\"").await;
let lines = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
let renamed_id = extract_value(&lines, "MAILBOXID ");
assert_eq!(
mailbox_id, renamed_id,
"RENAME must preserve the MAILBOXID: {lines:?}"
);
// Identifier-based selection resolves the mailbox regardless of its current name
imap.send(&format!(
"SELECT \"DoesNotExist\" (OBJECTID (MAILBOXID {mailbox_id} ACCOUNTID {account_id}))"
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(&format!("MAILBOXID {mailbox_id}"));
// An unknown MAILBOXID falls back to selecting by name
imap.send("SELECT \"ObjIdRenamed\" (OBJECTID (MAILBOXID abcdefgh ACCOUNTID abcdefgh))")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(&format!("MAILBOXID {mailbox_id}"));
// An undecodable identifier falls back to selecting by name instead of failing
imap.send("SELECT \"ObjIdRenamed\" (OBJECTID (MAILBOXID 456))")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(&format!("MAILBOXID {mailbox_id}"));
// Unrecognised keys in the OBJECTID parameter are ignored
imap.send("SELECT \"ObjIdRenamed\" (OBJECTID (FOOBAR baz MAILBOXID 456))")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(&format!("MAILBOXID {mailbox_id}"));
// Cleanup
imap.send("UNSELECT").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("DELETE \"ObjIdRenamed\"").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Implicit activation via the STATUS attribute on a fresh session
let mut imap2 = test.account("[email protected]").imap_client().await;
imap2.send("STATUS INBOX (OBJECTID)").await;
let lines = imap2.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
lines.iter().any(|l| l.contains("ENABLED OBJECTID+")),
"STATUS did not implicitly activate OBJECTID+: {lines:?}"
);
// Implicit activation via the SELECT OBJECTID parameter on a fresh session
let mut imap3 = test.account("[email protected]").imap_client().await;
imap3.send("SELECT INBOX (OBJECTID)").await;
let lines = imap3.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
lines.iter().any(|l| l.contains("ENABLED OBJECTID+")),
"SELECT (OBJECTID) did not implicitly activate OBJECTID+: {lines:?}"
);
assert!(
lines.iter().any(|l| l.contains("[OBJECTID (")),
"SELECT (OBJECTID) did not return a compound OBJECTID: {lines:?}"
);
// Activation via FETCH then a plain SELECT/CREATE must still carry the compound code
let mut imap4 = test.account("[email protected]").imap_client().await;
imap4.send("SELECT INBOX").await;
imap4.assert_read(Type::Tagged, ResponseType::Ok).await;
imap4.send("UID FETCH 1 (OBJECTID)").await;
let lines = imap4.assert_read(Type::Tagged, ResponseType::Ok).await;
assert!(
lines.iter().any(|l| l.contains("ENABLED OBJECTID+")),
"FETCH (OBJECTID) did not implicitly activate OBJECTID+: {lines:?}"
);
imap4.send("SELECT INBOX").await;
imap4
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("[OBJECTID (");
imap4.send("CREATE \"ObjIdTest4\"").await;
imap4
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("[OBJECTID (");
imap4.send("DELETE \"ObjIdTest4\"").await;
imap4.assert_read(Type::Tagged, ResponseType::Ok).await;
}
fn extract_value(lines: &[String], key: &str) -> String {
for line in lines {
if let Some((_, rest)) = line.split_once(key) {
return rest
.split([' ', ')'])
.next()
.expect("Missing value delimiter")
.to_string();
}
}
panic!("Key {key:?} not found in {lines:?}");
}
+186
View File
@@ -0,0 +1,186 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{
imap::AssertResult,
pop3::{Pop3Connection, ResponseType},
server::TestServer,
smtp::SmtpConnection,
};
pub async fn test(test: &TestServer) {
println!("Running POP3 tests...");
// Send 3 test emails
for i in 0..3 {
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
&format!(
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report {}\r\n",
"X-Spam-Status: No\r\n",
"\r\n",
"I'm going to need those TPS {} reports ASAP.\r\n",
"..\r\n",
"So, if you could do that, that'd be great."
),
i, i
),
)
.await;
}
// Connect to POP3
let account = test.account("[email protected]");
let mut pop3 = Pop3Connection::connect().await;
// Capabilities
pop3.send("CAPA").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("SASL PLAIN")
.assert_contains("IMPLEMENTATION");
// Noop
pop3.send("NOOP").await;
pop3.assert_read(ResponseType::Ok).await;
// Authenticate user/pass
pop3.send("PASS secret").await;
pop3.assert_read(ResponseType::Err).await;
pop3.send("USER [email protected]").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("PASS wrong_secret").await;
pop3.assert_read(ResponseType::Err).await;
pop3.send("USER [email protected]").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send(&format!("PASS {}", account.secret())).await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("QUIT").await;
// Authenticate using AUTH PLAIN
let mut pop3 = Pop3Connection::connect().await;
pop3.authenticate(account.name(), account.secret()).await;
// STAT
pop3.send("STAT").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 3 609");
// UTF8
pop3.send("UTF8").await;
pop3.assert_read(ResponseType::Ok).await;
// LIST
pop3.send("LIST").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 3 messages")
.assert_contains("1 203")
.assert_contains("2 203")
.assert_contains("3 203");
pop3.send("LIST 2").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 2 203");
// UIDL
pop3.send("UIDL").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 3 messages")
.assert_contains("1 ")
.assert_contains("2 ")
.assert_contains("3 ");
pop3.send("UIDL 2").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 2 ");
// RETR
pop3.send("RETR 1").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 203 octets")
.assert_contains("I'm going to need those TPS 0 reports ASAP.")
.assert_contains("So, if you could do that, that'd be great.");
pop3.send("RETR 3").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 203 octets")
.assert_contains("I'm going to need those TPS 2 reports ASAP.")
.assert_contains("So, if you could do that, that'd be great.");
pop3.send("RETR 4").await;
pop3.assert_read(ResponseType::Err).await;
// TOP
pop3.send("TOP 1 4").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 203 octets")
.assert_contains("Subject: TPS Report 0")
.assert_not_contains("I'm going to need those TPS 0 reports ASAP.");
pop3.send("TOP 3 4").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("+OK 203 octets")
.assert_contains("Subject: TPS Report 2")
.assert_not_contains("I'm going to need those TPS 2 reports ASAP.");
// DELE + RSET + QUIT (should not delete messages)
pop3.send("DELE 1").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("DELE 4").await;
pop3.assert_read(ResponseType::Err).await;
pop3.send("RSET").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("QUIT").await;
let mut pop3 = Pop3Connection::connect().await;
pop3.authenticate(account.name(), account.secret()).await;
pop3.send("STAT").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 3 609");
// DELE + QUIT (should delete messages)
pop3.send("DELE 2").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("QUIT").await;
pop3.assert_read(ResponseType::Ok).await;
let mut pop3 = Pop3Connection::connect().await;
pop3.authenticate(account.name(), account.secret()).await;
pop3.send("STAT").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 2 406");
pop3.send("TOP 1 4").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("TPS Report 0");
pop3.send("TOP 2 4").await;
pop3.assert_read(ResponseType::Multiline)
.await
.assert_contains("TPS Report 2");
// DELE using pipelining
pop3.send("DELE 1\r\nDELE 2").await;
pop3.assert_read(ResponseType::Ok).await;
pop3.assert_read(ResponseType::Ok).await;
pop3.send("QUIT").await;
pop3.assert_read(ResponseType::Ok).await;
let mut pop3 = Pop3Connection::connect().await;
pop3.authenticate(account.name(), account.secret()).await;
pop3.send("STAT").await;
pop3.assert_read(ResponseType::Ok)
.await
.assert_contains("+OK 0 0");
pop3.send("QUIT").await;
}
+139
View File
@@ -0,0 +1,139 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use super::{AssertResult, ImapConnection, Type};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, test: &TestServer) {
println!("Running SEARCH tests...");
// Searches without selecting a mailbox should fail.
imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await;
imap.assert_read(Type::Tagged, ResponseType::Bad).await;
// Select INBOX
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("10 EXISTS")
.assert_contains("[UIDNEXT 11]");
imap_check.send("SELECT INBOX").await;
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
// Min, Max and Count
imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1,10");
imap_check.send("UID SEARCH ALL").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 1 2 3 4 5 6 7 8 9 10");
// Filters
imap_check
.send("UID SEARCH OR FROM nathaniel SUBJECT argentina")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 1 3 4 6");
imap_check
.send("UID SEARCH UNSEEN OR KEYWORD Flag_007 KEYWORD Flag_004")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 5 8");
imap_check
.send("UID SEARCH TEXT coffee FROM vandelay SUBJECT exporting SENTON 20-Nov-2021")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 10");
imap_check
.send(concat!(
"UID SEARCH CHARSET UTF-8 TEXT {75+}\r\n",
"ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨"
))
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 10");
imap_check
.send("UID SEARCH NOT (FROM nathaniel ANSWERED)")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 2 3 5 7 8 9 10");
imap_check
.send("UID SEARCH UID 0:6 LARGER 1000 SMALLER 2000")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 1 2");
// Saved search
imap_check.send(
"UID SEARCH RETURN (SAVE ALL) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore",
)
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("1,3:4,6,8,10");
imap_check.send("UID SEARCH NOT $").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 2 5 7 9");
imap_check
.send("UID SEARCH $ SMALLER 1000 SUBJECT section")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SEARCH 8");
imap_check.send("UID SEARCH RETURN (MIN MAX) NOT $").await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MIN 2 MAX 9");
// Sort
imap_check
.send("UID SORT (REVERSE SUBJECT REVERSE DATE) UTF-8 FROM Nathaniel")
.await;
imap_check
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* SORT 6 4 1");
imap.send("UID SORT RETURN (COUNT ALL) (DATE SUBJECT) UTF-8 ALL")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(if !test.server.search_store().is_mysql() {
"COUNT 10 ALL 6,4:5,1,10,3,7:8,2,9"
} else {
"COUNT 10 ALL 9,3,7:8,2,6,4:5,1,10"
}); //6,4:5,1,10,9,3,7:8,2");
}
+75
View File
@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use crate::utils::server::TestServer;
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) {
println!("Running STORE tests...");
// Select INBOX
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("10 EXISTS")
.assert_contains("[UIDNEXT 11]");
// Set all messages to flag "Seen"
imap.send("UID STORE 1:10 +FLAGS.SILENT (\\Seen)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FLAGS", 0);
// Check that the flags were set
imap.send("UID FETCH 1:* (Flags)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("\\Seen", 10);
// Check status
imap.send("STATUS INBOX (UIDNEXT MESSAGES UNSEEN)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MESSAGES 10")
.assert_contains("UNSEEN 0")
.assert_contains("UIDNEXT 11");
// Remove Seen flag from all messages
imap.send("UID STORE 1:10 -FLAGS (\\Seen)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FLAGS", 10)
.assert_count("Seen", 0);
// Check that the flags were removed
imap.send("UID FETCH 1:* (Flags)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("\\Seen", 0);
imap.send("STATUS INBOX (UIDNEXT MESSAGES UNSEEN)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("MESSAGES 10")
.assert_contains("UNSEEN 10")
.assert_contains("UIDNEXT 11");
// Store using saved searches
test.wait_for_tasks().await;
imap.send("SEARCH RETURN (SAVE) FROM nathaniel").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID STORE $ +FLAGS (\\Answered)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FLAGS", 3);
// Remove Answered flag
imap.send("UID STORE 1:* -FLAGS (\\Answered)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("FLAGS", 3)
.assert_count("Answered", 0);
}
+120
View File
@@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{ImapConnection, Type, append::build_messages};
use crate::{
imap::{AssertResult, expand_uid_list},
utils::server::TestServer,
};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) {
println!("Running THREAD tests...");
// Create test messages
let messages = build_messages();
// Insert messages using Multiappend
imap.send("CREATE Manchego").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
for (pos, message) in messages.iter().enumerate() {
if pos == 0 {
imap.send(&format!("APPEND Manchego {{{}}}", message.len()))
.await;
} else {
imap.send_untagged(&format!(" {{{}}}", message.len())).await;
}
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
if pos < messages.len() - 1 {
imap.send_raw(message).await;
} else {
imap.send_untagged(message).await;
assert_eq!(
expand_uid_list(
&imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_append_uid()
)
.len(),
messages.len(),
);
}
}
// Obtain ThreadId and MessageId of the first message
test.wait_for_tasks().await;
imap.send("SELECT Manchego").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
let mut email_id = None;
let mut thread_id = None;
imap.send("UID FETCH 1 (OBJECTID)").await;
for line in imap.assert_read(Type::Tagged, ResponseType::Ok).await {
if let Some((_, value)) = line.split_once("EMAILID ") {
email_id = value
.split([' ', ')'])
.next()
.expect("Missing delimiter")
.to_string()
.into();
}
if let Some((_, value)) = line.split_once("THREADID ") {
thread_id = value
.split([' ', ')'])
.next()
.expect("Missing delimiter")
.to_string()
.into();
}
}
let email_id = email_id.expect("Missing EMAILID");
let thread_id = thread_id.expect("Missing THREADID");
// 4 different threads are expected
imap.send("THREAD REFERENCES UTF-8 1:*").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(1 2 3 4)")
.assert_contains("(5 6 7 8)")
.assert_contains("(9 10 11 12)");
// Filter by subject (mySQL does not support searching for short keywords)
if !test.server.search_store().is_mysql() {
imap.send("THREAD REFERENCES UTF-8 SUBJECT T1").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(5 6 7 8)")
.assert_count("(1 2 3 4)", 0)
.assert_count("(9 10 11 12)", 0);
}
// Filter by threadId and messageId
imap.send(&format!(
"UID THREAD REFERENCES UTF-8 THREADID {}",
thread_id
))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(1 2 3 4)")
.assert_count("(", 1);
imap.send(&format!("UID THREAD REFERENCES UTF-8 EMAILID {}", email_id))
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(1)")
.assert_count("(", 1);
// Delete all messages
imap.send("STORE 1:* +FLAGS.SILENT (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("EXPUNGE", 13);
}
+116
View File
@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use imap_proto::ResponseType;
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
println!("Running UIDBATCHES tests...");
// The capability is only advertised once authenticated
imap.send("CAPABILITY").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("UIDBATCHES");
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// A batch size below the configured minimum is rejected with TOOFEW
imap.send("UIDBATCHES 10").await;
imap.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_response_code("TOOFEW");
// Reversed batch ranges are a client bug
imap.send("UIDBATCHES 500 20:10").await;
imap.assert_read(Type::Tagged, ResponseType::Bad)
.await
.assert_response_code("CLIENTBUG");
// More batches than the server is willing to return
imap.send("UIDBATCHES 500 1:100000").await;
imap.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_response_code("TOOMANY");
// Malformed arguments
for command in ["UIDBATCHES", "UIDBATCHES abc", "UIDBATCHES 500 10"] {
imap.send(command).await;
imap.assert_read(Type::Tagged, ResponseType::Bad).await;
}
// INBOX holds fewer messages than one batch, so a single range covering
// the whole UID space is returned and it always reaches down to UID 1
imap.send("UIDBATCHES 500").await;
let response = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* UIDBATCHES (TAG ")
.assert_contains(":1");
let ranges = parse_ranges(&response);
assert_eq!(ranges.len(), 1, "Expected a single batch, got {ranges:?}");
assert_eq!(ranges[0].1, 1, "The last batch must reach UID 1");
// Requesting a batch range beyond what exists returns an empty response
imap.send("UIDBATCHES 500 50:60").await;
let response = imap
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* UIDBATCHES (TAG ");
assert!(
parse_ranges(&response).is_empty(),
"Expected no ranges, got {response:?}"
);
// Asking for the first batch explicitly matches the unbounded form
imap.send("UIDBATCHES 500 1:1").await;
let response = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
assert_eq!(parse_ranges(&response), ranges);
// UIDBATCHES must never populate the SEARCHRES $ variable
imap.send("UID SEARCH RETURN (SAVE) ALL").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UIDBATCHES 500").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID FETCH $ (UID)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("FETCH");
// Every batch must tile the UID space with no gaps
imap.send("UIDBATCHES 500").await;
let response = imap.assert_read(Type::Tagged, ResponseType::Ok).await;
let ranges = parse_ranges(&response);
for window in ranges.windows(2) {
assert_eq!(
window[1].0,
window[0].1 - 1,
"Batches must be contiguous, got {ranges:?}"
);
}
}
fn parse_ranges(response: &[String]) -> Vec<(u32, u32)> {
let line = response
.iter()
.find(|line| line.contains("* UIDBATCHES (TAG "))
.unwrap_or_else(|| panic!("No UIDBATCHES response in {response:?}"));
let Some((_, list)) = line.split_once(") ") else {
return Vec::new();
};
list.trim()
.split(',')
.filter(|range| !range.is_empty())
.map(|range| {
let (high, low) = range
.split_once(':')
.unwrap_or_else(|| panic!("Malformed UID range {range:?}"));
(high.parse().unwrap(), low.parse().unwrap())
})
.collect()
}
+144
View File
@@ -0,0 +1,144 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AssertResult, ImapConnection, Type};
use crate::utils::server::TestServer;
use imap_proto::ResponseType;
pub async fn test(test: &TestServer) {
println!("Running UIDONLY tests...");
// UIDONLY is a one-way switch, so it runs on a connection of its own
let account = test.account("[email protected]");
let mut imap = ImapConnection::connect(b"_u ").await;
imap.assert_read(Type::Untagged, ResponseType::Ok).await;
imap.authenticate(account.name(), account.secret()).await;
// The capability is only advertised once authenticated
imap.send("CAPABILITY").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("UIDONLY");
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Message numbers still work before UIDONLY is enabled
imap.send("FETCH 1 (UID)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(" FETCH (")
.assert_not_contains("UIDFETCH");
// Enable UIDONLY
imap.send("ENABLE UIDONLY").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* ENABLED UIDONLY");
// Every sequence-number command is now rejected with BAD [UIDREQUIRED]
for command in [
"FETCH 1 (UID)",
"STORE 1 +FLAGS (\\Seen)",
"SEARCH ALL",
"COPY 1 \"Deleted Items\"",
"MOVE 1 \"Deleted Items\"",
"SORT (ARRIVAL) UTF-8 ALL",
"THREAD REFERENCES UTF-8 ALL",
] {
imap.send(command).await;
imap.assert_read(Type::Tagged, ResponseType::Bad)
.await
.assert_response_code("UIDREQUIRED");
}
// The UID variants keep working and now answer with UIDFETCH
imap.send("UID FETCH 1:* (FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(" UIDFETCH (")
.assert_not_contains(" FETCH (");
// The UID is the first token of a UIDFETCH response
imap.send("UID FETCH 1 (FLAGS)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* 1 UIDFETCH (");
// UID STORE also answers with UIDFETCH
imap.send("UID STORE 1 +FLAGS (\\Answered)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(" UIDFETCH (")
.assert_not_contains(" FETCH (");
imap.send("UID STORE 1 -FLAGS (\\Answered)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// Plain EXPUNGE stays legal, it carries no message numbers
imap.send("EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// The bare sequence set criterion is banned
imap.send("UID SEARCH 1:*").await;
imap.assert_read(Type::Tagged, ResponseType::Bad)
.await
.assert_response_code("UIDREQUIRED");
// RFC 9586 names "UID <sequence set>" and ALL as the replacements, so both
// must keep working; UIDBATCHES results are consumed through the former
imap.send("UID SEARCH UID 1:*").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID SEARCH ALL").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID FETCH 1:* (UID)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// The SEARCHRES $ variable is not a sequence set either
imap.send("UID SEARCH RETURN (SAVE) ALL").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("UID SEARCH $").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
// UIDNOTSTICKY must never be advertised alongside UIDONLY
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_not_contains("UIDNOTSTICKY");
// Deletions are reported with VANISHED rather than EXPUNGE
imap.send("UID STORE 1 +FLAGS (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("EXPUNGE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("* VANISHED ")
.assert_not_contains("* 1 EXPUNGE");
// The QRESYNC sequence matching parameter is rejected once UIDONLY is on
imap.send("ENABLE QRESYNC").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("SELECT INBOX (QRESYNC (1 1 1:10 (1,2,3 1,2,3)))")
.await;
imap.assert_read(Type::Tagged, ResponseType::Bad)
.await
.assert_response_code("UIDREQUIRED");
// RFC 8437 requires UNAUTHENTICATE to clear every enabled extension, so
// message numbers must work again for the next user of this connection
imap.send("UNAUTHENTICATE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.authenticate(account.name(), account.secret()).await;
imap.send("SELECT INBOX").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("FETCH 1 (UID)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains(" FETCH (")
.assert_not_contains("UIDFETCH");
imap.send("LOGOUT").await;
imap.assert_read(Type::Untagged, ResponseType::Bye).await;
}