From 7a581b33b4929dc1353f1d9be9f5308f57bb5ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 17 Mar 2026 17:51:45 +0100 Subject: [PATCH] feat(imap): add back pagination for envelope listing --- src/imap/envelope/list.rs | 167 ++++++++++++++++++++++++++------------ 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/src/imap/envelope/list.rs b/src/imap/envelope/list.rs index fe07ade9..48ea1a63 100644 --- a/src/imap/envelope/list.rs +++ b/src/imap/envelope/list.rs @@ -4,12 +4,13 @@ use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; use io_imap::{ - coroutines::{fetch::*, select::*}, + coroutines::{fetch::*, select::*, status::*}, types::{ core::Vec1, envelope::Address, fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, - sequence::SequenceSet, + sequence::{SeqOrUid, Sequence, SequenceSet}, + status::{StatusDataItem, StatusDataItemName}, }, }; use io_stream::runtimes::std::handle; @@ -20,7 +21,7 @@ use serde::Serialize; use crate::imap::{ account::ImapAccount, - mailbox::arg::{MailboxNameOptionalArg, MailboxNoSelectFlag}, + mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; /// List IMAP envelopes from the given mailbox. @@ -30,17 +31,26 @@ use crate::imap::{ /// are fetched. #[derive(Debug, Parser)] pub struct ListEnvelopesCommand { + /// The sequence set of envelopes. + #[arg(value_name = "SEQUENCE")] + #[arg(conflicts_with = "page_size")] + #[arg(conflicts_with = "page")] + pub sequence_set: Option, + #[command(flatten)] - pub mailbox_name: MailboxNameOptionalArg, + pub mailbox_name: MailboxNameOptionalFlag, #[command(flatten)] pub mailbox_no_select: MailboxNoSelectFlag, + #[arg(long, default_value = "10")] + #[arg(conflicts_with = "sequence")] + pub page_size: usize, + #[arg(long, short, default_value = "0")] + #[arg(conflicts_with = "sequence")] + pub page: usize, - /// The sequence set of envelopes. - #[arg(short, long, default_value = "1:*")] - pub sequence: String, /// Use sequence numbers instead of UIDs. - #[arg(long)] - pub seq: bool, + #[arg(long, short, visible_alias = "seq")] + pub sequence: bool, } impl ListEnvelopesCommand { @@ -48,25 +58,64 @@ impl ListEnvelopesCommand { let mut imap = account.new_imap_session()?; let mailbox = self.mailbox_name.inner.try_into()?; - if !self.mailbox_no_select.inner { + let exists = if self.mailbox_no_select.inner { + let mut arg = None; + let mut coroutine = + ImapStatus::new(imap.context, mailbox, &[StatusDataItemName::Messages]); + + loop { + match coroutine.resume(arg.take()) { + ImapStatusResult::Io { io } => arg = Some(handle(&mut imap.stream, io)?), + ImapStatusResult::Ok { context, items } => { + imap.context = context; + break items.into_iter().find_map(|i| match i { + StatusDataItem::Messages(exists) => Some(exists), + _ => None, + }); + } + ImapStatusResult::Err { err, .. } => bail!(err), + } + } + } else { let mut arg = None; let mut coroutine = ImapSelect::new(imap.context, mailbox); - imap.context = loop { + loop { match coroutine.resume(arg.take()) { ImapSelectResult::Io { io } => arg = Some(handle(&mut imap.stream, io)?), - ImapSelectResult::Ok { context, .. } => break context, + ImapSelectResult::Ok { context, data } => { + imap.context = context; + break data.exists; + } ImapSelectResult::Err { err, .. } => bail!(err), } - }; - } + } + }; - let sequence_set: SequenceSet = self.sequence.parse()?; - let item_names = - MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]); + let mut has_sequence = false; + let sequence_set = match self.sequence_set { + Some(seq) => { + has_sequence = true; + seq.parse()? + } + None => match exists { + Some(n) => build_paginated_sequence(self.page, self.page_size, n as usize)?, + None => "1:*".try_into()?, + }, + }; + + let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![ + MessageDataItemName::Uid, + MessageDataItemName::Envelope, + ]); let mut arg = None; - let mut coroutine = ImapFetch::new(imap.context, sequence_set, item_names, !self.seq); + let mut coroutine = ImapFetch::new( + imap.context, + sequence_set, + item_names, + !self.sequence && has_sequence, + ); let data = loop { match coroutine.resume(arg.take()) { @@ -79,8 +128,7 @@ impl ListEnvelopesCommand { let table = EnvelopesTable { preset: account.table_preset, arrangement: account.table_arrangement, - envelopes: map_envelopes_table_entries(!self.seq, data), - uid_mode: !self.seq, + envelopes: map_envelopes_table_entries(data), }; printer.out(table) @@ -94,29 +142,28 @@ pub struct EnvelopesTable { #[serde(skip)] arrangement: ContentArrangement, envelopes: Vec, - uid_mode: bool, } impl fmt::Display for EnvelopesTable { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut table = Table::new(); - let id_header = if self.uid_mode { "UID" } else { "SEQ" }; - table .load_preset(&self.preset) .set_content_arrangement(self.arrangement.clone()) .set_header(Row::from([ - Cell::new(id_header), - Cell::new("Subject"), - Cell::new("From"), - Cell::new("Date"), + Cell::new("SEQ"), + Cell::new("UID"), + Cell::new("SUBJECT"), + Cell::new("FROM"), + Cell::new("DATE"), ])); for entry in &self.envelopes { let mut row = Row::new(); row.max_height(1); - row.add_cell(Cell::new(entry.id)); + row.add_cell(Cell::new(entry.seq)); + row.add_cell(Cell::new(entry.uid)); row.add_cell(Cell::new(&entry.subject)); row.add_cell(Cell::new(&entry.from)); row.add_cell(Cell::new(&entry.date)); @@ -128,57 +175,50 @@ impl fmt::Display for EnvelopesTable { } } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Default, Serialize)] pub struct EnvelopesTableEntry { - pub id: u32, + pub seq: u32, + pub uid: u32, pub date: String, pub from: String, pub subject: String, } fn map_envelopes_table_entries( - uid_mode: bool, data: HashMap>>, ) -> Vec { let mut entries: Vec = data .into_iter() .map(|(seq, items)| { - let mut id = seq.get(); - let mut date = String::new(); - let mut from = String::new(); - let mut subject = String::new(); + let mut entry = EnvelopesTableEntry::default(); + entry.seq = seq.get(); for item in items.into_iter() { match item { MessageDataItem::Uid(uid) => { - if uid_mode { - id = uid.get(); - } + entry.uid = uid.get(); } MessageDataItem::Envelope(env) => { - // NString wraps Option, access via .0 - if let Some(d) = &env.date.0 { - date = String::from_utf8_lossy(d.as_ref()).to_string(); + if let Some(d) = env.date.into_option() { + entry.date = String::from_utf8_lossy(d.as_ref()).to_string(); } - if let Some(s) = &env.subject.0 { - subject = decode_mime(String::from_utf8_lossy(s.as_ref()).as_ref()); + + if let Some(s) = env.subject.into_option() { + entry.subject = + decode_mime(String::from_utf8_lossy(s.as_ref()).as_ref()); } - from = format_addresses_short(&env.from); + + entry.from = format_addresses_short(&env.from); } _ => {} } } - EnvelopesTableEntry { - id, - date, - from, - subject, - } + entry }) .collect(); - entries.sort_by_key(|e| e.id); + entries.sort_by_key(|e| e.uid); entries.reverse(); entries } @@ -250,3 +290,28 @@ pub fn format_addresses_short(addrs: &[Address<'_>]) -> String { .collect::>() .join(", ") } + +fn build_paginated_sequence(page: usize, page_size: usize, total: usize) -> Result { + let seq = if page_size == 0 { + Sequence::Range(SeqOrUid::try_from(1).unwrap(), SeqOrUid::Asterisk) + } else { + let page_cursor = page * page_size; + if page_cursor >= total { + bail!("page {} out of bounds", page + 1); + } + + let mut count = 1; + let mut cursor = total - (total.min(page_cursor)); + + let page_size = page_size.min(total); + let from = SeqOrUid::Value(NonZeroU32::new(cursor as u32).unwrap()); + while cursor > 1 && count < page_size { + count += 1; + cursor -= 1; + } + let to = SeqOrUid::Value(NonZeroU32::new(cursor as u32).unwrap()); + Sequence::Range(to, from) + }; + + Ok(seq.into()) +}