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') => {