From b350955f21b2c11682b178d8a98bcf5bf446cd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 12 Jun 2026 10:54:22 +0200 Subject: [PATCH] fix(imap): add sort fallback Introduce a new config option imap.sort.fallback: true for a slower SEARCH + SORT combination, false for the SORT. If omitted, use fallback when the SORT capability is not returned by the server. Refs: #698 --- Cargo.lock | 55 ++++++++++++++++++------------------- Cargo.toml | 2 ++ config.sample.toml | 6 ++++ deny.toml | 12 ++------ src/config.rs | 34 ++++++++++++++++------- src/imap/client.rs | 29 ++++++++++++++----- src/imap/envelope/get.rs | 20 ++++++++++---- src/imap/envelope/list.rs | 28 +++++++++++++------ src/imap/envelope/search.rs | 15 ++++++---- src/imap/envelope/sort.rs | 22 +++++++++++---- src/imap/envelope/thread.rs | 29 ++++++++++++++----- src/imap/flag/add.rs | 18 ++++++++---- src/imap/flag/list.rs | 7 +++-- src/imap/flag/remove.rs | 18 ++++++++---- src/imap/flag/set.rs | 18 ++++++++---- src/imap/id.rs | 13 ++++++--- src/imap/mailbox/expunge.rs | 3 +- src/imap/mailbox/purge.rs | 9 ++++-- src/imap/mailbox/select.rs | 3 +- src/imap/message/copy.rs | 13 +++++++-- src/imap/message/export.rs | 16 +++++++++-- src/imap/message/get.rs | 16 +++++++++-- src/imap/message/move.rs | 13 +++++++-- src/imap/message/read.rs | 16 +++++++++-- src/imap/message/save.rs | 19 +++++++++---- src/wizard/account.rs | 1 + 26 files changed, 301 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 757ad88f..596c3861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,8 +1046,7 @@ dependencies = [ [[package]] name = "io-email" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d8602a9427bbeec3c2b37ad6c84789c2abc74d5a5ef21ef6b596f6bb3270b0" +source = "git+https://github.com/pimalaya/io-email#6eb3fd75e254586639a6f198a46973498e63015f" dependencies = [ "chrono", "chumsky", @@ -1089,8 +1088,7 @@ dependencies = [ [[package]] name = "io-imap" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3abdd895505c9e96145abec34f7a3ef106e70bff71b04b0f116362c379d27e7" +source = "git+https://github.com/pimalaya/io-imap#e6b4fb10633875b2f7d313f459dbd561458ed244" dependencies = [ "anyhow", "base64", @@ -1278,13 +1276,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1393,9 +1390,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" @@ -1578,9 +1575,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.6.0+3.6.2" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] @@ -1882,9 +1879,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rfc2047-decoder" @@ -2241,9 +2238,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "stable_deref_trait" @@ -2481,9 +2478,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -2538,9 +2535,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -2551,9 +2548,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2561,9 +2558,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -2574,9 +2571,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] @@ -2942,18 +2939,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a6abe0e6..04adc1f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,4 +77,6 @@ uds_windows = "1" [patch.crates-io] domain = { git = "https://github.com/nlnetlabs/domain", rev = "b30087125c13e71010d9783a1bcdd9eb9fa3f336" } +io-email.git = "https://github.com/pimalaya/io-email" +io-imap.git = "https://github.com/pimalaya/io-imap" pimconf.git = "https://github.com/pimalaya/pimconf" diff --git a/config.sample.toml b/config.sample.toml index 9ad3a2b3..e9ccd572 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -217,6 +217,12 @@ imap.sasl.plain.password.raw = "***" # a warning. `false` always sends NIL. An empty map sends `ID NIL`. #imap.id.fields = { name = true, version = true, vendor = true, support-url = true } +# RFC 5256 SORT fallback. When the server lacks the SORT capability, himalaya +# sorts client-side via SEARCH + FETCH. Leave unset to follow the capability; +# set `true` to always sort client-side, or `false` to always issue a server SORT. +# https://www.rfc-editor.org/rfc/rfc5256.html +#imap.sort.fallback = false + # -------------------------------------------------------------------------------- # JMAP config # https://www.iana.org/go/rfc8620 diff --git a/deny.toml b/deny.toml index 5c2e9109..f2e7a083 100644 --- a/deny.toml +++ b/deny.toml @@ -1,17 +1,9 @@ [sources] allow-git = [ "https://github.com/nlnetlabs/domain", - # "https://github.com/pimalaya/cli", - # "https://github.com/pimalaya/config", - # "https://github.com/pimalaya/io-email", - # "https://github.com/pimalaya/io-http", - # "https://github.com/pimalaya/io-imap", - # "https://github.com/pimalaya/io-jmap", - # "https://github.com/pimalaya/io-m2dir", - # "https://github.com/pimalaya/io-maildir", - # "https://github.com/pimalaya/io-smtp", + "https://github.com/pimalaya/io-email", + "https://github.com/pimalaya/io-imap", "https://github.com/pimalaya/pimconf", - # "https://github.com/pimalaya/stream", ] unknown-git = "deny" unknown-registry = "deny" diff --git a/src/config.rs b/src/config.rs index f6daba29..d9922b9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -314,24 +314,38 @@ pub struct ImapConfig { #[serde(default)] pub starttls: bool, - /// ALPN protocol identifiers offered during the TLS handshake. - /// Defaults to `["imap"]` (RFC 7595, IANA registry). Set to `[]` - /// to skip ALPN negotiation entirely. Only relevant for the - /// rustls provider; `native-tls` ignores ALPN. + /// ALPN protocol identifiers offered during the TLS handshake. Defaults to + /// `["imap"]` (RFC 7595, IANA registry). Set to `[]` to skip ALPN + /// negotiation entirely. Only relevant for the rustls provider; + /// `native-tls` ignores ALPN. #[serde(default = "io_imap::client::default_alpn")] pub alpn: Vec, /// Optional SASL credentials. When omitted, the connection skips - /// authentication entirely (no `AUTHENTICATE` command is sent); - /// to advertise the ANONYMOUS mechanism explicitly, set - /// `sasl.anonymous = {}`. + /// authentication entirely (no `AUTHENTICATE` command is sent); to + /// advertise the ANONYMOUS mechanism explicitly, set `sasl.anonymous = {}`. pub sasl: Option, - /// RFC 2971 `ID` extension quirks. Some providers (notably - /// mail.qq.com, fastmail) require an `ID` exchange straight after - /// authentication; set `id.auto = true` to opt in. + /// RFC 2971 `ID` extension quirks. Some providers (notably mail.qq.com, + /// fastmail) require an `ID` exchange straight after authentication; set + /// `id.auto = true` to opt in. #[serde(default)] pub id: ImapIdConfig, + + /// RFC 5256 `SORT` extension config. + #[serde(default)] + pub sort: ImapSortConfig, +} + +/// Per-account `imap.sort.*` options. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct ImapSortConfig { + /// Forces the SORT fallback on or off. `Some(true)` always sorts + /// client-side via SEARCH + FETCH; `Some(false)` always issues a server + /// `SORT`. Left unset, the fallback is enabled only when the server lacks + /// the SORT capability. + pub fallback: Option, } /// Per-account `imap.id.*` quirks. diff --git a/src/imap/client.rs b/src/imap/client.rs index 665a58d6..18c4d7ac 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -11,7 +11,7 @@ use std::{ }; use anyhow::{Result, anyhow}; -use io_imap::client::ImapClientStd as Inner; +use io_imap::{client::ImapClientStd as Inner, has_imap_capability, types::response::Capability}; use pimalaya_config::toml::TomlConfig; use pimalaya_stream::sasl::Sasl; use url::Url; @@ -23,14 +23,16 @@ use crate::{ pub struct ImapClient { inner: Inner, + capabilities: Vec>, + sort_fallback: Option, } impl ImapClient { - /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL). - /// The capability list reported by the connect handshake is - /// discarded; IMAP-specific subcommands that need it should call - /// [`Inner::capability`] explicitly. + /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL), + /// caching the capability list reported by the handshake and the + /// `imap.sort.fallback` config override for later policy checks. pub fn new(config: ImapConfig) -> Result { + let sort_fallback = config.sort.fallback; let tls = config.tls.into_tls(config.alpn); let auto_id = resolve_auto_id_params(&config.id)?; let server = parse_imap_server(&config.server)?; @@ -42,8 +44,21 @@ impl ImapClient { Some(cfg.try_into_sasl(host, port)) }) .transpose()?; - let (inner, _capability) = Inner::connect(&server, &tls, config.starttls, sasl, auto_id)?; - Ok(Self { inner }) + let (inner, capabilities) = Inner::connect(&server, &tls, config.starttls, sasl, auto_id)?; + Ok(Self { + inner, + capabilities, + sort_fallback, + }) + } + + /// Resolves the SORT fallback policy: the `imap.sort.fallback` + /// config override when set, otherwise on only when the server + /// lacks the SORT capability. When `true`, sort client-side via + /// SEARCH + FETCH instead of issuing a server `SORT`. + pub fn sort_fallback(&self) -> bool { + self.sort_fallback + .unwrap_or_else(|| !has_imap_capability!(self.capabilities, Sort(_))) } } diff --git a/src/imap/envelope/get.rs b/src/imap/envelope/get.rs index b3d5172b..798d84d4 100644 --- a/src/imap/envelope/get.rs +++ b/src/imap/envelope/get.rs @@ -3,9 +3,12 @@ use std::fmt; use anyhow::{Result, bail}; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::types::{ - core::Vec1, - fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + types::{ + core::Vec1, + fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, + }, }; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -47,14 +50,21 @@ impl ImapEnvelopeGetCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]); let sequence_set = self.id.parse()?; - let mut data = client.fetch(sequence_set, item_names, !self.seq)?; + let mut data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid: !self.seq, + modifiers: Vec::new(), + }, + )?; let Some((_, items)) = data.pop_first() else { bail!("No envelope returned for ID {}", self.id); diff --git a/src/imap/envelope/list.rs b/src/imap/envelope/list.rs index 32970edb..9c81151b 100644 --- a/src/imap/envelope/list.rs +++ b/src/imap/envelope/list.rs @@ -3,12 +3,15 @@ use std::{collections::BTreeMap, fmt, num::NonZeroU32}; use anyhow::{Result, bail}; use clap::Parser; use comfy_table::{Cell, Color, ContentArrangement, Row, Table}; -use io_imap::types::{ - core::Vec1, - envelope::Address, - fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, - sequence::{SeqOrUid, Sequence, SequenceSet}, - status::{StatusDataItem, StatusDataItemName}, +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + types::{ + core::Vec1, + envelope::Address, + fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, + sequence::{SeqOrUid, Sequence, SequenceSet}, + status::{StatusDataItem, StatusDataItemName}, + }, }; use log::debug; use pimalaya_cli::printer::Printer; @@ -66,7 +69,9 @@ impl ImapEnvelopeListCommand { _ => None, }) } else { - client.select(mailbox)?.exists + client + .select(mailbox, ImapMailboxSelectOptions::default())? + .exists }; let mut has_sequence = false; @@ -86,7 +91,14 @@ impl ImapEnvelopeListCommand { MessageDataItemName::Envelope, ]); - let data = client.fetch(sequence_set, item_names, !self.sequence && has_sequence)?; + let data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid: !self.sequence && has_sequence, + modifiers: Vec::new(), + }, + )?; let table = EnvelopesTable { preset: account.table_preset().to_string(), diff --git a/src/imap/envelope/search.rs b/src/imap/envelope/search.rs index 9ec0e65b..c1c56902 100644 --- a/src/imap/envelope/search.rs +++ b/src/imap/envelope/search.rs @@ -3,10 +3,13 @@ use std::fmt; use anyhow::{Result, anyhow, bail}; use clap::Parser; use comfy_table::{Cell, Color, ContentArrangement, Row, Table}; -use io_imap::types::{ - core::{AString, Vec1}, - datetime::NaiveDate, - search::SearchKey, +use io_imap::{ + rfc3501::{search::ImapMessageSearchOptions, select::ImapMailboxSelectOptions}, + types::{ + core::{AString, Vec1}, + datetime::NaiveDate, + search::SearchKey, + }, }; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -68,11 +71,11 @@ impl ImapEnvelopeSearchCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let criteria = parse_query(&self.query)?; - let ids = client.search(criteria, !self.seq)?; + let ids = client.search(criteria, ImapMessageSearchOptions { uid: !self.seq })?; let table = SearchTable { preset: account.table_preset().to_string(), diff --git a/src/imap/envelope/sort.rs b/src/imap/envelope/sort.rs index 77bd9170..6ec354da 100644 --- a/src/imap/envelope/sort.rs +++ b/src/imap/envelope/sort.rs @@ -3,9 +3,13 @@ use std::fmt; use anyhow::{Result, bail}; use clap::Parser; use comfy_table::{Cell, Color, ContentArrangement, Row, Table, presets}; -use io_imap::types::{ - core::Vec1, - extensions::sort::{SortCriterion, SortKey}, +use io_imap::{ + rfc3501::select::ImapMailboxSelectOptions, + rfc5256::sort::ImapMessageSortOptions, + types::{ + core::Vec1, + extensions::sort::{SortCriterion, SortKey}, + }, }; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -60,7 +64,7 @@ impl ImapEnvelopeSortCommand { ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; let sort_key = parse_sort_key(&self.sort)?; let sort_criteria = Vec1::unvalidated(vec![SortCriterion { @@ -69,7 +73,15 @@ impl ImapEnvelopeSortCommand { }]); let search_criteria = parse_query(&self.query)?; - let ids = client.sort(sort_criteria, search_criteria, !self.seq)?; + let fallback = client.sort_fallback(); + let ids = client.sort( + sort_criteria, + search_criteria, + ImapMessageSortOptions { + uid: !self.seq, + fallback, + }, + )?; let id_color = account.envelopes_list_table_id_color(); let table = SortResultsTable::new(ids, !self.seq, id_color); diff --git a/src/imap/envelope/thread.rs b/src/imap/envelope/thread.rs index e4092255..4f577190 100644 --- a/src/imap/envelope/thread.rs +++ b/src/imap/envelope/thread.rs @@ -2,10 +2,14 @@ use std::{collections::HashMap, fmt, num::NonZeroU32}; use anyhow::{Result, bail}; use clap::Parser; -use io_imap::types::{ - extensions::thread::{Thread, ThreadingAlgorithm}, - fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, - sequence::SequenceSet, +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + rfc5256::thread::ImapMessageThreadOptions, + types::{ + extensions::thread::{Thread, ThreadingAlgorithm}, + fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, + sequence::SequenceSet, + }, }; use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer, ser::SerializeStruct}; @@ -49,13 +53,17 @@ impl ImapEnvelopeThreadCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let algorithm = parse_algorithm(&self.algorithm)?; let search_criteria = parse_query(&self.query)?; - let threads = client.thread(algorithm, search_criteria, !self.seq)?; + let threads = client.thread( + algorithm, + search_criteria, + ImapMessageThreadOptions { uid: !self.seq }, + )?; let all_ids = collect_thread_ids(&threads); let subjects = if !all_ids.is_empty() { @@ -127,7 +135,14 @@ fn fetch_subjects( MessageDataItemName::Uid, ]); - let data = client.fetch(sequence_set, item_names, uid)?; + let data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid, + modifiers: Vec::new(), + }, + )?; let mut subjects: HashMap = HashMap::new(); diff --git a/src/imap/flag/add.rs b/src/imap/flag/add.rs index 48c9ac6e..b494a001 100644 --- a/src/imap/flag/add.rs +++ b/src/imap/flag/add.rs @@ -1,8 +1,11 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::{ - IntoStatic, - flag::{Flag, StoreType}, +use io_imap::{ + rfc3501::{select::ImapMailboxSelectOptions, store::ImapMessageStoreOptions}, + types::{ + IntoStatic, + flag::{Flag, StoreType}, + }, }; use pimalaya_cli::printer::{Message, Printer}; @@ -39,7 +42,7 @@ impl ImapFlagAddCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -49,7 +52,12 @@ impl ImapFlagAddCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - client.store(sequence_set, StoreType::Add, flags, !self.seq)?; + client.store( + sequence_set, + StoreType::Add, + flags, + ImapMessageStoreOptions { uid: !self.seq }, + )?; printer.out(Message::new("Flag(s) successfully added")) } diff --git a/src/imap/flag/list.rs b/src/imap/flag/list.rs index 4a4aadfc..9c541233 100644 --- a/src/imap/flag/list.rs +++ b/src/imap/flag/list.rs @@ -3,7 +3,10 @@ use std::{collections::BTreeMap, fmt}; use anyhow::Result; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_imap::types::flag::{Flag, FlagPerm}; +use io_imap::{ + rfc3501::select::ImapMailboxSelectOptions, + types::flag::{Flag, FlagPerm}, +}; use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer}; @@ -30,7 +33,7 @@ impl ImapFlagListCommand { ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; - let data = client.select(mailbox)?; + let data = client.select(mailbox, ImapMailboxSelectOptions::default())?; let flags = data.flags.unwrap_or_default(); let permanent_flags = data.permanent_flags.unwrap_or_default(); diff --git a/src/imap/flag/remove.rs b/src/imap/flag/remove.rs index 2e07650a..8e121131 100644 --- a/src/imap/flag/remove.rs +++ b/src/imap/flag/remove.rs @@ -1,8 +1,11 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::{ - IntoStatic, - flag::{Flag, StoreType}, +use io_imap::{ + rfc3501::{select::ImapMailboxSelectOptions, store::ImapMessageStoreOptions}, + types::{ + IntoStatic, + flag::{Flag, StoreType}, + }, }; use pimalaya_cli::printer::{Message, Printer}; @@ -39,7 +42,7 @@ impl ImapFlagRemoveCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -49,7 +52,12 @@ impl ImapFlagRemoveCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - client.store(sequence_set, StoreType::Remove, flags, !self.seq)?; + client.store( + sequence_set, + StoreType::Remove, + flags, + ImapMessageStoreOptions { uid: !self.seq }, + )?; printer.out(Message::new("Flag(s) successfully removed")) } diff --git a/src/imap/flag/set.rs b/src/imap/flag/set.rs index 77f56122..f61c1f29 100644 --- a/src/imap/flag/set.rs +++ b/src/imap/flag/set.rs @@ -1,8 +1,11 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::{ - IntoStatic, - flag::{Flag, StoreType}, +use io_imap::{ + rfc3501::{select::ImapMailboxSelectOptions, store::ImapMessageStoreOptions}, + types::{ + IntoStatic, + flag::{Flag, StoreType}, + }, }; use pimalaya_cli::printer::{Message, Printer}; @@ -39,7 +42,7 @@ impl ImapFlagSetCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -49,7 +52,12 @@ impl ImapFlagSetCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - client.store(sequence_set, StoreType::Replace, flags, !self.seq)?; + client.store( + sequence_set, + StoreType::Replace, + flags, + ImapMessageStoreOptions { uid: !self.seq }, + )?; printer.out(Message::new("Flag(s) successfully replaced")) } diff --git a/src/imap/id.rs b/src/imap/id.rs index 6121d0c2..b41df606 100644 --- a/src/imap/id.rs +++ b/src/imap/id.rs @@ -3,9 +3,12 @@ use std::{collections::HashMap, fmt}; use anyhow::{Result, anyhow}; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::types::{ - IntoStatic, - core::{IString, NString}, +use io_imap::{ + rfc2971::id::ImapServerIdOptions, + types::{ + IntoStatic, + core::{IString, NString}, + }, }; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -44,7 +47,9 @@ impl ImapIdCommand { params.extend(more); } - let params = client.id(Some(params.into_iter().collect()))?; + let params = client.id(ImapServerIdOptions { + parameters: Some(params.into_iter().collect()), + })?; let table = ServerIdTable { preset: account.table_preset().to_string(), diff --git a/src/imap/mailbox/expunge.rs b/src/imap/mailbox/expunge.rs index 7bded99a..3717a564 100644 --- a/src/imap/mailbox/expunge.rs +++ b/src/imap/mailbox/expunge.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Parser; +use io_imap::rfc3501::select::ImapMailboxSelectOptions; use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ @@ -24,7 +25,7 @@ impl ImapMailboxExpungeCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } client.expunge()?; diff --git a/src/imap/mailbox/purge.rs b/src/imap/mailbox/purge.rs index 2b9de5ec..ee13d661 100644 --- a/src/imap/mailbox/purge.rs +++ b/src/imap/mailbox/purge.rs @@ -1,6 +1,9 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::flag::{Flag, StoreType}; +use io_imap::{ + rfc3501::{select::ImapMailboxSelectOptions, store::ImapMessageStoreOptions}, + types::flag::{Flag, StoreType}, +}; use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ @@ -26,14 +29,14 @@ impl ImapMailboxPurgeCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } client.store( "1:*".try_into()?, StoreType::Add, vec![Flag::Deleted], - false, + ImapMessageStoreOptions { uid: false }, )?; client.expunge()?; diff --git a/src/imap/mailbox/select.rs b/src/imap/mailbox/select.rs index 167597c5..c1b7e2bb 100644 --- a/src/imap/mailbox/select.rs +++ b/src/imap/mailbox/select.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Parser; +use io_imap::rfc3501::select::ImapMailboxSelectOptions; use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg}; @@ -21,7 +22,7 @@ pub struct ImapMailboxSelectCommand { impl ImapMailboxSelectCommand { pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; printer.out(Message::new("Mailbox successfully selected")) } } diff --git a/src/imap/message/copy.rs b/src/imap/message/copy.rs index 91812d3a..2109c277 100644 --- a/src/imap/message/copy.rs +++ b/src/imap/message/copy.rs @@ -1,6 +1,9 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::mailbox::Mailbox; +use io_imap::{ + rfc3501::{copy::ImapMessageCopyOptions, select::ImapMailboxSelectOptions}, + types::mailbox::Mailbox, +}; use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ @@ -35,13 +38,17 @@ impl ImapMessageCopyCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let sequence_set = self.sequence_set.as_str().try_into()?; let destination: Mailbox = self.mailbox_dest_name.inner.try_into()?; - client.copy(sequence_set, destination, !self.seq)?; + client.copy( + sequence_set, + destination, + ImapMessageCopyOptions { uid: !self.seq }, + )?; printer.out(Message::new("Message(s) successfully copied")) } diff --git a/src/imap/message/export.rs b/src/imap/message/export.rs index 0beba746..9aa299e3 100644 --- a/src/imap/message/export.rs +++ b/src/imap/message/export.rs @@ -6,7 +6,10 @@ use std::{ use anyhow::{Result, bail}; use clap::Parser; -use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, +}; use mail_parser::{MessageParser, MimeHeaders}; use pimalaya_cli::printer::{Message, Printer}; @@ -65,7 +68,7 @@ impl ImapMessageExportCommand { ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; if self.id == 0 { bail!("Export message error: ID must be non-zero"); @@ -79,7 +82,14 @@ impl ImapMessageExportCommand { }]); let sequence_set = self.id.to_string().parse()?; - let mut data = client.fetch(sequence_set, item_names, !self.seq)?; + let mut data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid: !self.seq, + modifiers: Vec::new(), + }, + )?; let Some((_, items)) = data.pop_first() else { bail!( diff --git a/src/imap/message/get.rs b/src/imap/message/get.rs index 8bc08f69..ac577245 100644 --- a/src/imap/message/get.rs +++ b/src/imap/message/get.rs @@ -3,7 +3,10 @@ use std::fmt; use anyhow::{Result, bail}; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table, presets}; -use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, +}; use mail_parser::{Addr, Address, ContentType, Message, MessageParser, MimeHeaders}; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -36,7 +39,7 @@ impl ImapMessageGetCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let item_names = @@ -47,7 +50,14 @@ impl ImapMessageGetCommand { }]); let sequence_set = self.id.parse()?; - let mut data = client.fetch(sequence_set, item_names, !self.seq)?; + let mut data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid: !self.seq, + modifiers: Vec::new(), + }, + )?; let Some((_, items)) = data.pop_first() else { bail!("Get message `{}` error: no message data returned", self.id); diff --git a/src/imap/message/move.rs b/src/imap/message/move.rs index 0dfbd780..bad69841 100644 --- a/src/imap/message/move.rs +++ b/src/imap/message/move.rs @@ -1,6 +1,9 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::mailbox::Mailbox; +use io_imap::{ + rfc3501::select::ImapMailboxSelectOptions, rfc6851::r#move::ImapMessageMoveOptions, + types::mailbox::Mailbox, +}; use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ @@ -36,13 +39,17 @@ impl ImapMessageMoveCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let sequence_set = self.sequence_set.as_str().try_into()?; let destination: Mailbox<'static> = self.mailbox_dest_name.inner.try_into()?; - client.r#move(sequence_set, destination, !self.seq)?; + client.r#move( + sequence_set, + destination, + ImapMessageMoveOptions { uid: !self.seq }, + )?; printer.out(Message::new("Message(s) successfully moved")) } diff --git a/src/imap/message/read.rs b/src/imap/message/read.rs index ce1cb79e..b2dc3eaa 100644 --- a/src/imap/message/read.rs +++ b/src/imap/message/read.rs @@ -2,7 +2,10 @@ use std::fmt; use anyhow::{Result, bail}; use clap::Parser; -use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; +use io_imap::{ + rfc3501::{fetch::ImapMessageFetchOptions, select::ImapMailboxSelectOptions}, + types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, +}; use mail_parser::{Message, MessageParser}; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -41,7 +44,7 @@ impl ImapMessageReadCommand { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { - client.select(mailbox)?; + client.select(mailbox, ImapMailboxSelectOptions::default())?; } let item_names = @@ -52,7 +55,14 @@ impl ImapMessageReadCommand { }]); let sequence_set = self.id.parse()?; - let mut data = client.fetch(sequence_set, item_names, !self.seq)?; + let mut data = client.fetch( + sequence_set, + item_names, + ImapMessageFetchOptions { + uid: !self.seq, + modifiers: Vec::new(), + }, + )?; let Some((_, items)) = data.pop_first() else { bail!("Read message `{}` error: no message data returned", self.id); diff --git a/src/imap/message/save.rs b/src/imap/message/save.rs index 5769d688..6a3a7372 100644 --- a/src/imap/message/save.rs +++ b/src/imap/message/save.rs @@ -1,7 +1,8 @@ use anyhow::Result; use clap::Parser; -use io_imap::types::{ - IntoStatic, core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, +use io_imap::{ + rfc3501::append::ImapMessageAppendOptions, + types::{IntoStatic, flag::Flag, mailbox::Mailbox}, }; use pimalaya_cli::printer::{Message, Printer}; @@ -32,17 +33,23 @@ impl ImapMessageSaveCommand { pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?; let message = self.message.parse()?; - let message = Literal::try_from(message)?; - let message = LiteralOrLiteral8::Literal(message); - let flags: Vec<_> = self + let flags: Vec> = self .flag .iter() .map(String::as_str) .map(|f| Flag::try_from(f).map(IntoStatic::into_static)) .collect::>()?; - client.append(mailbox, flags, None, message)?; + client.append( + mailbox, + message.as_bytes(), + ImapMessageAppendOptions { + flags, + date: None, + non_sync: false, + }, + )?; printer.out(Message::new("Message successfully saved")) } diff --git a/src/wizard/account.rs b/src/wizard/account.rs index bd5916b4..cdb61b31 100644 --- a/src/wizard/account.rs +++ b/src/wizard/account.rs @@ -34,6 +34,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result { alpn: io_imap::client::default_alpn(), sasl, id: Default::default(), + sort: Default::default(), }) }