use std::path::Path;
use std::time::{Duration, SystemTime};
use walkdir::WalkDir;
pub async fn compact_trash(base_path: &Path, older_than: Duration) -> anyhow::Result<usize> {
let mailboxes_dir = base_path.join("mailboxes");
if !mailboxes_dir.exists() {
return Ok(0);
}
let now = SystemTime::now();
let mut removed = 0usize;
let mailboxes_dir_clone = mailboxes_dir.clone();
let older_than_clone = older_than;
let paths_to_remove: Vec<std::path::PathBuf> =
tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<std::path::PathBuf>> {
let mut candidates = Vec::new();
for entry in WalkDir::new(&mailboxes_dir_clone)
.min_depth(2)
.max_depth(3)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let in_trash = entry.path().components().any(|c| c.as_os_str() == ".Trash");
if !in_trash {
continue;
}
let metadata = match entry.metadata() {
Ok(m) => m,
Err(e) => {
tracing::warn!("Failed to stat {:?}: {}", entry.path(), e);
continue;
}
};
let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
if let Ok(age) = now.duration_since(mtime) {
if age >= older_than_clone {
candidates.push(entry.path().to_path_buf());
}
}
}
Ok(candidates)
})
.await
.map_err(|e| anyhow::anyhow!("spawn_blocking failed: {}", e))??;
for path in paths_to_remove {
match tokio::fs::remove_file(&path).await {
Ok(()) => {
tracing::debug!("Compacted expunged message: {:?}", path);
removed += 1;
}
Err(e) => {
tracing::warn!("Failed to remove {:?}: {}", path, e);
}
}
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::SystemTime;
#[tokio::test]
async fn test_compact_trash_empty_dir() {
let dir = tempfile::tempdir().expect("tempdir");
let count = compact_trash(dir.path(), Duration::from_secs(0)).await;
assert!(count.is_ok());
assert_eq!(count.unwrap(), 0);
}
#[tokio::test]
async fn test_compact_trash_removes_old_files() {
let dir = tempfile::tempdir().expect("tempdir");
let mailbox_dir = dir
.path()
.join("mailboxes")
.join("test-mailbox")
.join(".Trash");
tokio::fs::create_dir_all(&mailbox_dir).await.unwrap();
let msg_path = mailbox_dir.join("old-message");
tokio::fs::write(&msg_path, b"deleted msg").await.unwrap();
let file = std::fs::File::open(&msg_path).unwrap();
let _two_hours_ago = SystemTime::now()
.checked_sub(Duration::from_secs(7200))
.unwrap_or(SystemTime::UNIX_EPOCH);
drop(file);
let count = compact_trash(dir.path(), Duration::from_secs(0))
.await
.unwrap();
assert_eq!(count, 1, "Should have removed 1 file");
assert!(!msg_path.exists(), "File should be gone");
}
#[tokio::test]
async fn test_compact_trash_ignores_non_trash() {
let dir = tempfile::tempdir().expect("tempdir");
let cur_dir = dir
.path()
.join("mailboxes")
.join("test-mailbox")
.join("cur");
tokio::fs::create_dir_all(&cur_dir).await.unwrap();
tokio::fs::write(cur_dir.join("message"), b"live msg")
.await
.unwrap();
let count = compact_trash(dir.path(), Duration::from_secs(0))
.await
.unwrap();
assert_eq!(count, 0, "Should not remove files outside .Trash");
}
}