v0.2.0
This commit is contained in:
1 parent
fc81ecafd6
commit
dc57aa7089
89 files changed
+5290
-1551
No files matched your search
+86
-9
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user