Files
inbuxa-server/tests/src/imap/acl.rs
T
jcoffey-dev 58d2804278
github/ci (branch) GitHub Actions
ci / github (pull_request) Skipped
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Canceled after 25m26s
Don't let a group's members share its calendars, address books or files
#146 stopped a group's members sharing its mailboxes on. The same
shortcut lets them through everywhere else a group owns things: a
member counts as the account's owner, so Calendar/set, AddressBook/set
and FileNode/set skip the share check, and so does the WebDAV ACL
method. Who has what a group owns is decided by who is in the group.

For a member through a group only (is_group_member_only):

- Calendar/set, AddressBook/set and FileNode/set refuse a shareWith
  change as forbidden, on create and update; for files at the top of
  the account too, not only inside a folder;
- the DAV ACL method answers 403 on the group's calendars, address
  books and files;
- myRights reports mayShare false (JmapRights::owner_rights), and the
  DAV current-user-privilege-set leaves out all and write-acl.

Reading who something is shared with is unchanged, as in JMAP.

Tests: a new jmap::group_share module has a member create with a
share, create without one (and check myRights), share afterwards, and
an outsider reach each kind; the WebDAV ACL test has a member try the
ACL method on the group's folders; the IMAP ACL test now checks #146's
SETACL refusal, which had no test of its own. jmap_tests, webdav_tests
and imap_tests pass (RocksDB). specs/multi-account.md MA-D0.
2026-10-05 15:03:15 -07:00

403 lines
13 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use super::{AssertResult, ImapConnection, Type, append::assert_append_message};
use crate::utils::{server::TestServer, smtp::SmtpConnection};
use imap_proto::ResponseType;
pub async fn test(
mut imap_john: &mut ImapConnection,
_imap_check: &mut ImapConnection,
test: &TestServer,
) {
// Delivery to support account
println!("Running ACL tests...");
let mut lmtp = SmtpConnection::connect().await;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
// Connect to all test accounts
let mut imap_jane = test.account("[email protected]").imap_client().await;
let mut imap_bill = test.account("[email protected]").imap_client().await;
// Jane should see the Support account
imap_jane.send("LIST \"\" \"*\"").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("Shared Folders/[email protected]/INBOX");
imap_jane
.send("SELECT \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("FETCH 1 (PREVIEW)").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("TPS reports ASAP");
imap_jane.send("UNSELECT").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
// Jane should be able to create folders under the Support account
imap_jane
.send("CREATE \"Shared Folders/[email protected]/inbox/Jane's Folder\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("LIST \"\" \"*\"").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals(
"* LIST () \"/\" \"Shared Folders/[email protected]/INBOX/Jane's Folder\"",
);
imap_jane
.send("DELETE \"Shared Folders/[email protected]/INBOX/Jane's Folder\"")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
// inbuxa: MA-D0: but she can't share the group's mailbox on
imap_jane
.send("SETACL \"Shared Folders/[email protected]/INBOX\" [email protected] lr")
.await;
imap_jane
.assert_read(Type::Tagged, ResponseType::No)
.await
.assert_contains("NOPERM");
// John should have no shared folders
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 0);
imap_john.send("NAMESPACE").await;
imap_john.assert_read(Type::Tagged, ResponseType::Ok).await;
// List rights
imap_jane.send("LISTRIGHTS INBOX [email protected]").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* LISTRIGHTS \"INBOX\" \"[email protected]\" r l ws i et k x p a");
// Jane shares her Inbox to John, expect a Shared Folders item in John's list
imap_jane.send("SETACL INBOX [email protected] lr").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders\"")
.assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders/[email protected]\"")
.assert_equals("* LIST () \"/\" \"Shared Folders/[email protected]/INBOX\"");
// Grant access to Bill and check ACLs
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl");
imap_jane
.send("SETACL INBOX [email protected] lrxtws")
.await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl")
.assert_contains("\"[email protected]\" tewsrxl");
imap_bill.send("LIST \"\" \"*\"").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("Shared Folders/[email protected]/INBOX");
// Namespace should now return the Shared Folders namespace
imap_john.send("NAMESPACE").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* NAMESPACE ((\"\" \"/\")) ((\"Shared Folders\" \"/\")) NIL");
// List John's right on Jane's Inbox
imap_john
.send("MYRIGHTS \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* MYRIGHTS \"Shared Folders/[email protected]/INBOX\" rl");
// John should not be able to append messages
assert_append_message(
imap_john,
"Shared Folders/[email protected]/INBOX",
"From: john\n\ncontents",
ResponseType::No,
)
.await;
// Grant insert access to John on Jane's Inbox, and try inserting the
// message again.
imap_jane.send("SETACL INBOX [email protected] +i").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john
.send("MYRIGHTS \"Shared Folders/[email protected]/INBOX\"")
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_equals("* MYRIGHTS \"Shared Folders/[email protected]/INBOX\" rli");
assert_append_message(
imap_john,
"Shared Folders/[email protected]/INBOX",
"From: john\n\ncontents",
ResponseType::Ok,
)
.await;
// Only Bill should be allowed to delete messages on Jane's Inbox
for imap in [&mut imap_john, &mut imap_bill] {
imap.send("SELECT \"Shared Folders/[email protected]/INBOX\"")
.await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
}
imap_john.send("UID STORE 1 +FLAGS (\\Deleted)").await;
imap_john.assert_read(Type::Tagged, ResponseType::No).await;
imap_bill.send("UID STORE 1 +FLAGS (\\Deleted)").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_john.send("UID EXPUNGE").await;
imap_john.assert_read(Type::Tagged, ResponseType::No).await;
imap_john.send("UID FETCH 1 (PREVIEW)").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("contents");
imap_bill.send("UID EXPUNGE").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill.send("UID FETCH 1 (PREVIEW)").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("contents", 0);
imap_bill
.send("STATUS \"Shared Folders/[email protected]/INBOX\" (MESSAGES)")
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("(MESSAGES 0)");
// Test copying and moving between shared mailboxes
let uid = assert_append_message(
imap_john,
"INBOX",
"From: john\n\ncopy test",
ResponseType::Ok,
)
.await
.into_append_uid();
imap_john.send("SELECT INBOX").await;
imap_john.assert_read(Type::Tagged, ResponseType::Ok).await;
// Copy from John's Inbox to Jane's Inbox
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid
))
.await;
let uid = imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
// Check that both Bill and Jane can see the message
imap_bill.send("NOOP").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
imap_jane.send("SELECT INBOX").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
// Bill now moves the message to his own Inbox
imap_bill.send(&format!("UID MOVE {} INBOX", uid)).await;
let uid_moved = imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
// Both Jane and Bill should not see the message on Jane's Inbox anymore
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("copy test", 0);
imap_jane
.send(&format!("UID FETCH {} (PREVIEW)", uid))
.await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("copy test", 0);
// Check that the message has been moved to Bill's Inbox
imap_bill.send("SELECT INBOX").await;
imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_bill
.send(&format!("UID FETCH {} (PREVIEW)", uid_moved))
.await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("copy test");
// Repeating a cross-account copy returns the existing destination UID
// instead of creating a duplicate, and moving a message that the destination
// account already holds still expunges the source.
let uid_dedup = assert_append_message(
imap_john,
"INBOX",
concat!(
"Message-ID: <[email protected]>\n",
"From: john\n",
"Subject: dedup\n",
"\n",
"dedup test"
),
ResponseType::Ok,
)
.await
.into_append_uid();
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
let uid_dedup_dest = imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid();
imap_john
.send(&format!(
"UID COPY {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
assert_eq!(
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid(),
uid_dedup_dest
);
imap_jane.send("NOOP").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("UID FETCH 1:* (PREVIEW)").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("dedup test", 1);
imap_john
.send(&format!(
"UID MOVE {} \"Shared Folders/[email protected]/INBOX\"",
uid_dedup
))
.await;
assert_eq!(
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.into_copy_uid(),
uid_dedup_dest
);
imap_john
.send(&format!("UID FETCH {} (PREVIEW)", uid_dedup))
.await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("dedup test", 0);
// Jane stops sharing with Bill, and removes Insert access to John
imap_jane.send("DELETEACL INBOX [email protected]").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("SETACL INBOX [email protected] -i").await;
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
imap_jane.send("GETACL INBOX").await;
imap_jane
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_contains("\"[email protected]\" rl")
.assert_count("[email protected]", 0);
// Bill should not have access to Jane's Inbox anymore
imap_bill.send("LIST \"\" \"*\"").await;
imap_bill
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 0);
// And John should still have access
imap_john.send("LIST \"\" \"*\"").await;
imap_john
.assert_read(Type::Tagged, ResponseType::Ok)
.await
.assert_count("Shared Folders", 3);
}