diff --git a/crates/flectar-mail-core/src/db/migrations.rs b/crates/flectar-mail-core/src/db/migrations.rs index 2822b056..5c9b1e13 100644 --- a/crates/flectar-mail-core/src/db/migrations.rs +++ b/crates/flectar-mail-core/src/db/migrations.rs @@ -48,6 +48,7 @@ const MIGRATIONS: &[Migration] = &[ Migration::Plain(include_str!("migrations/029_mail_transfers.sql")), Migration::Plain(include_str!("migrations/030_folder_server_only_unseen.sql")), Migration::Plain(include_str!("migrations/031_notification_tracking.sql")), + Migration::Plain(include_str!("migrations/032_account_caught_up_at.sql")), ]; pub const LATEST_VERSION: i64 = MIGRATIONS.len() as i64; diff --git a/crates/flectar-mail-core/src/db/migrations/032_account_caught_up_at.sql b/crates/flectar-mail-core/src/db/migrations/032_account_caught_up_at.sql new file mode 100644 index 00000000..0a85efc3 --- /dev/null +++ b/crates/flectar-mail-core/src/db/migrations/032_account_caught_up_at.sql @@ -0,0 +1,5 @@ +-- When the account's last complete sync started: everything its server had +-- received by then is stored here. Mail that arrives after it, while no copy +-- of the app is following the account, is what a check in the background +-- tells the user about. +ALTER TABLE accounts ADD COLUMN caught_up_at INTEGER; diff --git a/crates/flectar-mail-core/src/db/repo/accounts.rs b/crates/flectar-mail-core/src/db/repo/accounts.rs index 45a3ef6a..8afb22cd 100644 --- a/crates/flectar-mail-core/src/db/repo/accounts.rs +++ b/crates/flectar-mail-core/src/db/repo/accounts.rs @@ -555,6 +555,16 @@ pub fn update_sync_state_with_error( )? != 0) } +/// Note that a sync of the account that started at `started` stored all the +/// mail its Inbox had by then. +pub fn set_caught_up(conn: &Connection, id: i64, started: i64) -> Result<()> { + conn.execute( + "UPDATE accounts SET caught_up_at = ?2 WHERE id = ?1", + params![id, started], + )?; + Ok(()) +} + /// Move one account before or after another and compact the persisted order. /// A complete rewrite keeps ordering deterministic after any number of moves /// and avoids fragile fractional positions. diff --git a/crates/flectar-mail-core/src/jmap/sync.rs b/crates/flectar-mail-core/src/jmap/sync.rs index b3782803..ecb95a7b 100644 --- a/crates/flectar-mail-core/src/jmap/sync.rs +++ b/crates/flectar-mail-core/src/jmap/sync.rs @@ -8,7 +8,8 @@ use crate::events::CoreEvent; use crate::jmap::client::{self, ConnectedClient}; use crate::models::{AccountConfig, Address, MailProtocol, SenderDeliveryStatus, now_ms, roles}; use crate::sync::engine::{ - PriorityFetchCmd, SyncCmd, SyncCtx, configured_sync_interval, set_state, set_state_error, + PriorityFetchCmd, SyncCmd, SyncCtx, caught_up, configured_sync_interval, set_state, + set_state_error, }; use futures::{FutureExt, Stream, StreamExt}; use jmap_client::core::error::MethodErrorType; @@ -122,9 +123,11 @@ async fn actor( ), } } + let started = now_ms(); set_state(&ctx, config.id, "syncing").await; sync_mailboxes(&ctx, config.id, &connected).await?; sync_emails(&ctx, &config, &connected, live_since).await?; + caught_up(&ctx, config.id, started).await; execute_actions(&ctx, &config, &connected).await?; // The counts only explain the badges, so they never fail a sync. if let Err(error) = sync_mailbox_counts(&ctx, config.id, &connected).await { diff --git a/crates/flectar-mail-core/src/sync/engine.rs b/crates/flectar-mail-core/src/sync/engine.rs index 8f80044d..aa5d107c 100644 --- a/crates/flectar-mail-core/src/sync/engine.rs +++ b/crates/flectar-mail-core/src/sync/engine.rs @@ -1280,6 +1280,20 @@ pub(crate) async fn set_state_error(ctx: &SyncCtx, account_id: i64, state: &str, set_state_inner(ctx, account_id, state, Some(error)).await; } +/// Note that a sync of the account that started at `started` stored all the +/// mail its Inbox had by then. A check in the background tells of what +/// arrives after it while the app isn't running (see +/// `Core::announce_missed`). +pub(crate) async fn caught_up(ctx: &SyncCtx, account_id: i64, started: i64) { + let noted = ctx + .db + .write(move |conn| repo::accounts::set_caught_up(conn, account_id, started)) + .await; + if let Err(error) = noted { + tracing::warn!(account_id, %error, "could not note how far a sync caught up"); + } +} + async fn set_state_inner(ctx: &SyncCtx, account_id: i64, state: &str, error: Option<&str>) { let st = state.to_string(); let error = error.map(str::to_owned); @@ -1469,6 +1483,7 @@ async fn run_cycle( inbox_pushed, } = mode; let account_id = config.id; + let started = now_ms(); // The folder list: first thing, then once it is FOLDER_LIST_INTERVAL old. let listed_now = !immediate_follow_up && listing.due(tokio::time::Instant::now()); @@ -1545,6 +1560,9 @@ async fn run_cycle( inbox_baseline, ) .await?; + if !inbox_forward_remaining { + caught_up(ctx, account_id, started).await; + } } // A manual Sync Now resolves at the foreground Inbox boundary. Finish any diff --git a/crates/flectar-mail-core/src/sync/gmail.rs b/crates/flectar-mail-core/src/sync/gmail.rs index bf33a63a..bc965efd 100644 --- a/crates/flectar-mail-core/src/sync/gmail.rs +++ b/crates/flectar-mail-core/src/sync/gmail.rs @@ -7,8 +7,8 @@ //! pending-action queue remain provider-neutral and fully offline-first. use super::engine::{ - PriorityFetchCmd, SyncCmd, SyncCtx, configured_sync_interval, emit_sync_status, set_state, - set_state_error, + PriorityFetchCmd, SyncCmd, SyncCtx, caught_up, configured_sync_interval, emit_sync_status, + set_state, set_state_error, }; use crate::db::repo::{self, messages::NewMessage}; use crate::error::{CoreError, Result}; @@ -2556,6 +2556,7 @@ async fn sync_once( live_since: i64, ) -> Result { let account_id = config.id; + let started = now_ms(); let mut state = ctx .db .read(move |conn| repo::gmail::state(conn, account_id)) @@ -2764,6 +2765,10 @@ async fn sync_once( if page.next_page_token.is_none() && !page.affected_threads.is_empty() { sync_draft_map(ctx, config, api).await?; } + if page.next_page_token.is_none() { + // The history ran out: everything up to this pass is stored. + caught_up(ctx, account_id, started).await; + } Ok(true) } diff --git a/crates/flectar-mail-core/src/sync/pop3.rs b/crates/flectar-mail-core/src/sync/pop3.rs index 5bac295e..5d75dec7 100644 --- a/crates/flectar-mail-core/src/sync/pop3.rs +++ b/crates/flectar-mail-core/src/sync/pop3.rs @@ -10,8 +10,8 @@ //! was. use super::engine::{ - PriorityFetchCmd, RestoreRequest, SyncCmd, SyncCtx, configured_sync_interval, set_state, - set_state_error, + PriorityFetchCmd, RestoreRequest, SyncCmd, SyncCtx, caught_up, configured_sync_interval, + set_state, set_state_error, }; use super::ingest::{self, Delivery, Importer}; use crate::accounts::credentials::{self, Slot}; @@ -75,6 +75,7 @@ pub(crate) async fn run(ctx: SyncCtx, mut config: AccountConfig, mut inputs: Inp tracing::warn!(account_id, %error, "completing local POP3 actions failed"); } if next_sync.is_some_and(|at| at <= Instant::now()) { + let started = now_ms(); next_sync = match sync(&ctx, &config, &mut catch_up).await { Ok(synced) => { backoff = Duration::from_secs(1); @@ -83,6 +84,7 @@ pub(crate) async fn run(ctx: SyncCtx, mut config: AccountConfig, mut inputs: Inp ctx.bus.emit(CoreEvent::NetworkState { online: true }); let next = match synced { Synced::Complete => { + caught_up(&ctx, account_id, started).await; finish(&mut waiters, Ok(())); Instant::now() + configured_sync_interval(&ctx.db).await }