This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 14:36:07 +01:00
1 parent fc81ecafd6
commit dc57aa7089
89 files changed
+5290 -1551

No files matched your search

+86 -9
View File
@@ -9,6 +9,7 @@ use crate::imap::client::ImapCredentials;
use crate::imap::connection::ImapStream;
use crate::imap::parse::{decode_rfc2047, flag_to_string, has_attachments, imap_address_to_email};
use crate::realtime::events::{EventBus, MailEvent};
use crate::search::engine::SearchEngine;
use crate::{db, imap};
pub struct IdleManager {
@@ -34,6 +35,7 @@ impl IdleManager {
creds: ImapCredentials,
event_bus: Arc<EventBus>,
config: Arc<AppConfig>,
search_engine: Arc<SearchEngine>,
) {
use dashmap::mapref::entry::Entry;
@@ -43,7 +45,15 @@ impl IdleManager {
}
Entry::Vacant(entry) => {
let handle = tokio::spawn(async move {
idle_loop(&user_hash, &folder, &creds, &event_bus, &config).await;
idle_loop(
&user_hash,
&folder,
&creds,
&event_bus,
&config,
&search_engine,
)
.await;
});
entry.insert(IdleTask {
handle,
@@ -94,12 +104,13 @@ async fn idle_loop(
creds: &ImapCredentials,
event_bus: &EventBus,
config: &AppConfig,
search_engine: &SearchEngine,
) {
let mut backoff = std::time::Duration::from_secs(1);
let max_backoff = std::time::Duration::from_secs(60);
loop {
match run_idle_session(user_hash, folder, creds, event_bus, config).await {
match run_idle_session(user_hash, folder, creds, event_bus, config, search_engine).await {
Ok(()) => {
tracing::info!(user_hash = %user_hash, folder = %folder, "IDLE session ended normally");
break;
@@ -125,6 +136,7 @@ async fn run_idle_session(
creds: &ImapCredentials,
event_bus: &EventBus,
config: &AppConfig,
search_engine: &SearchEngine,
) -> Result<(), String> {
use imap::client::connect;
@@ -173,7 +185,8 @@ async fn run_idle_session(
let result;
(session, result) =
fetch_new_after_idle(session, user_hash, folder, config).await;
fetch_new_after_idle(session, user_hash, folder, config, search_engine)
.await;
if result.new_count > 0 {
event_bus
@@ -256,6 +269,7 @@ async fn fetch_new_after_idle(
user_hash: &str,
folder: &str,
config: &AppConfig,
search_engine: &SearchEngine,
) -> (async_imap::Session<ImapStream>, ReconcileResult) {
let (cached_modseq, max_cached_uid, cached_count, cached_uid_validity, header_sync) =
match db::pool::open_user_db(&config.data_dir, user_hash) {
@@ -310,7 +324,7 @@ async fn fetch_new_after_idle(
}
if cached_modseq > 0 && server_modseq > 0 {
let fetch_query = format!("(UID FLAGS) (CHANGEDSINCE {})", cached_modseq);
let fetch_query = format!("(UID FLAGS) (CHANGEDSINCE {cached_modseq})");
let changed_items: Option<Vec<(u32, Vec<String>)>> =
match session.uid_fetch("1:*", &fetch_query).await {
Ok(stream) => {
@@ -396,8 +410,14 @@ async fn fetch_new_after_idle(
let mut deleted_count = 0u32;
if server_exists < cached_count {
deleted_count =
fetch_uids_and_remove_deleted(&mut session, user_hash, folder, config).await;
deleted_count = fetch_uids_and_remove_deleted(
&mut session,
user_hash,
folder,
config,
search_engine,
)
.await;
}
let (latest_uid, latest_sender, latest_subject) = if new_count > 0 {
@@ -514,7 +534,7 @@ async fn fetch_new_after_idle(
&& let Ok(conn) = db::pool::open_user_db(&config.data_dir, user_hash)
{
for uid in &deleted_uids {
if db::messages::delete_message(&conn, folder, *uid).is_ok() {
if delete_cached_message(&conn, search_engine, user_hash, folder, *uid) {
deleted_count += 1;
}
}
@@ -794,11 +814,55 @@ async fn fetch_and_store_headers(
new_count
}
fn delete_cached_message(
conn: &db::pool::Connection,
search_engine: &SearchEngine,
user_hash: &str,
folder: &str,
uid: u32,
) -> bool {
let transaction = match conn.unchecked_transaction() {
Ok(transaction) => transaction,
Err(error) => {
tracing::warn!(uid, folder = %folder, error = %error, "IDLE delete: failed to start transaction");
return false;
}
};
if let Err(error) = db::messages::delete_message(&transaction, folder, uid) {
tracing::warn!(uid, folder = %folder, error = %error, "IDLE delete: failed to remove cached message");
return false;
}
if search_engine.is_enabled() {
let index = match search_engine.open_user_index(user_hash) {
Ok(index) => index,
Err(error) => {
tracing::warn!(uid, folder = %folder, error = %error, "IDLE delete: failed to open search index");
return false;
}
};
if let Err(error) = index.delete_message_with_connection(&transaction, uid, folder) {
tracing::warn!(uid, folder = %folder, error = %error, "IDLE delete: failed to enqueue search deletion");
return false;
}
}
match transaction.commit() {
Ok(()) => true,
Err(error) => {
tracing::warn!(uid, folder = %folder, error = %error, "IDLE delete: failed to commit transaction");
false
}
}
}
async fn fetch_uids_and_remove_deleted(
session: &mut async_imap::Session<ImapStream>,
user_hash: &str,
folder: &str,
config: &AppConfig,
search_engine: &SearchEngine,
) -> u32 {
let imap_uids: Option<std::collections::HashSet<u32>> =
match session.uid_fetch("1:*", "UID").await {
@@ -840,7 +904,8 @@ async fn fetch_uids_and_remove_deleted(
if let Ok(conn) = db::pool::open_user_db(&config.data_dir, user_hash) {
let cached = db::messages::get_all_uids_and_flags(&conn, folder).unwrap_or_default();
for (uid, _) in &cached {
if !imap_uids.contains(uid) && db::messages::delete_message(&conn, folder, *uid).is_ok()
if !imap_uids.contains(uid)
&& delete_cached_message(&conn, search_engine, user_hash, folder, *uid)
{
deleted_count += 1;
}
@@ -859,6 +924,9 @@ mod tests {
let manager = IdleManager::new();
let config = Arc::new(serde_json::from_str::<AppConfig>("{}").unwrap());
let event_bus = Arc::new(EventBus::new());
let search_engine = Arc::new(SearchEngine::disabled(std::path::PathBuf::from(
"/tmp/rustbox-idle-test-search",
)));
let creds = ImapCredentials {
host: "localhost".into(),
port: 143,
@@ -876,6 +944,7 @@ mod tests {
creds.clone(),
event_bus.clone(),
config.clone(),
search_engine.clone(),
)
.await;
let original_task = manager.tasks.get(&key).unwrap().handle.id();
@@ -887,6 +956,7 @@ mod tests {
creds.clone(),
event_bus.clone(),
config.clone(),
search_engine.clone(),
)
.await;
assert_eq!(manager.tasks.len(), 1);
@@ -900,7 +970,14 @@ mod tests {
assert!(manager.tasks.is_empty());
manager
.start_idle(key.0.clone(), key.1.clone(), creds, event_bus, config)
.start_idle(
key.0.clone(),
key.1.clone(),
creds,
event_bus,
config,
search_engine,
)
.await;
assert_ne!(manager.tasks.get(&key).unwrap().handle.id(), original_task);
manager.stop_idle(&key.0, &key.1).await;