IMAP Acl command, rate limiting and ManageSieve server.

This commit is contained in:
mdecimus
2023-06-29 18:51:26 +02:00
parent f5048da232
commit 29f3ca284b
217 changed files with 14864 additions and 366 deletions

291
tests/src/imap/acl.rs Normal file
View 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
View 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
View 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;
}

View File

@@ -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
View 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
View 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
View 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("&ldquo;exporting&rdquo;")
.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
View 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
View 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;
}

View 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;
}
}

View File

@@ -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
View 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
View 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
View 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);
}

View File

@@ -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())

View File

@@ -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!(

View File

@@ -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!()
}
};
})
}