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
This commit is contained in:
Clément DOUIN
2026-06-12 10:54:22 +02:00
parent e1885d2cda
commit b350955f21
26 changed files with 301 additions and 134 deletions
Generated
+26 -29
View File
@@ -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",
+2
View File
@@ -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"
+6
View File
@@ -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
+2 -10
View File
@@ -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"
+24 -10
View File
@@ -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<String>,
/// 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<SaslConfig>,
/// 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<bool>,
}
/// Per-account `imap.id.*` quirks.
+22 -7
View File
@@ -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<Capability<'static>>,
sort_fallback: Option<bool>,
}
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<Self> {
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(_)))
}
}
+15 -5
View File
@@ -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);
+20 -8
View File
@@ -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(),
+9 -6
View File
@@ -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(),
+17 -5
View File
@@ -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);
+22 -7
View File
@@ -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<u32, String> = HashMap::new();
+13 -5
View File
@@ -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::<Result<_, _>>()?;
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"))
}
+5 -2
View File
@@ -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();
+13 -5
View File
@@ -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::<Result<_, _>>()?;
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"))
}
+13 -5
View File
@@ -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::<Result<_, _>>()?;
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"))
}
+9 -4
View File
@@ -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(),
+2 -1
View File
@@ -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()?;
+6 -3
View File
@@ -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()?;
+2 -1
View File
@@ -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"))
}
}
+10 -3
View File
@@ -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"))
}
+13 -3
View File
@@ -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!(
+13 -3
View File
@@ -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);
+10 -3
View File
@@ -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"))
}
+13 -3
View File
@@ -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);
+13 -6
View File
@@ -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<Flag<'static>> = self
.flag
.iter()
.map(String::as_str)
.map(|f| Flag::try_from(f).map(IntoStatic::into_static))
.collect::<Result<_, _>>()?;
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"))
}
+1
View File
@@ -34,6 +34,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result<ImapConfig> {
alpn: io_imap::client::default_alpn(),
sasl,
id: Default::default(),
sort: Default::default(),
})
}