git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commite6119652ff7ee727325ed9e6a98d44c4a9787182
parent4da4692117
authorLucas Galante <[email protected]>
date2026-07-27 09:52
feat: sync on start and folder switch, remember last account

- start_sync() wraps sync_imap with a 30s throttle so tab-hopping doesn't
  stack IMAP connections; explicit account selection forces through
- entering Inbox/Sent/Trash refreshes from the server (throttled)
- last-selected account persists to a cce-email-account.txt sidecar
  (accounts.json stays settings-owned) and wins over is_default on start,
  so the on-start sync hits the account actually in use

Live-verified: cold start boots into the synced Gmail inbox with no
interaction; immediate folder switch is silent; a switch past the window
reconnects.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 59 insertions(+), 9 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 372a50d..811157a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -134,6 +134,9 @@ struct ClearEmailApp {
     body_sb_drag_offset: f32,
     compose_open: bool,
     compose_title: String,
+    /// When the most recent IMAP sync was spawned — folder switches re-sync
+    /// through [`Self::start_sync`], throttled against tab-hopping.
+    last_sync_start: Option<std::time::Instant>,
     status_message: Option<(String, f32)>, // (message, timer)
     sender: calloop::channel::Sender<AppMessage>,
 
@@ -205,6 +208,24 @@ fn save_accounts(accounts: &[AccountInfo]) {
     }
 }
 
+/// Sidecar remembering which account was last selected, so a restart returns
+/// to (and on-start syncs) the account the user actually reads. Deliberately
+/// not in accounts.json — that file is owned by cce-system-settings.
+fn selected_account_path() -> std::path::PathBuf {
+    cce_ui::config::cce_config_dir().join("cce-email-account.txt")
+}
+
+fn load_selected_account_email() -> Option<String> {
+    std::fs::read_to_string(selected_account_path())
+        .ok()
+        .map(|s| s.trim().to_string())
+        .filter(|s| !s.is_empty())
+}
+
+fn save_selected_account_email(email: &str) {
+    let _ = std::fs::write(selected_account_path(), email);
+}
+
 fn get_account_emails_path(email: &str) -> std::path::PathBuf {
     let p = cce_ui::config::cce_config_dir();
     if !p.exists() {
@@ -964,6 +985,20 @@ impl ClearEmailApp {
     }
 
 
+    /// Spawn an IMAP sync for the selected account. Unforced calls (folder
+    /// switches) are throttled so tab-hopping doesn't stack connections;
+    /// forced calls (explicit account selection) always run.
+    fn start_sync(&mut self, force: bool) {
+        const MIN_SYNC_GAP: std::time::Duration = std::time::Duration::from_secs(30);
+        if !force && self.last_sync_start.is_some_and(|t| t.elapsed() < MIN_SYNC_GAP) {
+            return;
+        }
+        if let Some(acc) = self.accounts.get(self.selected_account_idx) {
+            sync_imap(acc.clone(), self.sender.clone());
+            self.last_sync_start = Some(std::time::Instant::now());
+        }
+    }
+
     fn save_emails(&self) {
         if let Some(acc) = self.accounts.get(self.selected_account_idx) {
             save_emails_for_account(&acc.email, &self.emails);
@@ -1289,7 +1324,13 @@ impl Application for ClearEmailApp {
         let btn_unread = Button::new(561.0, 8.0, 110.0, 26.0).with_label("Mark Unread");
 
         let accounts = load_accounts();
-        let selected_account_idx = accounts.iter().position(|a| a.is_default).unwrap_or(0);
+        // Last-used account wins (sidecar file), else the configured default:
+        // the on-start sync below should hit the account the user actually
+        // reads, not whichever entry carries the is_default flag.
+        let selected_account_idx = load_selected_account_email()
+            .and_then(|e| accounts.iter().position(|a| a.email == e))
+            .or_else(|| accounts.iter().position(|a| a.is_default))
+            .unwrap_or(0);
 
         let btn_manage_accounts = Button::new(66.0, 15.0, 300.0, 26.0).with_label("Manage Accounts...");
 
@@ -1315,11 +1356,15 @@ impl Application for ClearEmailApp {
             Vec::new()
         };
 
-        if let Some(acc) = accounts.get(selected_account_idx) {
+        let last_sync_start = if let Some(acc) = accounts.get(selected_account_idx) {
             sync_imap(acc.clone(), _sender.clone());
-        }
+            Some(std::time::Instant::now())
+        } else {
+            None
+        };
 
         Self {
+            last_sync_start,
             keys: EmailKeys::load(),
             btn_compose,
             paginator,
@@ -1396,13 +1441,17 @@ impl Application for ClearEmailApp {
                         self.accounts.get(new_idx).map(|a| a.email.as_str()) != prev_email.as_deref();
                     self.selected_account_idx = new_idx;
                     if changed_account {
-                        if let Some(acc) = self.accounts.get(new_idx) {
-                            self.emails = load_emails_for_account(&acc.email);
-                            sync_imap(acc.clone(), self.sender.clone());
+                        if let Some(email) = self.accounts.get(new_idx).map(|a| a.email.clone()) {
+                            self.emails = load_emails_for_account(&email);
+                            save_selected_account_email(&email);
+                            self.start_sync(true);
                         } else {
                             self.emails = Vec::new();
                         }
                     }
+                } else {
+                    // Mail folders refresh from the server on entry (throttled).
+                    self.start_sync(false);
                 }
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -1534,9 +1583,10 @@ impl Application for ClearEmailApp {
             }
             AppMessage::SelectAccount(idx) => {
                 self.selected_account_idx = idx;
-                if let Some(acc) = self.accounts.get(idx) {
-                    self.emails = load_emails_for_account(&acc.email);
-                    sync_imap(acc.clone(), self.sender.clone());
+                if let Some(email) = self.accounts.get(idx).map(|a| a.email.clone()) {
+                    self.emails = load_emails_for_account(&email);
+                    save_selected_account_email(&email);
+                    self.start_sync(true);
                 } else {
                     self.emails = Vec::new();
                 }