IMAP Acl command, rate limiting and ManageSieve server.
This commit is contained in:
291
tests/src/imap/acl.rs
Normal file
291
tests/src/imap/acl.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{append::assert_append_message, AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// Connect to all test accounts
|
||||
let mut imap_jane = ImapConnection::connect(b"_w ").await;
|
||||
let mut imap_bill = ImapConnection::connect(b"_z ").await;
|
||||
for (imap, secret) in [
|
||||
(&mut imap_jane, "AGphbmUuc21pdGhAZXhhbXBsZS5jb20Ac2VjcmV0"),
|
||||
(&mut imap_bill, "AGZvb2JhckBleGFtcGxlLmNvbQBzZWNyZXQ="),
|
||||
] {
|
||||
imap.assert_read(Type::Untagged, ResponseType::Ok).await;
|
||||
imap.send(&format!(
|
||||
"AUTHENTICATE PLAIN {{{}+}}\r\n{}",
|
||||
secret.len(),
|
||||
secret
|
||||
))
|
||||
.await;
|
||||
imap.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 jdoe@example.com").await;
|
||||
imap_jane
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* LISTRIGHTS \"INBOX\" \"jdoe@example.com\" 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 jdoe@example.com 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/Jane Smith\"")
|
||||
.assert_equals("* LIST () \"/\" \"Shared Folders/Jane Smith/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("\"jdoe@example.com\" rl");
|
||||
|
||||
imap_jane
|
||||
.send("SETACL INBOX foobar@example.com 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("\"jdoe@example.com\" rl")
|
||||
.assert_contains("\"foobar@example.com\" tewsrxl");
|
||||
|
||||
imap_bill.send("LIST \"\" \"*\"").await;
|
||||
imap_bill
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("Shared Folders/Jane Smith/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/Jane Smith/Inbox\"")
|
||||
.await;
|
||||
imap_john
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* MYRIGHTS \"Shared Folders/Jane Smith/Inbox\" rl");
|
||||
|
||||
// John should not be able to append messages
|
||||
assert_append_message(
|
||||
imap_john,
|
||||
"Shared Folders/Jane Smith/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 jdoe@example.com +i").await;
|
||||
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
imap_john
|
||||
.send("MYRIGHTS \"Shared Folders/Jane Smith/Inbox\"")
|
||||
.await;
|
||||
imap_john
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* MYRIGHTS \"Shared Folders/Jane Smith/Inbox\" rli");
|
||||
assert_append_message(
|
||||
imap_john,
|
||||
"Shared Folders/Jane Smith/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/Jane Smith/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::Ok).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/Jane Smith/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/Jane Smith/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");
|
||||
|
||||
// Jane stops sharing with Bill, and removes Insert access to John
|
||||
imap_jane.send("DELETEACL INBOX foobar@example.com").await;
|
||||
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
imap_jane.send("SETACL INBOX jdoe@example.com -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("\"jdoe@example.com\" rl")
|
||||
.assert_count("foobar@example.com", 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);
|
||||
}
|
||||
125
tests/src/imap/append.rs
Normal file
125
tests/src/imap/append.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{fs, io};
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{resources_dir, AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// Invalid APPEND commands
|
||||
imap.send("APPEND \"All Mail\" {1+}\r\na").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
||||
.await
|
||||
.assert_response_code("CANNOT");
|
||||
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().map_or(true, |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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
52
tests/src/imap/basic.rs
Normal file
52
tests/src/imap/basic.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// 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 IMAP\" \"version\" ");
|
||||
|
||||
// Login should be disabled
|
||||
imap.send("LOGIN jdoe@example.com 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::fs;
|
||||
|
||||
use imap::op::fetch::AsImapDataItem;
|
||||
use imap_proto::{
|
||||
@@ -7,13 +7,11 @@ use imap_proto::{
|
||||
};
|
||||
use mail_parser::Message;
|
||||
|
||||
use super::resources_dir;
|
||||
|
||||
#[test]
|
||||
fn body_structure() {
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("resources");
|
||||
test_dir.push("imap");
|
||||
test_dir.push("messages");
|
||||
for file_name in fs::read_dir(&test_dir).unwrap() {
|
||||
for file_name in fs::read_dir(&resources_dir()).unwrap() {
|
||||
let mut file_name = file_name.as_ref().unwrap().path();
|
||||
if file_name.extension().map_or(true, |e| e != "txt") {
|
||||
continue;
|
||||
|
||||
290
tests/src/imap/condstore.rs
Normal file
290
tests/src/imap/condstore.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use crate::imap::{
|
||||
append::{assert_append_message, build_messages},
|
||||
AssertResult,
|
||||
};
|
||||
|
||||
use super::{ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
|
||||
// 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(&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::No)
|
||||
.await
|
||||
.assert_contains("* 1 FETCH")
|
||||
.assert_contains("UID 3)")
|
||||
.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)")
|
||||
.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");
|
||||
}
|
||||
118
tests/src/imap/copy_move.rs
Normal file
118
tests/src/imap/copy_move.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// Select INBOX
|
||||
imap.send("SELECT INBOX").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Copying to "All Mail" or the same mailbox should fail
|
||||
imap.send("COPY 1:* INBOX").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
||||
.await
|
||||
.assert_response_code("CANNOT");
|
||||
|
||||
imap.send("COPY 1:* \"All Mail\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
||||
.await
|
||||
.assert_response_code("CANNOT");
|
||||
|
||||
// Copying to a non-existent mailbox should fail
|
||||
imap.send("COPY 1:* \"/dev/null\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
||||
.await
|
||||
.assert_response_code("TRYCREATE");
|
||||
|
||||
// Create test folders
|
||||
imap.send("CREATE \"Scamorza Affumicata\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
imap.send("CREATE \"Burrata al Tartufo\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Copy messages
|
||||
imap.send("COPY 1,3,5,7 \"Scamorza Affumicata\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("COPYUID")
|
||||
.assert_contains("1:4");
|
||||
|
||||
// Check status
|
||||
imap.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE)")
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 4")
|
||||
.assert_contains("UNSEEN 4")
|
||||
.assert_contains("UIDNEXT 5")
|
||||
.assert_contains("SIZE 5851");
|
||||
|
||||
// Move all messages to Burrata
|
||||
imap.send("SELECT \"Scamorza Affumicata\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
imap.send("MOVE 1:* \"Burrata al Tartufo\"").await;
|
||||
imap.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.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
||||
.await;
|
||||
imap.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.send("SELECT \"Burrata al Tartufo\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
imap.send("MOVE 1:* \"Scamorza Affumicata\"").await;
|
||||
imap.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.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
||||
.await;
|
||||
imap.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)");
|
||||
}
|
||||
170
tests/src/imap/fetch.rs
Normal file
170
tests/src/imap/fetch.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// 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 EMAILID THREADID ",
|
||||
"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("THREADID (")
|
||||
.assert_contains("EMAILID (")
|
||||
.assert_contains("but then I thought, why not do both?")
|
||||
.assert_contains(concat!(
|
||||
"ENVELOPE (\"Sat, 20 Nov 2021 22:22:01 +0000\" ",
|
||||
"\"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) \"mixed\" (\"boundary\" \"giddyup\") NIL NIL NIL)",
|
||||
" 0 \"cdb0382a03a15601fb1b3c7422521620\" NIL NIL NIL) ",
|
||||
"\"mixed\" (\"boundary\" \"festivus\") NIL NIL NIL)"
|
||||
));
|
||||
|
||||
// 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] {101}")
|
||||
.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("ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨")
|
||||
.assert_contains("Vandelay");
|
||||
|
||||
// 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 ");
|
||||
|
||||
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.");
|
||||
}
|
||||
154
tests/src/imap/idle.rs
Normal file
154
tests/src/imap/idle.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
|
||||
// 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;
|
||||
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: test@domain.com\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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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");
|
||||
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");
|
||||
|
||||
// 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;
|
||||
}
|
||||
322
tests/src/imap/mailbox.rs
Normal file
322
tests/src/imap/mailbox.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(mut imap: &mut ImapConnection, mut imap_check: &mut ImapConnection) {
|
||||
// List folders
|
||||
imap.send("LIST \"\" \"*\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_folders(
|
||||
[
|
||||
("All Mail", ["NoInferiors"]),
|
||||
("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;
|
||||
for imap in [&mut imap, &mut imap_check] {
|
||||
imap.send("LIST \"\" \"*\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_folders(
|
||||
[
|
||||
("All Mail", ["NoInferiors"]),
|
||||
("INBOX", [""]),
|
||||
("Deleted Items", [""]),
|
||||
("Fruit", [""]),
|
||||
("Fruit/Apple", [""]),
|
||||
("Fruit/Apple/Green", [""]),
|
||||
("Tofu", [""]),
|
||||
],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Folders under All Mail should not be allowed
|
||||
imap.send("CREATE \"All Mail/Untitled\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No).await;
|
||||
|
||||
// Enable IMAP4rev2
|
||||
imap.send("ENABLE IMAP4rev2").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Create missing parent folders
|
||||
imap.send("CREATE \"/Vegetable/Broccoli\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("[MAILBOXID (");
|
||||
|
||||
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(
|
||||
[
|
||||
("All Mail", ["NoInferiors", "All"]),
|
||||
("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", ""]),
|
||||
("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(
|
||||
[
|
||||
("All Mail", ["NoInferiors", "All"]),
|
||||
("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 \"All Mail\"").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::No).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(
|
||||
[
|
||||
("All Mail", ["NoInferiors", "All"]),
|
||||
("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(
|
||||
[
|
||||
("All Mail", ["NoInferiors", "All"]),
|
||||
("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(
|
||||
[
|
||||
("All Mail", [""]),
|
||||
("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;
|
||||
}
|
||||
224
tests/src/imap/managesieve.rs
Normal file
224
tests/src/imap/managesieve.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
use super::AssertResult;
|
||||
|
||||
pub async fn test() {
|
||||
// Connect to ManageSieve
|
||||
let mut sieve = SieveConnection::connect().await;
|
||||
sieve
|
||||
.assert_read(ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("IMPLEMENTATION");
|
||||
|
||||
// Authenticate
|
||||
sieve
|
||||
.send("AUTHENTICATE \"PLAIN\" \"AGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0\"")
|
||||
.await;
|
||||
sieve
|
||||
.assert_read(ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MAXREDIRECTS");
|
||||
|
||||
// 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;
|
||||
sieve
|
||||
.send_literal(
|
||||
"PUTSCRIPT \"holidays\" ",
|
||||
"require \"vacation\"; vacation \"Gone fishin'\";\r\n",
|
||||
)
|
||||
.await;
|
||||
sieve.assert_read(ResponseType::Ok).await;
|
||||
sieve.send("PUTSCRIPT \"holidays\" \"discard;\"").await;
|
||||
sieve
|
||||
.assert_read(ResponseType::No)
|
||||
.await
|
||||
.assert_contains("ALREADYEXISTS");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
pub struct SieveConnection {
|
||||
reader: Lines<BufReader<ReadHalf<TcpStream>>>,
|
||||
writer: WriteHalf<TcpStream>,
|
||||
}
|
||||
|
||||
impl SieveConnection {
|
||||
pub async fn connect() -> Self {
|
||||
let (reader, writer) =
|
||||
tokio::io::split(TcpStream::connect("127.0.0.1:4190").await.unwrap());
|
||||
SieveConnection {
|
||||
reader: BufReader::new(reader).lines(),
|
||||
writer,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_read(&mut self, rt: ResponseType) -> Vec<String> {
|
||||
let lines = self.read().await;
|
||||
let mut buf = Vec::with_capacity(10);
|
||||
rt.serialize(&mut buf);
|
||||
if lines
|
||||
.last()
|
||||
.unwrap()
|
||||
.starts_with(&String::from_utf8(buf).unwrap())
|
||||
{
|
||||
lines
|
||||
} else {
|
||||
panic!("Expected {:?} from server but got: {:?}", rt, lines);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
loop {
|
||||
match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await {
|
||||
Ok(Ok(Some(line))) => {
|
||||
let is_done =
|
||||
line.starts_with("OK") || line.starts_with("NO") || line.starts_with("BYE");
|
||||
println!("<- {:?}", line);
|
||||
lines.push(line);
|
||||
if is_done {
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
Ok(Ok(None)) => {
|
||||
panic!("Invalid response: {:?}.", lines);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
panic!("Connection broken: {} ({:?})", err, lines);
|
||||
}
|
||||
Err(_) => panic!("Timeout while waiting for server response: {:?}", lines),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, text: &str) {
|
||||
println!("-> {:?}", text);
|
||||
self.writer.write_all(text.as_bytes()).await.unwrap();
|
||||
self.writer.write_all(b"\r\n").await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn send_raw(&mut self, text: &str) {
|
||||
println!("-> {:?}", text);
|
||||
self.writer.write_all(text.as_bytes()).await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn send_literal(&mut self, text: &str, literal: &str) {
|
||||
self.send(&format!("{}{{{}+}}\r\n{}", text, literal.len(), literal))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,630 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
pub mod acl;
|
||||
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 search;
|
||||
pub mod store;
|
||||
pub mod thread;
|
||||
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use ::managesieve::core::ManageSieveSessionManager;
|
||||
use directory::config::ConfigDirectory;
|
||||
use imap::core::{ImapSessionManager, IMAP};
|
||||
use imap_proto::ResponseType;
|
||||
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
||||
use smtp::core::SMTP;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf},
|
||||
net::TcpStream,
|
||||
sync::{mpsc, watch},
|
||||
};
|
||||
use utils::{config::ServerProtocol, UnwrapFailure};
|
||||
|
||||
use crate::{
|
||||
add_test_certs,
|
||||
directory::sql::{
|
||||
add_to_group_id, create_test_directory, create_test_user, create_test_user_with_email,
|
||||
},
|
||||
store::TempDir,
|
||||
};
|
||||
|
||||
const SERVER: &str = r#"
|
||||
[server]
|
||||
hostname = "imap.example.org"
|
||||
|
||||
[server.listener.imap]
|
||||
bind = ["127.0.0.1:9991"]
|
||||
protocol = "imap"
|
||||
max-connections = 81920
|
||||
|
||||
[server.listener.imaptls]
|
||||
bind = ["127.0.0.1:9992"]
|
||||
protocol = "imap"
|
||||
max-connections = 81920
|
||||
tls.implict = true
|
||||
|
||||
[server.listener.sieve]
|
||||
bind = ["127.0.0.1:4190"]
|
||||
protocol = "managesieve"
|
||||
max-connections = 81920
|
||||
|
||||
[server.socket]
|
||||
reuse-addr = true
|
||||
|
||||
[server.tls]
|
||||
enable = true
|
||||
implicit = false
|
||||
certificate = "default"
|
||||
|
||||
[session.ehlo]
|
||||
reject-non-fqdn = false
|
||||
|
||||
[session.rcpt]
|
||||
relay = [ { if = "authenticated-as", ne = "", then = true },
|
||||
{ else = false } ]
|
||||
directory = "sql"
|
||||
|
||||
[session.rcpt.errors]
|
||||
total = 5
|
||||
wait = "1ms"
|
||||
|
||||
[queue]
|
||||
path = "{TMP}"
|
||||
hash = 64
|
||||
|
||||
[report]
|
||||
path = "{TMP}"
|
||||
hash = 64
|
||||
|
||||
[resolver]
|
||||
type = "system"
|
||||
|
||||
[queue.outbound]
|
||||
next-hop = [ { if = "rcpt-domain", in-list = "local/domains", then = "local" },
|
||||
{ if = "rcpt-domain", in-list = "local/remote-domains", then = "mock-smtp" },
|
||||
{ else = false } ]
|
||||
|
||||
[remote."mock-smtp"]
|
||||
address = "localhost"
|
||||
port = 9999
|
||||
protocol = "smtp"
|
||||
|
||||
[remote."mock-smtp".tls]
|
||||
implicit = false
|
||||
allow-invalid-certs = true
|
||||
|
||||
[session.extensions]
|
||||
future-release = [ { if = "authenticated-as", ne = "", then = "99999999d"},
|
||||
{ else = false } ]
|
||||
|
||||
[store]
|
||||
db.path = "{TMP}/sqlite.db"
|
||||
|
||||
[store.blob]
|
||||
type = "local"
|
||||
|
||||
[store.blob.local]
|
||||
path = "{TMP}"
|
||||
|
||||
[certificate.default]
|
||||
cert = "file://{CERT}"
|
||||
private-key = "file://{PK}"
|
||||
|
||||
[jmap]
|
||||
directory = "sql"
|
||||
|
||||
[jmap.protocol]
|
||||
set.max-objects = 100000
|
||||
|
||||
[jmap.protocol.request]
|
||||
max-concurrent = 8
|
||||
|
||||
[jmap.protocol.upload]
|
||||
max-size = 5000000
|
||||
max-concurrent = 4
|
||||
ttl = "1m"
|
||||
|
||||
[jmap.protocol.upload.quota]
|
||||
files = 3
|
||||
size = 50000
|
||||
|
||||
[jmap.rate-limit]
|
||||
account.rate = "1000/1m"
|
||||
authentication.rate = "100/2s"
|
||||
anonymous.rate = "100/1m"
|
||||
|
||||
[jmap.event-source]
|
||||
throttle = "500ms"
|
||||
|
||||
[jmap.web-sockets]
|
||||
throttle = "500ms"
|
||||
|
||||
[jmap.push]
|
||||
throttle = "500ms"
|
||||
attempts.interval = "500ms"
|
||||
|
||||
[directory."sql"]
|
||||
type = "sql"
|
||||
address = "sqlite::memory:"
|
||||
|
||||
[directory."sql".pool]
|
||||
max-connections = 1
|
||||
|
||||
[directory."sql".query]
|
||||
login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'"
|
||||
name = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ?"
|
||||
id = "SELECT id, name, type, secret, description, quota FROM accounts WHERE id = ?"
|
||||
members = "SELECT gid FROM group_members WHERE uid = ?"
|
||||
recipients = "SELECT id FROM emails WHERE address = ?"
|
||||
emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC"
|
||||
verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5"
|
||||
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50"
|
||||
domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1"
|
||||
|
||||
[directory."sql".columns]
|
||||
name = "name"
|
||||
description = "description"
|
||||
secret = "secret"
|
||||
id = "id"
|
||||
email = "address"
|
||||
quota = "quota"
|
||||
type = "type"
|
||||
|
||||
[directory."local"]
|
||||
type = "memory"
|
||||
|
||||
[directory."local".lookup]
|
||||
domains = ["example.com"]
|
||||
remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"]
|
||||
|
||||
[oauth]
|
||||
key = "parerga_und_paralipomena"
|
||||
max-auth-attempts = 1
|
||||
|
||||
[oauth.expiry]
|
||||
user-code = "1s"
|
||||
token = "1s"
|
||||
refresh-token = "3s"
|
||||
refresh-token-renew = "2s"
|
||||
"#;
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct IMAPTest {
|
||||
jmap: Arc<JMAP>,
|
||||
imap: Arc<IMAP>,
|
||||
temp_dir: TempDir,
|
||||
shutdown_tx: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest {
|
||||
// Load and parse config
|
||||
let temp_dir = TempDir::new("imap_tests", delete_if_exists);
|
||||
let config = utils::config::Config::parse(
|
||||
&add_test_certs(SERVER).replace("{TMP}", &temp_dir.path.display().to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let servers = config.parse_servers().unwrap();
|
||||
let directory = config.parse_directory().unwrap();
|
||||
|
||||
// Start JMAP and SMTP servers
|
||||
servers.bind(&config);
|
||||
let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
|
||||
let smtp = SMTP::init(&config, &servers, &directory, delivery_tx)
|
||||
.await
|
||||
.failed("Invalid configuration file");
|
||||
let jmap = JMAP::init(&config, &directory, delivery_rx, smtp.clone())
|
||||
.await
|
||||
.failed("Invalid configuration file");
|
||||
let imap: Arc<IMAP> = IMAP::init(&config)
|
||||
.await
|
||||
.failed("Invalid configuration file");
|
||||
let shutdown_tx = servers.spawn(|server, shutdown_rx| {
|
||||
match &server.protocol {
|
||||
ServerProtocol::Jmap => {
|
||||
server.spawn(JmapSessionManager::new(jmap.clone()), shutdown_rx)
|
||||
}
|
||||
ServerProtocol::Imap => server.spawn(
|
||||
ImapSessionManager::new(jmap.clone(), imap.clone()),
|
||||
shutdown_rx,
|
||||
),
|
||||
ServerProtocol::ManageSieve => server.spawn(
|
||||
ManageSieveSessionManager::new(jmap.clone(), imap.clone()),
|
||||
shutdown_rx,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
});
|
||||
|
||||
// Create tables and test accounts
|
||||
create_test_directory(jmap.directory.as_ref()).await;
|
||||
create_test_user(jmap.directory.as_ref(), "admin", "secret", "Superuser").await;
|
||||
add_to_group_id(jmap.directory.as_ref(), "admin", 0).await;
|
||||
create_test_user_with_email(
|
||||
jmap.directory.as_ref(),
|
||||
"jdoe@example.com",
|
||||
"secret",
|
||||
"John Doe",
|
||||
)
|
||||
.await;
|
||||
create_test_user_with_email(
|
||||
jmap.directory.as_ref(),
|
||||
"jane.smith@example.com",
|
||||
"secret",
|
||||
"Jane Smith",
|
||||
)
|
||||
.await;
|
||||
create_test_user_with_email(
|
||||
jmap.directory.as_ref(),
|
||||
"foobar@example.com",
|
||||
"secret",
|
||||
"Bill Foobar",
|
||||
)
|
||||
.await;
|
||||
|
||||
if delete_if_exists {
|
||||
jmap.store.destroy().await;
|
||||
}
|
||||
|
||||
IMAPTest {
|
||||
jmap,
|
||||
imap,
|
||||
temp_dir,
|
||||
shutdown_tx,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn imap_tests() {
|
||||
// Prepare settings
|
||||
let delete = true;
|
||||
let handle = init_imap_tests(delete).await;
|
||||
|
||||
// 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
|
||||
for imap in [&mut imap, &mut imap_check] {
|
||||
imap.send("AUTHENTICATE PLAIN {32+}\r\nAGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0")
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
}
|
||||
|
||||
// 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).await;
|
||||
append::test(&mut imap, &mut imap_check).await;
|
||||
search::test(&mut imap, &mut imap_check).await;
|
||||
fetch::test(&mut imap, &mut imap_check).await;
|
||||
store::test(&mut imap, &mut imap_check).await;
|
||||
copy_move::test(&mut imap, &mut imap_check).await;
|
||||
thread::test(&mut imap, &mut imap_check).await;
|
||||
idle::test(&mut imap, &mut imap_check).await;
|
||||
condstore::test(&mut imap, &mut imap_check).await;
|
||||
acl::test(&mut imap, &mut imap_check).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;
|
||||
}
|
||||
|
||||
// Run ManageSieve tests
|
||||
managesieve::test().await;
|
||||
|
||||
// Remove test data
|
||||
if delete {
|
||||
handle.temp_dir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImapConnection {
|
||||
tag: &'static [u8],
|
||||
reader: Lines<BufReader<ReadHalf<TcpStream>>>,
|
||||
writer: WriteHalf<TcpStream>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Tagged,
|
||||
Untagged,
|
||||
Continuation,
|
||||
Status,
|
||||
}
|
||||
|
||||
impl ImapConnection {
|
||||
pub async fn connect(tag: &'static [u8]) -> Self {
|
||||
let (reader, writer) =
|
||||
tokio::io::split(TcpStream::connect("127.0.0.1:9991").await.unwrap());
|
||||
ImapConnection {
|
||||
tag,
|
||||
reader: BufReader::new(reader).lines(),
|
||||
writer,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_read(&mut self, t: Type, rt: ResponseType) -> Vec<String> {
|
||||
let lines = self.read(t).await;
|
||||
let mut buf = Vec::with_capacity(10);
|
||||
buf.extend_from_slice(match t {
|
||||
Type::Tagged => self.tag,
|
||||
Type::Untagged | Type::Status => b"* ",
|
||||
Type::Continuation => b"+ ",
|
||||
});
|
||||
if !matches!(t, Type::Continuation | Type::Status) {
|
||||
rt.serialize(&mut buf);
|
||||
}
|
||||
if lines
|
||||
.last()
|
||||
.unwrap()
|
||||
.starts_with(&String::from_utf8(buf).unwrap())
|
||||
{
|
||||
lines
|
||||
} else {
|
||||
panic!("Expected {:?}/{:?} from server but got: {:?}", t, rt, lines);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read(&mut self, t: Type) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
loop {
|
||||
match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await {
|
||||
Ok(Ok(Some(line))) => {
|
||||
let is_done = line.starts_with(match t {
|
||||
Type::Tagged => std::str::from_utf8(self.tag).unwrap(),
|
||||
Type::Untagged | Type::Status => "* ",
|
||||
Type::Continuation => "+ ",
|
||||
});
|
||||
println!("<- {:?}", line);
|
||||
lines.push(line);
|
||||
if is_done {
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
Ok(Ok(None)) => {
|
||||
panic!("Invalid response: {:?}.", lines);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
panic!("Connection broken: {} ({:?})", err, lines);
|
||||
}
|
||||
Err(_) => panic!("Timeout while waiting for server response: {:?}", lines),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, text: &str) {
|
||||
println!("-> {}{:?}", std::str::from_utf8(self.tag).unwrap(), text);
|
||||
self.writer.write_all(self.tag).await.unwrap();
|
||||
self.writer.write_all(text.as_bytes()).await.unwrap();
|
||||
self.writer.write_all(b"\r\n").await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn send_untagged(&mut self, text: &str) {
|
||||
println!("-> {:?}", text);
|
||||
self.writer.write_all(text.as_bytes()).await.unwrap();
|
||||
self.writer.write_all(b"\r\n").await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn send_raw(&mut self, text: &str) {
|
||||
println!("-> {:?}", text);
|
||||
self.writer.write_all(text.as_bytes()).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AssertResult: Sized {
|
||||
fn assert_folders<'x>(
|
||||
self,
|
||||
expected: impl IntoIterator<Item = (&'x str, impl IntoIterator<Item = &'x str>)>,
|
||||
match_all: bool,
|
||||
) -> Self;
|
||||
|
||||
fn assert_response_code(self, code: &str) -> Self;
|
||||
fn assert_contains(self, text: &str) -> Self;
|
||||
fn assert_count(self, text: &str, occurences: usize) -> Self;
|
||||
fn assert_equals(self, text: &str) -> Self;
|
||||
fn into_response_code(self) -> String;
|
||||
fn into_highest_modseq(self) -> String;
|
||||
fn into_uid_validity(self) -> String;
|
||||
fn into_append_uid(self) -> String;
|
||||
fn into_copy_uid(self) -> String;
|
||||
fn into_modseq(self) -> String;
|
||||
}
|
||||
|
||||
impl AssertResult for Vec<String> {
|
||||
fn assert_folders<'x>(
|
||||
self,
|
||||
expected: impl IntoIterator<Item = (&'x str, impl IntoIterator<Item = &'x str>)>,
|
||||
match_all: bool,
|
||||
) -> Self {
|
||||
let mut match_count = 0;
|
||||
'outer: for (mailbox_name, flags) in expected.into_iter() {
|
||||
for result in self.iter() {
|
||||
if result.contains(&format!("\"{}\"", mailbox_name)) {
|
||||
for flag in flags {
|
||||
if !flag.is_empty() && !result.contains(flag) {
|
||||
panic!("Expected mailbox {} to have flag {}", mailbox_name, flag);
|
||||
}
|
||||
}
|
||||
match_count += 1;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
panic!("Mailbox {} is not present.", mailbox_name);
|
||||
}
|
||||
if match_all && match_count != self.len() - 1 {
|
||||
panic!(
|
||||
"Expected {} mailboxes, but got {}",
|
||||
match_count,
|
||||
self.len() - 1
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_response_code(self, code: &str) -> Self {
|
||||
if !self.last().unwrap().contains(&format!("[{}]", code)) {
|
||||
panic!(
|
||||
"Response code {:?} not found, got {:?}",
|
||||
code,
|
||||
self.last().unwrap()
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_contains(self, text: &str) -> Self {
|
||||
for line in &self {
|
||||
if line.contains(text) {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
panic!("Expected response to contain {:?}, got {:?}", text, self);
|
||||
}
|
||||
|
||||
fn assert_count(self, text: &str, occurences: usize) -> Self {
|
||||
assert_eq!(
|
||||
self.iter().filter(|l| l.contains(text)).count(),
|
||||
occurences,
|
||||
"Expected {} occurrences of {:?}, found {}.",
|
||||
occurences,
|
||||
text,
|
||||
self.iter().filter(|l| l.contains(text)).count()
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_equals(self, text: &str) -> Self {
|
||||
for line in &self {
|
||||
if line == text {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
panic!("Expected response to be {:?}, got {:?}", text, self);
|
||||
}
|
||||
|
||||
fn into_response_code(self) -> String {
|
||||
if let Some((_, code)) = self.last().unwrap().split_once('[') {
|
||||
if let Some((code, _)) = code.split_once(']') {
|
||||
return code.to_string();
|
||||
}
|
||||
}
|
||||
panic!("No response code found in {:?}", self.last().unwrap());
|
||||
}
|
||||
|
||||
fn into_append_uid(self) -> String {
|
||||
if let Some((_, code)) = self.last().unwrap().split_once("[APPENDUID ") {
|
||||
if let Some((code, _)) = code.split_once(']') {
|
||||
if let Some((_, uid)) = code.split_once(' ') {
|
||||
return uid.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("No APPENDUID found in {:?}", self.last().unwrap());
|
||||
}
|
||||
|
||||
fn into_copy_uid(self) -> String {
|
||||
for line in &self {
|
||||
if let Some((_, code)) = line.split_once("[COPYUID ") {
|
||||
if let Some((code, _)) = code.split_once(']') {
|
||||
if let Some((_, uid)) = code.split_once(' ') {
|
||||
return uid.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("No COPYUID found in {:?}", self);
|
||||
}
|
||||
|
||||
fn into_highest_modseq(self) -> String {
|
||||
for line in &self {
|
||||
if let Some((_, value)) = line.split_once("HIGHESTMODSEQ ") {
|
||||
if let Some((value, _)) = value.split_once(']') {
|
||||
return value.to_string();
|
||||
} else if let Some((value, _)) = value.split_once(')') {
|
||||
return value.to_string();
|
||||
} else {
|
||||
panic!("No HIGHESTMODSEQ delimiter found in {:?}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("No HIGHESTMODSEQ entries found in {:?}", self);
|
||||
}
|
||||
|
||||
fn into_modseq(self) -> String {
|
||||
for line in &self {
|
||||
if let Some((_, value)) = line.split_once("MODSEQ (") {
|
||||
if let Some((value, _)) = value.split_once(')') {
|
||||
return value.to_string();
|
||||
} else {
|
||||
panic!("No MODSEQ delimiter found in {:?}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("No MODSEQ entries found in {:?}", self);
|
||||
}
|
||||
|
||||
fn into_uid_validity(self) -> String {
|
||||
for line in &self {
|
||||
if let Some((_, value)) = line.split_once("UIDVALIDITY ") {
|
||||
if let Some((value, _)) = value.split_once(']') {
|
||||
return value.to_string();
|
||||
} else if let Some((value, _)) = value.split_once(')') {
|
||||
return value.to_string();
|
||||
} else {
|
||||
panic!("No UIDVALIDITY delimiter found in {:?}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("No UIDVALIDITY entries found in {:?}", self);
|
||||
}
|
||||
}
|
||||
|
||||
fn resources_dir() -> PathBuf {
|
||||
let mut resources = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
resources.push("resources");
|
||||
resources.push("imap");
|
||||
resources
|
||||
}
|
||||
|
||||
138
tests/src/imap/search.rs
Normal file
138
tests/src/imap/search.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
|
||||
// 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("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("COUNT 10 ALL 6,4:5,1,3,7:8,10,2,9");
|
||||
}
|
||||
77
tests/src/imap/store.rs
Normal file
77
tests/src/imap/store.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use super::{AssertResult, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// 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);
|
||||
|
||||
// Store using saved searches
|
||||
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", 10)
|
||||
.assert_count("Answered", 0);
|
||||
}
|
||||
126
tests/src/imap/thread.rs
Normal file
126
tests/src/imap/thread.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart IMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::ResponseType;
|
||||
|
||||
use crate::imap::AssertResult;
|
||||
|
||||
use super::{append::build_messages, ImapConnection, Type};
|
||||
|
||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
// 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!(
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.into_append_uid(),
|
||||
format!("1:{}", messages.len()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain ThreadId and MessageId of the first message
|
||||
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 (EMAILID THREADID)").await;
|
||||
for line in imap.assert_read(Type::Tagged, ResponseType::Ok).await {
|
||||
if let Some((_, value)) = line.split_once("EMAILID (") {
|
||||
email_id = value
|
||||
.split_once(')')
|
||||
.expect("Missing delimiter")
|
||||
.0
|
||||
.to_string()
|
||||
.into();
|
||||
}
|
||||
if let Some((_, value)) = line.split_once("THREADID (") {
|
||||
thread_id = value
|
||||
.split_once(')')
|
||||
.expect("Missing delimiter")
|
||||
.0
|
||||
.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)");
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -655,7 +655,7 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
for id in [jane_id.id(), john_id.id()] {
|
||||
add_user_id_to_group_id(directory, id as u32, sales_id.id() as u32).await;
|
||||
}
|
||||
server.access_tokens.lock().clear();
|
||||
server.access_tokens.clear();
|
||||
john_client.refresh_session().await.unwrap();
|
||||
jane_client.refresh_session().await.unwrap();
|
||||
bill_client.refresh_session().await.unwrap();
|
||||
@@ -750,7 +750,7 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
|
||||
// Remove John from the sales group
|
||||
remove_from_group(directory, john_id.id() as u32, sales_id.id() as u32).await;
|
||||
server.sessions.lock().clear();
|
||||
server.sessions.clear();
|
||||
assert_forbidden(
|
||||
john_client
|
||||
.set_default_account_id(&sales_id.to_string())
|
||||
|
||||
@@ -53,8 +53,8 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.await;
|
||||
|
||||
// Reset rate limiters
|
||||
server.rate_limit_auth.lock().clear();
|
||||
server.rate_limit_unauth.lock().clear();
|
||||
server.rate_limit_auth.clear();
|
||||
server.rate_limit_unauth.clear();
|
||||
|
||||
// Incorrect passwords should be rejected with a 401 error
|
||||
assert!(matches!(
|
||||
|
||||
@@ -88,7 +88,9 @@ pub fn start_test_server(core: Arc<SMTP>, protocols: &[ServerProtocol]) -> watch
|
||||
server.spawn(smtp_manager.clone(), shutdown_rx)
|
||||
}
|
||||
ServerProtocol::Http => server.spawn(smtp_admin_manager.clone(), shutdown_rx),
|
||||
ServerProtocol::Imap | ServerProtocol::Jmap => unreachable!(),
|
||||
ServerProtocol::Imap | ServerProtocol::Jmap | ServerProtocol::ManageSieve => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user