Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utils::{
|
||||
imap::{AssertResult, ImapConnection, Type},
|
||||
server::TestServer,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use common::Server;
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess},
|
||||
mailbox::{INBOX_ID, JUNK_ID, TRASH_ID},
|
||||
};
|
||||
use imap_proto::ResponseType;
|
||||
use registry::schema::{
|
||||
enums::{TaskAccountMaintenanceType, TaskStoreMaintenanceType},
|
||||
prelude::Property,
|
||||
structs::{
|
||||
DataRetention, SpamClassifier, Task, TaskAccountMaintenance, TaskStatus,
|
||||
TaskStoreMaintenance,
|
||||
},
|
||||
};
|
||||
use store::{IterateParams, LogKey, U32_LEN, U64_LEN, write::key::DeserializeBigEndian};
|
||||
use types::id::Id;
|
||||
|
||||
const EXPUNGE_TRASH_AFTER: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
println!("Running Account purge tests...");
|
||||
let inbox_id = Id::from(INBOX_ID).to_string();
|
||||
let trash_id = Id::from(TRASH_ID).to_string();
|
||||
let junk_id = Id::from(JUNK_ID).to_string();
|
||||
let admin = test.account("[email protected]");
|
||||
|
||||
// Set test settings
|
||||
admin
|
||||
.registry_update_setting(
|
||||
DataRetention {
|
||||
max_changes_history: Some(1),
|
||||
expunge_trash_after: Some(EXPUNGE_TRASH_AFTER.into()),
|
||||
..Default::default()
|
||||
},
|
||||
&[Property::MaxChangesHistory, Property::ExpungeTrashAfter],
|
||||
)
|
||||
.await;
|
||||
admin
|
||||
.registry_update_setting(
|
||||
SpamClassifier {
|
||||
hold_samples_for: 1u64.into(),
|
||||
..Default::default()
|
||||
},
|
||||
&[Property::HoldSamplesFor],
|
||||
)
|
||||
.await;
|
||||
admin.reload_settings().await;
|
||||
|
||||
// Create test account
|
||||
let account = test
|
||||
.create_user_account(
|
||||
"[email protected]",
|
||||
"[email protected]",
|
||||
"this is a very strong password",
|
||||
&[],
|
||||
"[email protected]",
|
||||
)
|
||||
.await;
|
||||
let client = account.jmap_client().await;
|
||||
|
||||
let mut imap = ImapConnection::connect(b"_x ").await;
|
||||
imap.assert_read(Type::Untagged, ResponseType::Ok).await;
|
||||
imap.authenticate("[email protected]", "this is a very strong password")
|
||||
.await;
|
||||
imap.send("STATUS INBOX (UIDNEXT MESSAGES UNSEEN)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 0");
|
||||
|
||||
// Create test messages
|
||||
let mut message_ids = Vec::new();
|
||||
let mut pass = 0;
|
||||
let mut changes = AHashSet::new();
|
||||
|
||||
loop {
|
||||
pass += 1;
|
||||
for folder_id in [&inbox_id, &trash_id, &junk_id] {
|
||||
message_ids.push(
|
||||
client
|
||||
.email_import(
|
||||
format!(
|
||||
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."
|
||||
),
|
||||
pass, folder_id
|
||||
)
|
||||
.into_bytes(),
|
||||
[folder_id],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id(),
|
||||
);
|
||||
}
|
||||
|
||||
if pass == 1 {
|
||||
let (changes_, is_truncated) = get_changes(&test.server).await;
|
||||
assert!(!is_truncated);
|
||||
changes = changes_;
|
||||
tokio::time::sleep(EXPUNGE_TRASH_AFTER + std::time::Duration::from_secs(1)).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check IMAP status
|
||||
imap.send("LIST \"\" \"*\" RETURN (STATUS (MESSAGES))")
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("\"INBOX\" (MESSAGES 2)")
|
||||
.assert_contains("\"Deleted Items\" (MESSAGES 2)")
|
||||
.assert_contains("\"Junk Mail\" (MESSAGES 2)");
|
||||
|
||||
// Make sure both messages and changes are present
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_cached_messages(account.id().document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.emails
|
||||
.items
|
||||
.len(),
|
||||
6
|
||||
);
|
||||
|
||||
// Purge junk/trash messages and old changes
|
||||
admin
|
||||
.registry_create_object(Task::AccountMaintenance(TaskAccountMaintenance {
|
||||
account_id: account.id(),
|
||||
maintenance_type: TaskAccountMaintenanceType::Purge,
|
||||
status: TaskStatus::now(),
|
||||
}))
|
||||
.await;
|
||||
test.wait_for_tasks().await;
|
||||
let cache = test
|
||||
.server
|
||||
.get_cached_messages(account.id().document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Only 4 messages should remain
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_cached_messages(account.id().document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.emails
|
||||
.items
|
||||
.len(),
|
||||
4
|
||||
);
|
||||
assert_eq!(cache.in_mailbox(INBOX_ID).count(), 2);
|
||||
assert_eq!(cache.in_mailbox(TRASH_ID).count(), 1);
|
||||
assert_eq!(cache.in_mailbox(JUNK_ID).count(), 1);
|
||||
|
||||
// Check IMAP status
|
||||
imap.send("LIST \"\" \"*\" RETURN (STATUS (MESSAGES))")
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("\"INBOX\" (MESSAGES 2)")
|
||||
.assert_contains("\"Deleted Items\" (MESSAGES 1)")
|
||||
.assert_contains("\"Junk Mail\" (MESSAGES 1)");
|
||||
|
||||
// Compare changes
|
||||
let (new_changes, is_truncated) = get_changes(&test.server).await;
|
||||
assert!(!changes.is_empty());
|
||||
assert!(!new_changes.is_empty());
|
||||
assert!(is_truncated);
|
||||
for change in &changes {
|
||||
assert!(
|
||||
!new_changes.contains(change),
|
||||
"Change {change:?} was not purged, expected {} changes, got {}",
|
||||
changes.len(),
|
||||
new_changes.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Delete expired training samples
|
||||
admin
|
||||
.registry_create_object(Task::StoreMaintenance(TaskStoreMaintenance {
|
||||
maintenance_type: TaskStoreMaintenanceType::PurgeBlob,
|
||||
shard_index: None,
|
||||
status: TaskStatus::now(),
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Delete account
|
||||
admin.destroy_account(account).await;
|
||||
test.wait_for_tasks().await;
|
||||
test.assert_is_empty().await;
|
||||
|
||||
// Reset settings
|
||||
admin
|
||||
.registry_update_setting(SpamClassifier::default(), &[Property::HoldSamplesFor])
|
||||
.await;
|
||||
test.cleanup().await;
|
||||
}
|
||||
|
||||
async fn get_changes(server: &Server) -> (AHashSet<(u64, u8)>, bool) {
|
||||
let mut changes = AHashSet::new();
|
||||
let mut is_truncated = false;
|
||||
server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
LogKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
change_id: 0,
|
||||
},
|
||||
LogKey {
|
||||
account_id: u32::MAX,
|
||||
collection: u8::MAX,
|
||||
change_id: u64::MAX,
|
||||
},
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
if !value.is_empty() {
|
||||
changes.insert((
|
||||
key.deserialize_be_u64(key.len() - U64_LEN).unwrap(),
|
||||
key[U32_LEN],
|
||||
));
|
||||
} else {
|
||||
is_truncated = true;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
(changes, is_truncated)
|
||||
}
|
||||
Reference in New Issue
Block a user