From df112755ec7594b013af83d36a29e5270774c9fd Mon Sep 17 00:00:00 2001 From: Shautvast Date: Tue, 17 Feb 2026 20:58:12 +0100 Subject: [PATCH] Fetch older emails on scroll with lazy loading Introduce Inbox struct to track the oldest fetched sequence number. When scrolling past the last email, automatically fetch the next batch of 50 older messages and append them to the list. Co-Authored-By: Claude Opus 4.6 --- src/inbox.rs | 112 +++++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 27 +++++++------ 2 files changed, 101 insertions(+), 38 deletions(-) diff --git a/src/inbox.rs b/src/inbox.rs index ce0a560..43c313c 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -3,61 +3,121 @@ use crate::config::Config; use crate::connect::ImapSession; use crate::{connect, Email}; -const MAX_FETCH: u32 = 50; +const BATCH_SIZE: u32 = 50; -/// Refresh inbox using NOOP + fetch. Reconnects on error. +pub(crate) struct Inbox { + pub emails: Vec, + /// The lowest IMAP sequence number we've fetched so far (1-based). + /// None means we haven't fetched yet or inbox was empty. + oldest_seq: Option, +} + +impl Inbox { + pub fn new() -> Self { + Self { + emails: Vec::new(), + oldest_seq: None, + } + } + + pub fn has_older(&self) -> bool { + self.oldest_seq.map_or(false, |s| s > 1) + } +} + +/// Refresh inbox (full reload). Reconnects on error. pub(crate) fn refresh( session: &mut Option, config: &Config, -) -> Result, String> { +) -> Result { // If we have a session, try NOOP to keep alive / detect changes if let Some(s) = session.as_mut() { if s.noop().is_ok() { - return fetch_inbox(s); + return fetch_latest(s); } } // Session is dead or missing — reconnect *session = None; let mut new_session = connect::connect(config)?; - let result = fetch_inbox(&mut new_session); + let result = fetch_latest(&mut new_session); *session = Some(new_session); result } +/// Fetch older emails and append them to the existing inbox. +pub(crate) fn fetch_more( + session: &mut Option, + inbox: &mut Inbox, + config: &Config, +) -> Result<(), String> { + let oldest = match inbox.oldest_seq { + Some(s) if s > 1 => s, + _ => return Ok(()), + }; -fn fetch_range(exists: u32) -> String { - let start = exists.saturating_sub(MAX_FETCH - 1).max(1); - format!("{}:{}", start, exists) + let ensure_session = |session: &mut Option| -> Result<(), String> { + if session.as_mut().map_or(true, |s| s.noop().is_err()) { + *session = None; + *session = Some(connect::connect(config)?); + } + Ok(()) + }; + ensure_session(session)?; + + let s = session.as_mut().unwrap(); + let end = oldest - 1; + let start = end.saturating_sub(BATCH_SIZE - 1).max(1); + let range = format!("{}:{}", start, end); + + let mut older = fetch_range_emails(s, &range)?; + older.reverse(); + inbox.oldest_seq = Some(start); + inbox.emails.append(&mut older); + Ok(()) } -fn fetch_inbox(session: &mut ImapSession) -> Result, String> { +fn fetch_latest(session: &mut ImapSession) -> Result { + let exists = select_inbox(session)?; + if exists == 0 { + return Ok(Inbox::new()); + } + let start = exists.saturating_sub(BATCH_SIZE - 1).max(1); + let range = format!("{}:{}", start, exists); + let mut emails = fetch_range_emails(session, &range)?; + emails.reverse(); + Ok(Inbox { + emails, + oldest_seq: Some(start), + }) +} + +fn select_inbox(session: &mut ImapSession) -> Result { match session { ImapSession::Plain(s) => { let mailbox = s.select("INBOX").map_err(|e| e.to_string())?; - if mailbox.exists == 0 { - return Ok(Vec::new()); - } - let range = fetch_range(mailbox.exists); - let messages = s - .fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") - .map_err(|e| e.to_string())?; - let mut emails = parse_emails(&messages); - emails.reverse(); - Ok(emails) + Ok(mailbox.exists) } ImapSession::Tls(s) => { let mailbox = s.select("INBOX").map_err(|e| e.to_string())?; - if mailbox.exists == 0 { - return Ok(Vec::new()); - } - let range = fetch_range(mailbox.exists); + Ok(mailbox.exists) + } + } +} + +fn fetch_range_emails(session: &mut ImapSession, range: &str) -> Result, String> { + match session { + ImapSession::Plain(s) => { let messages = s .fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") .map_err(|e| e.to_string())?; - let mut emails = parse_emails(&messages); - emails.reverse(); - Ok(emails) + Ok(parse_emails(&messages)) + } + ImapSession::Tls(s) => { + let messages = s + .fetch(range, "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") + .map_err(|e| e.to_string())?; + Ok(parse_emails(&messages)) } } } diff --git a/src/lib.rs b/src/lib.rs index c4d4840..8c28acf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,8 +26,8 @@ pub fn main(config: &Config, terminal: &mut Terminal>) let mut inbox = inbox::refresh(&mut session, config); let mut last_fetch = Instant::now(); let mut list_state = ListState::default(); - if let Ok(emails) = &inbox { - if !emails.is_empty() { + if let Ok(ib) = &inbox { + if !ib.emails.is_empty() { list_state.select(Some(0)); } } @@ -47,8 +47,8 @@ pub fn main(config: &Config, terminal: &mut Terminal>) .split(area); match &inbox { - Ok(emails) if !emails.is_empty() => { - let items: Vec = emails + Ok(ib) if !ib.emails.is_empty() => { + let items: Vec = ib.emails .iter() .map(|e| { ListItem::new(format!("{} | {} | {}", e.date, e.from, e.subject)) @@ -58,7 +58,7 @@ pub fn main(config: &Config, terminal: &mut Terminal>) let list = List::new(items) .block( Block::default() - .title(format!("Inbox ({} messages)", emails.len())) + .title(format!("Inbox ({} messages)", ib.emails.len())) .borders(Borders::ALL), ) .style(Style::default().fg(Color::White)) @@ -98,8 +98,8 @@ pub fn main(config: &Config, terminal: &mut Terminal>) KeyCode::Char('r') => { inbox = inbox::refresh(&mut session, config); last_fetch = Instant::now(); - if let Ok(emails) = &inbox { - if !emails.is_empty() { + if let Ok(ib) = &inbox { + if !ib.emails.is_empty() { list_state.select(Some(0)); } else { list_state.select(None); @@ -107,11 +107,14 @@ pub fn main(config: &Config, terminal: &mut Terminal>) } } KeyCode::Down | KeyCode::Char('j') => { - if let Ok(emails) = &inbox { - let i = list_state.selected().map_or(0, |i| { - if i + 1 < emails.len() { i + 1 } else { i } - }); - list_state.select(Some(i)); + if let Ok(ib) = &mut inbox { + let len = ib.emails.len(); + let i = list_state.selected().map_or(0, |i| i + 1); + if i >= len && ib.has_older() { + let _ = inbox::fetch_more(&mut session, ib, config); + } + let new_len = ib.emails.len(); + list_state.select(Some(i.min(new_len.saturating_sub(1)))); } } KeyCode::Up | KeyCode::Char('k') => {