#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.
292 lines
8.1 KiB
Rust
292 lines
8.1 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 crate::utils::server::TestServerBuilder;
|
|
use registry::{
|
|
schema::{
|
|
enums::{MtaProtocol, Permission},
|
|
properties::Property,
|
|
structs::{
|
|
CalendarAlarm, Expression, ExpressionMatch, Imap, Jmap, MtaExtensions,
|
|
MtaOutboundStrategy, MtaRoute, MtaRouteRelay, MtaStageAuth, Sharing,
|
|
},
|
|
},
|
|
types::list::List,
|
|
};
|
|
|
|
pub mod calendar;
|
|
pub mod compliance;
|
|
pub mod contacts;
|
|
pub mod core;
|
|
pub mod files;
|
|
pub mod group_share;
|
|
pub mod mail;
|
|
pub mod principal;
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
pub async fn jmap_tests() {
|
|
let mut test = TestServerBuilder::new("jmap_tests")
|
|
.await
|
|
.with_default_listeners()
|
|
.await
|
|
.build()
|
|
.await;
|
|
|
|
// Create admin account
|
|
let admin = test.create_admin_account("[email protected]").await;
|
|
|
|
// Create test users
|
|
for (name, secret, description, aliases) in [
|
|
(
|
|
"[email protected]",
|
|
"12345 + extra safety",
|
|
"John Doe",
|
|
&["[email protected]"][..],
|
|
),
|
|
(
|
|
"[email protected]",
|
|
"abcde + extra safety",
|
|
"Jane Smith",
|
|
&["[email protected]"],
|
|
),
|
|
(
|
|
"[email protected]",
|
|
"098765 + extra safety",
|
|
"Bill Foobar",
|
|
&["[email protected]"],
|
|
),
|
|
(
|
|
"[email protected]",
|
|
"aabbcc + extra safety",
|
|
"Robert Foobar",
|
|
&[][..],
|
|
),
|
|
] {
|
|
let account = admin
|
|
.create_user_account(
|
|
name,
|
|
secret,
|
|
description,
|
|
aliases,
|
|
vec![Permission::UnlimitedRequests, Permission::UnlimitedUploads],
|
|
)
|
|
.await;
|
|
test.insert_account(account);
|
|
}
|
|
|
|
// Create test group
|
|
test.insert_account(
|
|
admin
|
|
.create_group_account("[email protected]", "Sales Group", &[])
|
|
.await,
|
|
);
|
|
|
|
// Add test settings
|
|
admin
|
|
.registry_create_object(Imap {
|
|
allow_plain_text_auth: true,
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin
|
|
.registry_update_setting(
|
|
Jmap {
|
|
set_max_objects: 100_000,
|
|
get_max_results: 100_000,
|
|
event_source_throttle: 500u64.into(),
|
|
push_throttle: 500u64.into(),
|
|
websocket_throttle: 500u64.into(),
|
|
push_attempt_wait: 500u64.into(),
|
|
push_verify_timeout: 5000u64.into(),
|
|
..Default::default()
|
|
},
|
|
&[
|
|
Property::SetMaxObjects,
|
|
Property::GetMaxResults,
|
|
Property::EventSourceThrottle,
|
|
Property::PushThrottle,
|
|
Property::WebsocketThrottle,
|
|
Property::PushAttemptWait,
|
|
Property::PushVerifyTimeout,
|
|
],
|
|
)
|
|
.await;
|
|
admin
|
|
.registry_create_object(MtaStageAuth {
|
|
require: Expression {
|
|
else_: "false".to_string(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin
|
|
.registry_create_object(CalendarAlarm {
|
|
min_trigger_interval: 1000u64.into(),
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin
|
|
.registry_create_object(Sharing {
|
|
allow_directory_queries: true,
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin
|
|
.registry_create_object(MtaOutboundStrategy {
|
|
route: Expression {
|
|
match_: List::from_iter([
|
|
ExpressionMatch {
|
|
if_: "rcpt_domain == 'example.com'".into(),
|
|
then: "'local'".into(),
|
|
},
|
|
ExpressionMatch {
|
|
if_: concat!(
|
|
"contains(['remote.org', 'foobar.com', ",
|
|
"'test.com', 'other_domain.com'], rcpt_domain)"
|
|
)
|
|
.into(),
|
|
then: "'mock-smtp'".into(),
|
|
},
|
|
]),
|
|
else_: "'mx'".to_string(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin
|
|
.registry_create_object(MtaRoute::Relay(MtaRouteRelay {
|
|
address: "127.0.0.1".into(),
|
|
port: 9999,
|
|
allow_invalid_certs: true,
|
|
implicit_tls: false,
|
|
name: "mock-smtp".into(),
|
|
protocol: MtaProtocol::Smtp,
|
|
..Default::default()
|
|
}))
|
|
.await;
|
|
admin
|
|
.registry_create_object(MtaExtensions {
|
|
future_release: Expression {
|
|
match_: List::from_iter([ExpressionMatch {
|
|
if_: "!is_empty(authenticated_as)".into(),
|
|
then: "99999999d".into(),
|
|
}]),
|
|
else_: "false".to_string(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
admin.reload_settings().await;
|
|
|
|
test.insert_account(admin);
|
|
|
|
mail::get::test(&test).await;
|
|
mail::set::test(&test).await;
|
|
mail::parse::test(&test).await;
|
|
mail::query::test(&test).await;
|
|
mail::search_snippet::test(&test).await;
|
|
mail::changes::test(&test).await;
|
|
mail::query_changes::test(&test).await;
|
|
mail::copy::test(&test).await;
|
|
mail::thread_get::test(&test).await;
|
|
mail::thread_merge::test(&test).await;
|
|
mail::mailbox::test(&test).await;
|
|
mail::acl::test(&test).await;
|
|
mail::sieve_script::test(&test).await;
|
|
mail::vacation_response::test(&test).await;
|
|
mail::submission::test(&test).await;
|
|
|
|
core::event_source::test(&test).await;
|
|
core::websocket::test(&test).await;
|
|
core::push_subscription::test(&test).await;
|
|
core::blob::test(&test).await;
|
|
|
|
contacts::addressbook::test(&test).await;
|
|
contacts::contact::test(&test).await;
|
|
contacts::acl::test(&test).await;
|
|
|
|
files::node::test(&test).await;
|
|
files::acl::test(&test).await;
|
|
|
|
calendar::calendars::test(&test).await;
|
|
calendar::event::test(&test).await;
|
|
calendar::instance::test(&test).await;
|
|
calendar::notification::test(&test).await;
|
|
calendar::alarm::test(&test).await;
|
|
|
|
calendar::identity::test(&test).await;
|
|
calendar::acl::test(&test).await;
|
|
group_share::test(&test).await;
|
|
|
|
principal::get::test(&test).await;
|
|
principal::availability::test(&test).await;
|
|
|
|
compliance::test(&test).await;
|
|
|
|
if test.is_reset() {
|
|
test.temp_dir.delete();
|
|
}
|
|
}
|
|
|
|
pub fn find_values(string: &str, name: &str) -> Vec<String> {
|
|
let mut last_pos = 0;
|
|
let mut values = Vec::new();
|
|
|
|
while let Some(pos) = string[last_pos..].find(name) {
|
|
let mut value = string[last_pos + pos + name.len()..]
|
|
.split('"')
|
|
.nth(1)
|
|
.unwrap();
|
|
if value.ends_with('\\') {
|
|
value = &value[..value.len() - 1];
|
|
}
|
|
values.push(value.to_string());
|
|
last_pos += pos + name.len();
|
|
}
|
|
|
|
values
|
|
}
|
|
|
|
pub fn replace_values(mut string: String, find: &[String], replace: &[String]) -> String {
|
|
for (find, replace) in find.iter().zip(replace.iter()) {
|
|
string = string.replace(find, replace);
|
|
}
|
|
string
|
|
}
|
|
|
|
pub fn replace_boundaries(string: String) -> String {
|
|
let values = find_values(&string, "boundary=");
|
|
if !values.is_empty() {
|
|
replace_values(
|
|
string,
|
|
&values,
|
|
&(0..values.len())
|
|
.map(|i| format!("boundary_{}", i))
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
} else {
|
|
string
|
|
}
|
|
}
|
|
|
|
pub fn replace_blob_ids(string: String) -> String {
|
|
let values = find_values(&string, "blobId\":");
|
|
if !values.is_empty() {
|
|
replace_values(
|
|
string,
|
|
&values,
|
|
&(0..values.len())
|
|
.map(|i| format!("blob_{}", i))
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
} else {
|
|
string
|
|
}
|
|
}
|