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:
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, §ions, 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, §ions) {
|
||||
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, §ions, 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, §ions, (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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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("“exporting”")
|
||||
.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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:?}");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user