clean implem part 1

This commit is contained in:
Clément DOUIN
2026-03-09 22:21:20 +01:00
parent eb6b721ba6
commit 0ad22c8630
27 changed files with 820 additions and 747 deletions
Generated
+61
View File
@@ -302,6 +302,7 @@ version = "7.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
dependencies = [ dependencies = [
"crossterm",
"unicode-segmentation", "unicode-segmentation",
"unicode-width 0.2.2", "unicode-width 0.2.2",
] ]
@@ -322,6 +323,29 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags",
"crossterm_winapi",
"document-features",
"parking_lot",
"rustix",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@@ -354,6 +378,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.5" version = "1.0.5"
@@ -942,6 +975,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@@ -2092,6 +2131,22 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -2101,6 +2156,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
+1 -1
View File
@@ -34,7 +34,7 @@ pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["b
anyhow = "1" anyhow = "1"
chrono = { version = "0.4", default-features = false } chrono = { version = "0.4", default-features = false }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
comfy-table = { version = "7", default-features = false } comfy-table = "7"
dirs = "6" dirs = "6"
gethostname = "1" gethostname = "1"
html2text = "0.12" html2text = "0.12"
+8 -1
View File
@@ -2,14 +2,16 @@ use std::{env::temp_dir, path::PathBuf};
use crate::config::{AccountConfig, Config}; use crate::config::{AccountConfig, Config};
use anyhow::Result; use anyhow::Result;
use comfy_table::presets; use comfy_table::{presets, ContentArrangement};
use dirs::download_dir; use dirs::download_dir;
#[derive(Debug)] #[derive(Debug)]
pub struct Account<B> { pub struct Account<B> {
pub backend: B, pub backend: B,
pub downloads_dir: PathBuf, pub downloads_dir: PathBuf,
pub table_preset: String, pub table_preset: String,
pub table_arrangement: ContentArrangement,
} }
impl<B> Account<B> { impl<B> Account<B> {
@@ -36,6 +38,11 @@ impl<B> Account<B> {
.table_preset .table_preset
.or(account_config.table_preset) .or(account_config.table_preset)
.unwrap_or(presets::UTF8_FULL_CONDENSED.to_string()), .unwrap_or(presets::UTF8_FULL_CONDENSED.to_string()),
table_arrangement: config
.table_arrangement
.or(account_config.table_arrangement)
.unwrap_or_default()
.into(),
}) })
} }
} }
+22
View File
@@ -1,6 +1,7 @@
use std::{collections::HashMap, fmt, path::PathBuf, process::Command}; use std::{collections::HashMap, fmt, path::PathBuf, process::Command};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use comfy_table::ContentArrangement;
use pimalaya_toolbox::config::TomlConfig; use pimalaya_toolbox::config::TomlConfig;
use secrecy::SecretString; use secrecy::SecretString;
use serde::{de::Visitor, Deserialize, Deserializer}; use serde::{de::Visitor, Deserialize, Deserializer};
@@ -14,6 +15,7 @@ use url::Url;
pub struct Config { pub struct Config {
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>, pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
pub accounts: HashMap<String, AccountConfig>, pub accounts: HashMap<String, AccountConfig>,
} }
@@ -47,11 +49,31 @@ pub struct AccountConfig {
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>, pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
pub imap: Option<ImapConfig>, pub imap: Option<ImapConfig>,
pub smtp: Option<SmtpConfig>, pub smtp: Option<SmtpConfig>,
} }
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum TableArrangementConfig {
#[default]
Dynamic,
DynamicFullWidth,
Disabled,
}
impl From<TableArrangementConfig> for ContentArrangement {
fn from(arrangement: TableArrangementConfig) -> Self {
match arrangement {
TableArrangementConfig::Dynamic => ContentArrangement::Dynamic,
TableArrangementConfig::DynamicFullWidth => ContentArrangement::DynamicFullWidth,
TableArrangementConfig::Disabled => ContentArrangement::Disabled,
}
}
}
/// IMAP configuration. /// IMAP configuration.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
+10 -9
View File
@@ -7,33 +7,34 @@ use crate::imap::{
id::IdCommand, mailbox::command::MailboxCommand, message::command::MessageCommand, id::IdCommand, mailbox::command::MailboxCommand, message::command::MessageCommand,
}; };
/// IMAP CLI (requires `imap` cargo feature). /// IMAP CLI (requires the `imap` cargo feature).
/// ///
/// This command gives you access to the IMAP CLI API, and allows /// This command gives you access to the IMAP CLI API, and allows you
/// you to manage IMAP mailboxes: list mailboxes, read messages, /// to manage IMAP mailboxes, envelopes, flags, messages etc.
/// add flags etc.
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")] #[command(rename_all = "lowercase")]
pub enum ImapCommand { pub enum ImapCommand {
Id(IdCommand),
#[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)] #[command(subcommand)]
Envelopes(EnvelopeCommand), Envelopes(EnvelopeCommand),
#[command(subcommand)] #[command(subcommand)]
Flags(FlagCommand), Flags(FlagCommand),
#[command(subcommand)] #[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)]
#[command(aliases = ["msgs", "msg"])] #[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand), Messages(MessageCommand),
Id(IdCommand),
} }
impl ImapCommand { impl ImapCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
match self { match self {
Self::Id(cmd) => cmd.exec(printer, account),
Self::Envelopes(cmd) => cmd.exec(printer, account), Self::Envelopes(cmd) => cmd.exec(printer, account),
Self::Flags(cmd) => cmd.exec(printer, account), Self::Flags(cmd) => cmd.exec(printer, account),
Self::Id(cmd) => cmd.exec(printer, account),
Self::Mailboxes(cmd) => cmd.exec(printer, account), Self::Mailboxes(cmd) => cmd.exec(printer, account),
Self::Messages(cmd) => cmd.exec(printer, account), Self::Messages(cmd) => cmd.exec(printer, account),
} }
+4 -4
View File
@@ -10,15 +10,15 @@ use crate::imap::{
}, },
}; };
/// Manage message envelopes. /// Manage IMAP envelopes.
/// ///
/// An envelope contains header information about a message such as /// An envelope contains header information about a message such as
/// date, subject, from, to, cc, bcc, etc. This subcommand allows you /// date, subject, from, to, cc, bcc, etc. This subcommand allows you
/// to list, get, search, sort, and thread envelopes. /// to get, list, search, sort, and thread envelopes.
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum EnvelopeCommand { pub enum EnvelopeCommand {
List(ListEnvelopesCommand),
Get(GetEnvelopeCommand), Get(GetEnvelopeCommand),
List(ListEnvelopesCommand),
Search(SearchEnvelopesCommand), Search(SearchEnvelopesCommand),
Sort(SortEnvelopesCommand), Sort(SortEnvelopesCommand),
Thread(ThreadEnvelopesCommand), Thread(ThreadEnvelopesCommand),
@@ -27,8 +27,8 @@ pub enum EnvelopeCommand {
impl EnvelopeCommand { impl EnvelopeCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
match self { match self {
Self::List(cmd) => cmd.exec(printer, account),
Self::Get(cmd) => cmd.exec(printer, account), Self::Get(cmd) => cmd.exec(printer, account),
Self::List(cmd) => cmd.exec(printer, account),
Self::Search(cmd) => cmd.exec(printer, account), Self::Search(cmd) => cmd.exec(printer, account),
Self::Sort(cmd) => cmd.exec(printer, account), Self::Sort(cmd) => cmd.exec(printer, account),
Self::Thread(cmd) => cmd.exec(printer, account), Self::Thread(cmd) => cmd.exec(printer, account),
+155 -108
View File
@@ -2,7 +2,7 @@ use std::{fmt, num::NonZeroU32};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; use comfy_table::{Cell, Row, Table};
use io_imap::{ use io_imap::{
coroutines::{fetch::*, select::*}, coroutines::{fetch::*, select::*},
types::{ types::{
@@ -12,16 +12,16 @@ use io_imap::{
}; };
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer}; use serde::Serialize;
use crate::imap::{ use crate::imap::{
account::ImapAccount, account::ImapAccount,
envelope::list::{decode_mime, format_addresses}, envelope::list::{decode_mime, format_address},
mailbox::arg::MailboxNameOptionalFlag, mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream, stream,
}; };
/// Get a single message envelope. /// Get a single IMAP envelope.
/// ///
/// This command displays detailed envelope information for a specific /// This command displays detailed envelope information for a specific
/// message, including all header fields like date, subject, from, to, /// message, including all header fields like date, subject, from, to,
@@ -30,11 +30,12 @@ use crate::imap::{
pub struct GetEnvelopeCommand { pub struct GetEnvelopeCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq). /// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")] #[arg(name = "id", value_name = "ID")]
pub id: u32, pub id: u32,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
pub seq: bool, pub seq: bool,
@@ -42,25 +43,27 @@ pub struct GetEnvelopeCommand {
impl GetEnvelopeCommand { impl GetEnvelopeCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
};
}
let Some(id) = NonZeroU32::new(self.id) else {
bail!("ID must be non-zero");
}; };
// FETCH envelope
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]); MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
@@ -75,109 +78,153 @@ impl GetEnvelopeCommand {
} }
}; };
let table = EnvelopeDetailTable::new(items); let table = EnvelopeTable {
preset: account.table_preset,
envelope: items.into(),
};
printer.out(table)?; printer.out(table)
Ok(())
} }
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct EnvelopeDetail { pub struct EnvelopeTable {
pub date: String, #[serde(skip)]
pub subject: String, pub preset: String,
pub message_id: String, pub envelope: EnvelopeTableItems,
pub in_reply_to: String,
pub from: String,
pub sender: String,
pub reply_to: String,
pub to: String,
pub cc: String,
pub bcc: String,
} }
pub struct EnvelopeDetailTable { impl fmt::Display for EnvelopeTable {
detail: EnvelopeDetail,
}
impl EnvelopeDetailTable {
pub fn new(items: Vec1<MessageDataItem<'static>>) -> Self {
let mut detail = EnvelopeDetail {
date: String::new(),
subject: String::new(),
message_id: String::new(),
in_reply_to: String::new(),
from: String::new(),
sender: String::new(),
reply_to: String::new(),
to: String::new(),
cc: String::new(),
bcc: String::new(),
};
for item in items.into_iter() {
if let MessageDataItem::Envelope(env) = item {
// NString wraps Option<IString>, access via .0
if let Some(d) = &env.date.0 {
detail.date = String::from_utf8_lossy(d.as_ref()).to_string();
}
if let Some(s) = &env.subject.0 {
detail.subject = decode_mime(&String::from_utf8_lossy(s.as_ref()));
}
if let Some(m) = &env.message_id.0 {
detail.message_id = String::from_utf8_lossy(m.as_ref()).to_string();
}
if let Some(r) = &env.in_reply_to.0 {
detail.in_reply_to = String::from_utf8_lossy(r.as_ref()).to_string();
}
detail.from = format_addresses(&env.from);
detail.sender = format_addresses(&env.sender);
detail.reply_to = format_addresses(&env.reply_to);
detail.to = format_addresses(&env.to);
detail.cc = format_addresses(&env.cc);
detail.bcc = format_addresses(&env.bcc);
}
}
Self { detail }
}
}
impl fmt::Display for EnvelopeDetailTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new(); let mut table = Table::new();
table table
.load_preset(presets::ASCII_MARKDOWN) .load_preset(&self.preset)
.set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_header(Row::from([Cell::new("HEADER"), Cell::new("VALUE")]));
.set_header(Row::from([Cell::new("FIELD"), Cell::new("VALUE")]));
let fields = [ table.add_row(Row::from([
("Date", &self.detail.date), Cell::new("Message ID"),
("Subject", &self.detail.subject), match &self.envelope.message_id {
("Message-ID", &self.detail.message_id), Some(id) => Cell::new(id),
("From", &self.detail.from), None => Cell::new(""),
("Sender", &self.detail.sender), },
("To", &self.detail.to), ]));
("Cc", &self.detail.cc),
("Bcc", &self.detail.bcc),
("Reply-To", &self.detail.reply_to),
("In-Reply-To", &self.detail.in_reply_to),
];
for (name, value) in fields { table.add_row(Row::from([
table.add_row(Row::from([Cell::new(name), Cell::new(value)])); Cell::new("In Reply To"),
match &self.envelope.in_reply_to {
Some(id) => Cell::new(id),
None => Cell::new(""),
},
]));
table.add_row(Row::from([
Cell::new("Date"),
match &self.envelope.date {
Some(date) => Cell::new(date),
None => Cell::new(""),
},
]));
table.add_row(Row::from([
Cell::new("Subject"),
match &self.envelope.subject {
Some(subject) => Cell::new(subject),
None => Cell::new(""),
},
]));
table.add_row(Row::from([
Cell::new("Sender"),
Cell::new(self.envelope.sender.join(", ")),
]));
table.add_row(Row::from([
Cell::new("From"),
Cell::new(self.envelope.from.join(", ")),
]));
table.add_row(Row::from([
Cell::new("Reply To"),
Cell::new(self.envelope.reply_to.join(", ")),
]));
table.add_row(Row::from([
Cell::new("To"),
Cell::new(self.envelope.to.join(", ")),
]));
table.add_row(Row::from([
Cell::new("Cc"),
Cell::new(self.envelope.cc.join(", ")),
]));
table.add_row(Row::from([
Cell::new("Bcc"),
Cell::new(self.envelope.bcc.join(", ")),
]));
writeln!(f)?;
writeln!(f, "{table}")
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct EnvelopeTableItems {
pub date: Option<String>,
pub subject: Option<String>,
pub message_id: Option<String>,
pub in_reply_to: Option<String>,
pub from: Vec<String>,
pub sender: Vec<String>,
pub reply_to: Vec<String>,
pub to: Vec<String>,
pub cc: Vec<String>,
pub bcc: Vec<String>,
}
impl From<Vec1<MessageDataItem<'_>>> for EnvelopeTableItems {
fn from(items: Vec1<MessageDataItem<'_>>) -> Self {
let mut table = EnvelopeTableItems::default();
for item in items.into_iter() {
if let MessageDataItem::Envelope(env) = item {
if let Some(d) = &env.date.into_option() {
table
.date
.replace(String::from_utf8_lossy(d.as_ref()).to_string());
}
if let Some(s) = &env.subject.into_option() {
table
.subject
.replace(decode_mime(&String::from_utf8_lossy(s.as_ref())));
}
if let Some(m) = &env.message_id.into_option() {
table
.message_id
.replace(String::from_utf8_lossy(m.as_ref()).to_string());
}
if let Some(r) = &env.in_reply_to.into_option() {
table
.in_reply_to
.replace(String::from_utf8_lossy(r.as_ref()).to_string());
}
table.from.extend(env.from.iter().map(format_address));
table.sender.extend(env.sender.iter().map(format_address));
table
.reply_to
.extend(env.reply_to.iter().map(format_address));
table.to.extend(env.to.iter().map(format_address));
table.cc.extend(env.cc.iter().map(format_address));
table.bcc.extend(env.bcc.iter().map(format_address));
}
} }
writeln!(f)?; table
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for EnvelopeDetailTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.detail.serialize(serializer)
} }
} }
+109 -116
View File
@@ -1,8 +1,8 @@
use std::fmt; use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{ use io_imap::{
coroutines::{fetch::*, select::*}, coroutines::{fetch::*, select::*},
types::{ types::{
@@ -16,23 +16,15 @@ use io_stream::runtimes::std::handle;
use log::debug; use log::debug;
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use rfc2047_decoder::{Decoder, RecoverStrategy}; use rfc2047_decoder::{Decoder, RecoverStrategy};
use serde::{Serialize, Serializer}; use serde::Serialize;
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalArg, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalArg, MailboxSelectFlag},
stream,
};
/// Decode RFC 2047 MIME-encoded string, falling back to original on error. /// List IMAP envelopes from the given mailbox.
pub fn decode_mime(s: &str) -> String {
let decoder = Decoder::new().too_long_encoded_word_strategy(RecoverStrategy::Decode);
match decoder.decode(s.as_bytes()) {
Ok(s) => s,
Err(err) => {
debug!("cannot decode rfc2047 string `{s}`: {err}");
s.to_string()
}
}
}
/// List message envelopes in a mailbox.
/// ///
/// This command displays envelopes for messages in the specified /// This command displays envelopes for messages in the specified
/// mailbox. You can specify a sequence set to limit which messages /// mailbox. You can specify a sequence set to limit which messages
@@ -41,11 +33,12 @@ pub fn decode_mime(s: &str) -> String {
pub struct ListEnvelopesCommand { pub struct ListEnvelopesCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalArg, pub mailbox: MailboxNameOptionalArg,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (default: "1:*" for all). /// The sequence set of envelopes.
#[arg(short, long, default_value = "1:*")] #[arg(short, long, default_value = "1:*")]
pub sequence: String, pub sequence: String,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
pub seq: bool, pub seq: bool,
@@ -53,26 +46,24 @@ pub struct ListEnvelopesCommand {
impl ListEnvelopesCommand { impl ListEnvelopesCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse sequence set
let sequence_set: SequenceSet = self.sequence.parse()?; let sequence_set: SequenceSet = self.sequence.parse()?;
// FETCH envelopes
let item_names = let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]); MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
@@ -87,75 +78,27 @@ impl ListEnvelopesCommand {
} }
}; };
let table = EnvelopesTable::new(data, !self.seq); let table = EnvelopesTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
envelopes: map_envelopes_table_entries(!self.seq, data),
uid_mode: !self.seq,
};
printer.out(table)?; printer.out(table)
Ok(())
} }
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct EnvelopeEntry {
pub id: u32,
pub date: String,
pub from: String,
pub subject: String,
}
pub struct EnvelopesTable { pub struct EnvelopesTable {
entries: Vec<EnvelopeEntry>, #[serde(skip)]
preset: String,
#[serde(skip)]
arrangement: ContentArrangement,
envelopes: Vec<EnvelopesTableEntry>,
uid_mode: bool, uid_mode: bool,
} }
impl EnvelopesTable {
pub fn new(
data: std::collections::HashMap<std::num::NonZeroU32, Vec1<MessageDataItem<'static>>>,
uid_mode: bool,
) -> Self {
let mut entries: Vec<EnvelopeEntry> = 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();
for item in items.into_iter() {
match item {
MessageDataItem::Uid(uid) => {
if uid_mode {
id = uid.get();
}
}
MessageDataItem::Envelope(env) => {
// NString wraps Option<IString>, access via .0
if let Some(d) = &env.date.0 {
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());
}
from = format_addresses_short(&env.from);
}
_ => {}
}
}
EnvelopeEntry {
id,
date,
from,
subject,
}
})
.collect();
entries.sort_by_key(|e| e.id);
Self { entries, uid_mode }
}
}
impl fmt::Display for EnvelopesTable { impl fmt::Display for EnvelopesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new(); let mut table = Table::new();
@@ -163,35 +106,94 @@ impl fmt::Display for EnvelopesTable {
let id_header = if self.uid_mode { "UID" } else { "SEQ" }; let id_header = if self.uid_mode { "UID" } else { "SEQ" };
table table
.load_preset(presets::ASCII_FULL) .load_preset(&self.preset)
.set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([ .set_header(Row::from([
Cell::new(id_header), Cell::new(id_header),
Cell::new("DATE"), Cell::new("Subject"),
Cell::new("FROM"), Cell::new("From"),
Cell::new("SUBJECT"), Cell::new("Date"),
])); ]));
for entry in &self.entries { for entry in &self.envelopes {
let mut row = Row::new(); let mut row = Row::new();
row.max_height(1); row.max_height(1);
row.add_cell(Cell::new(entry.id)); row.add_cell(Cell::new(entry.id));
row.add_cell(Cell::new(&entry.date));
row.add_cell(Cell::new(&entry.from));
row.add_cell(Cell::new(&entry.subject)); row.add_cell(Cell::new(&entry.subject));
row.add_cell(Cell::new(&entry.from));
row.add_cell(Cell::new(&entry.date));
table.add_row(row); table.add_row(row);
} }
writeln!(f)?; writeln!(f)?;
write!(f, "{table}")?; writeln!(f, "{table}")
writeln!(f)?;
Ok(())
} }
} }
impl Serialize for EnvelopesTable { #[derive(Clone, Debug, Serialize)]
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { pub struct EnvelopesTableEntry {
self.entries.serialize(serializer) pub id: u32,
pub date: String,
pub from: String,
pub subject: String,
}
fn map_envelopes_table_entries(
uid_mode: bool,
data: HashMap<NonZeroU32, Vec1<MessageDataItem<'_>>>,
) -> Vec<EnvelopesTableEntry> {
let mut entries: Vec<EnvelopesTableEntry> = 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();
for item in items.into_iter() {
match item {
MessageDataItem::Uid(uid) => {
if uid_mode {
id = uid.get();
}
}
MessageDataItem::Envelope(env) => {
// NString wraps Option<IString>, access via .0
if let Some(d) = &env.date.0 {
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());
}
from = format_addresses_short(&env.from);
}
_ => {}
}
}
EnvelopesTableEntry {
id,
date,
from,
subject,
}
})
.collect();
entries.sort_by_key(|e| e.id);
entries.reverse();
entries
}
/// Decode RFC 2047 MIME-encoded string, falling back to original on error.
pub fn decode_mime(s: &str) -> String {
let decoder = Decoder::new().too_long_encoded_word_strategy(RecoverStrategy::Decode);
match decoder.decode(s.as_bytes()) {
Ok(s) => s,
Err(err) => {
debug!("cannot decode rfc2047 string `{s}`: {err}");
s.to_string()
}
} }
} }
@@ -250,12 +252,3 @@ pub fn format_addresses_short(addrs: &[Address<'_>]) -> String {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
} }
/// Full addresses formatter for detailed view.
pub fn format_addresses(addrs: &[Address<'_>]) -> String {
addrs
.iter()
.map(format_address)
.collect::<Vec<_>>()
.join(", ")
}
+76 -75
View File
@@ -1,8 +1,8 @@
use std::fmt; use std::fmt;
use anyhow::{bail, Result}; use anyhow::{anyhow, bail, Result};
use clap::Parser; use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{ use io_imap::{
coroutines::{search::*, select::*}, coroutines::{search::*, select::*},
types::{ types::{
@@ -13,11 +13,15 @@ use io_imap::{
}; };
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer}; use serde::Serialize;
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalArg, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Search messages by criteria. /// Search IMAP messages by criteria.
/// ///
/// This command searches for messages matching the given criteria and /// This command searches for messages matching the given criteria and
/// returns a list of matching sequence numbers or UIDs. /// returns a list of matching sequence numbers or UIDs.
@@ -45,7 +49,9 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalArg, st
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct SearchEnvelopesCommand { pub struct SearchEnvelopesCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalArg, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// Search query (e.g., "from:alice unseen"). /// Search query (e.g., "from:alice unseen").
#[arg(name = "query", value_name = "QUERY", default_value = "all")] #[arg(name = "query", value_name = "QUERY", default_value = "all")]
@@ -58,26 +64,25 @@ pub struct SearchEnvelopesCommand {
impl SearchEnvelopesCommand { impl SearchEnvelopesCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse query into search criteria
let criteria = parse_query(&self.query)?; let criteria = parse_query(&self.query)?;
// SEARCH
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSearch::new(context, criteria, !self.seq); let mut coroutine = ImapSearch::new(context, criteria, !self.seq);
@@ -89,9 +94,54 @@ impl SearchEnvelopesCommand {
} }
}; };
let table = SearchResultsTable::new(ids, !self.seq); let table = SearchTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
ids: ids
.into_iter()
.map(|id| SearchResult { id: id.get() })
.collect(),
uid_mode: !self.seq,
};
printer.out(table)?; printer.out(table)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct SearchResult {
pub id: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct SearchTable {
#[serde(skip)]
preset: String,
#[serde(skip)]
arrangement: ContentArrangement,
uid_mode: bool,
ids: Vec<SearchResult>,
}
impl fmt::Display for SearchTable {
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)]));
for result in &self.ids {
table.add_row(Row::from([Cell::new(result.id)]));
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
writeln!(f, "Found {} message(s)", self.ids.len())?;
Ok(()) Ok(())
} }
} }
@@ -206,67 +256,18 @@ fn parse_date(s: &str) -> Result<NaiveDate> {
let year: i32 = parts[0] let year: i32 = parts[0]
.parse() .parse()
.map_err(|_| anyhow::anyhow!("Invalid year in date '{s}'"))?; .map_err(|_| anyhow!("Invalid year in date '{s}'"))?;
let month: u32 = parts[1] let month: u32 = parts[1]
.parse() .parse()
.map_err(|_| anyhow::anyhow!("Invalid month in date '{s}'"))?; .map_err(|_| anyhow!("Invalid month in date '{s}'"))?;
let day: u32 = parts[2] let day: u32 = parts[2]
.parse() .parse()
.map_err(|_| anyhow::anyhow!("Invalid day in date '{s}'"))?; .map_err(|_| anyhow!("Invalid day in date '{s}'"))?;
// Create chrono::NaiveDate first // Create chrono::NaiveDate first
let chrono_date = chrono::NaiveDate::from_ymd_opt(year, month, day) let chrono_date = chrono::NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| anyhow::anyhow!("Invalid date '{s}'"))?; .ok_or_else(|| anyhow!("Invalid date '{s}'"))?;
// Convert to imap-types NaiveDate // Convert to imap-types NaiveDate
NaiveDate::try_from(chrono_date).map_err(|e| anyhow::anyhow!("Invalid date '{s}': {e}")) NaiveDate::try_from(chrono_date).map_err(|e| anyhow!("Invalid date '{s}': {e}"))
}
#[derive(Clone, Debug, Serialize)]
pub struct SearchResult {
pub id: u32,
}
pub struct SearchResultsTable {
results: Vec<SearchResult>,
uid_mode: bool,
}
impl SearchResultsTable {
pub fn new(ids: Vec<std::num::NonZeroU32>, uid_mode: bool) -> Self {
let results = ids
.into_iter()
.map(|id| SearchResult { id: id.get() })
.collect();
Self { results, uid_mode }
}
}
impl fmt::Display for SearchResultsTable {
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(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([Cell::new(id_header)]));
for result in &self.results {
table.add_row(Row::from([Cell::new(result.id)]));
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
writeln!(f, "Found {} message(s)", self.results.len())?;
Ok(())
}
}
impl Serialize for SearchResultsTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.results.serialize(serializer)
}
} }
+25 -32
View File
@@ -3,6 +3,7 @@ use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use io_imap::{ use io_imap::{
context::ImapContext,
coroutines::{fetch::*, select::*, thread::*}, coroutines::{fetch::*, select::*, thread::*},
types::{ types::{
extensions::thread::{Thread, ThreadingAlgorithm}, extensions::thread::{Thread, ThreadingAlgorithm},
@@ -17,11 +18,11 @@ use serde::{Serialize, Serializer};
use crate::imap::{ use crate::imap::{
account::ImapAccount, account::ImapAccount,
envelope::{list::decode_mime, search::parse_query}, envelope::{list::decode_mime, search::parse_query},
mailbox::arg::MailboxNameOptionalArg, mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream, stream::{self, Stream},
}; };
/// Thread messages by algorithm. /// Thread IMAP messages by algorithm.
/// ///
/// This command groups messages into conversation threads using the /// This command groups messages into conversation threads using the
/// specified threading algorithm. Requires the THREAD IMAP extension. /// specified threading algorithm. Requires the THREAD IMAP extension.
@@ -32,7 +33,9 @@ use crate::imap::{
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct ThreadEnvelopesCommand { pub struct ThreadEnvelopesCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalArg, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// Threading algorithm (orderedsubject or references). /// Threading algorithm (orderedsubject or references).
#[arg(short = 'A', long, default_value = "references")] #[arg(short = 'A', long, default_value = "references")]
@@ -49,29 +52,26 @@ pub struct ThreadEnvelopesCommand {
impl ThreadEnvelopesCommand { impl ThreadEnvelopesCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse threading algorithm
let algorithm = parse_algorithm(&self.algorithm)?; let algorithm = parse_algorithm(&self.algorithm)?;
// Parse search criteria
let search_criteria = parse_query(&self.query)?; let search_criteria = parse_query(&self.query)?;
// THREAD
let mut arg = None; let mut arg = None;
let mut coroutine = ImapThread::new(context, algorithm, search_criteria, !self.seq); let mut coroutine = ImapThread::new(context, algorithm, search_criteria, !self.seq);
@@ -95,10 +95,9 @@ impl ThreadEnvelopesCommand {
HashMap::new() HashMap::new()
}; };
let table = ThreadResultsTable::new(threads, subjects, !self.seq); let table = ThreadResultsTable::new(threads, subjects);
printer.out(table)?; printer.out(table)
Ok(())
} }
} }
@@ -139,8 +138,8 @@ fn collect_thread_ids_recursive(thread: &Thread, ids: &mut Vec<NonZeroU32>) {
} }
fn fetch_subjects( fn fetch_subjects(
stream: &mut stream::Stream, stream: &mut Stream,
context: io_imap::context::ImapContext, context: ImapContext,
ids: &[NonZeroU32], ids: &[NonZeroU32],
uid: bool, uid: bool,
) -> Result<HashMap<u32, String>> { ) -> Result<HashMap<u32, String>> {
@@ -211,17 +210,11 @@ pub struct ThreadEntry {
pub struct ThreadResultsTable { pub struct ThreadResultsTable {
threads: Vec<Thread>, threads: Vec<Thread>,
subjects: HashMap<u32, String>, subjects: HashMap<u32, String>,
#[allow(dead_code)]
uid_mode: bool,
} }
impl ThreadResultsTable { impl ThreadResultsTable {
pub fn new(threads: Vec<Thread>, subjects: HashMap<u32, String>, uid_mode: bool) -> Self { pub fn new(threads: Vec<Thread>, subjects: HashMap<u32, String>) -> Self {
Self { Self { threads, subjects }
threads,
subjects,
uid_mode,
}
} }
fn build_entries(&self) -> Vec<ThreadEntry> { fn build_entries(&self) -> Vec<ThreadEntry> {
+26 -24
View File
@@ -10,24 +10,29 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Add flags to messages. /// Add IMAP flag(s) to message(s).
/// ///
/// This command adds the specified flags to messages identified by /// This command adds the given flags to messages identified by the
/// the given sequence set. /// given sequence set.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct AddFlagsCommand { pub struct AddFlagsCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). /// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")] #[arg(value_name = "SEQUENCE")]
pub sequence_set: String, pub sequence_set: String,
/// The flags to add (e.g., "\\Seen", "\\Flagged"). /// The flags to add (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)] #[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>, pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
@@ -36,33 +41,30 @@ pub struct AddFlagsCommand {
impl AddFlagsCommand { impl AddFlagsCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// First, SELECT the mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse flags let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self let flags: Vec<Flag<'static>> = self
.flags .flag
.iter() .iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None; let mut arg = None;
let mut coroutine = let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Add, flags, !self.seq); ImapStoreSilent::new(context, sequence_set, StoreType::Add, flags, !self.seq);
+2 -3
View File
@@ -10,11 +10,10 @@ use crate::imap::{
}, },
}; };
/// Manage message flags. /// Manage IMAP flags.
/// ///
/// A flag is a label attached to a message. This subcommand allows /// A flag is a label attached to a message. This subcommand allows
/// you to manage them: list available flags, add flags to messages, /// you to manage them.
/// remove flags from messages, etc.
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum FlagCommand { pub enum FlagCommand {
List(ListFlagsCommand), List(ListFlagsCommand),
+49 -35
View File
@@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{ use io_imap::{
coroutines::select::*, coroutines::select::*,
types::flag::{Flag, FlagPerm}, types::flag::{Flag, FlagPerm},
@@ -11,9 +11,9 @@ use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalArg, stream}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
/// List available flags for a mailbox. /// List available IMAP flags for the given mailbox.
/// ///
/// This command displays the flags and permanent flags that are /// This command displays the flags and permanent flags that are
/// available in the given mailbox. These flags come from the SELECT /// available in the given mailbox. These flags come from the SELECT
@@ -21,7 +21,7 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalArg, st
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct ListFlagsCommand { pub struct ListFlagsCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalArg, pub mailbox: MailboxNameArg,
} }
impl ListFlagsCommand { impl ListFlagsCommand {
@@ -47,36 +47,36 @@ impl ListFlagsCommand {
}; };
let table = FlagsTable { let table = FlagsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
flags, flags,
permanent_flags, permanent_flags,
}; };
printer.out(table)?; printer.out(table)
Ok(())
} }
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct FlagEntry { pub struct FlagsTable<'a> {
pub name: String, #[serde(skip_serializing)]
pub permanent: bool, preset: String,
#[serde(skip_serializing)]
arrangement: ContentArrangement,
#[serde(serialize_with = "serialize_flags")]
flags: Vec<Flag<'a>>,
#[serde(serialize_with = "serialize_permanent_flags")]
permanent_flags: Vec<FlagPerm<'a>>,
} }
pub struct FlagsTable { impl FlagsTable<'_> {
flags: Vec<Flag<'static>>, fn build_entries(&self) -> Vec<(String, bool)> {
permanent_flags: Vec<FlagPerm<'static>>,
}
impl FlagsTable {
fn build_entries(&self) -> Vec<FlagEntry> {
let mut entries: BTreeMap<String, bool> = BTreeMap::new(); let mut entries: BTreeMap<String, bool> = BTreeMap::new();
// Add flags
for flag in &self.flags { for flag in &self.flags {
entries.entry(flag.to_string()).or_insert(false); entries.entry(flag.to_string()).or_insert(false);
} }
// Mark permanent flags
for flag in &self.permanent_flags { for flag in &self.permanent_flags {
let name = match flag { let name = match flag {
FlagPerm::Flag(f) => f.to_string(), FlagPerm::Flag(f) => f.to_string(),
@@ -85,38 +85,52 @@ impl FlagsTable {
entries.insert(name, true); entries.insert(name, true);
} }
entries entries.into_iter().collect()
.into_iter()
.map(|(name, permanent)| FlagEntry { name, permanent })
.collect()
} }
} }
impl fmt::Display for FlagsTable { impl fmt::Display for FlagsTable<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new(); let mut table = Table::new();
table table
.load_preset(presets::ASCII_MARKDOWN) .load_preset(&self.preset)
.set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([Cell::new("FLAG"), Cell::new("PERMANENT")])); .set_header(Row::from([Cell::new("FLAG"), Cell::new("PERMANENT")]));
for entry in self.build_entries() { for (flag, perm) in self.build_entries() {
table.add_row(Row::from([ table.add_row(Row::from([
Cell::new(&entry.name), Cell::new(&flag),
Cell::new(if entry.permanent { "true" } else { "" }), Cell::new(if perm { "true" } else { "" }),
])); ]));
} }
writeln!(f)?; writeln!(f)?;
write!(f, "{table}")?; writeln!(f, "{table}")
writeln!(f)?;
Ok(())
} }
} }
impl Serialize for FlagsTable { pub fn serialize_flags<S: Serializer>(
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { flags: &Vec<Flag<'_>>,
self.build_entries().serialize(serializer) serializer: S,
} ) -> Result<S::Ok, S::Error> {
flags
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.serialize(serializer)
}
fn serialize_permanent_flags<S: Serializer>(
flags: &Vec<FlagPerm<'_>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
flags
.iter()
.map(|f| match f {
FlagPerm::Flag(f) => f.to_string(),
FlagPerm::Asterisk => "\\*".to_string(),
})
.collect::<Vec<_>>()
.serialize(serializer)
} }
+25 -23
View File
@@ -10,24 +10,29 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Remove flags from messages. /// Remove IMAP flag(s) from message(s).
/// ///
/// This command removes the specified flags from messages identified /// This command removes the specified flag(s) from message(s)
/// by the given sequence set. /// identified by the given sequence set.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct RemoveFlagsCommand { pub struct RemoveFlagsCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). /// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")] #[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String, pub sequence_set: String,
/// The flags to remove (e.g., "\\Seen", "\\Flagged"). /// The flags to remove (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)] #[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>, pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
@@ -36,33 +41,30 @@ pub struct RemoveFlagsCommand {
impl RemoveFlagsCommand { impl RemoveFlagsCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// First, SELECT the mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse flags let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self let flags: Vec<Flag<'static>> = self
.flags .flag
.iter() .iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None; let mut arg = None;
let mut coroutine = let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Remove, flags, !self.seq); ImapStoreSilent::new(context, sequence_set, StoreType::Remove, flags, !self.seq);
+24 -22
View File
@@ -10,9 +10,13 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Set flags on messages (replacing existing flags). /// Set IMAP flag(s) on message(s), replacing any existing flags.
/// ///
/// This command replaces all existing flags on messages identified by /// This command replaces all existing flags on messages identified by
/// the given sequence set with the specified flags. /// the given sequence set with the specified flags.
@@ -20,14 +24,15 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct SetFlagsCommand { pub struct SetFlagsCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). /// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")] #[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String, pub sequence_set: String,
/// The flags to set (e.g., "\\Seen", "\\Flagged"). /// The flags to set (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)] #[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>, pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
@@ -36,33 +41,30 @@ pub struct SetFlagsCommand {
impl SetFlagsCommand { impl SetFlagsCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// First, SELECT the mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse flags let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self let flags: Vec<Flag<'static>> = self
.flags .flag
.iter() .iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None; let mut arg = None;
let mut coroutine = let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Replace, flags, !self.seq); ImapStoreSilent::new(context, sequence_set, StoreType::Replace, flags, !self.seq);
@@ -75,6 +77,6 @@ impl SetFlagsCommand {
} }
} }
printer.out(Message::new("Flag(s) successfully set")) printer.out(Message::new("Flag(s) successfully replaced"))
} }
} }
+14 -1
View File
@@ -35,6 +35,19 @@ impl Default for MailboxNameOptionalArg {
} }
} }
#[derive(Debug, Parser)]
pub struct MailboxSelectFlag {
/// Select the given mailbox before performing the current action.
///
/// This argument can be omitted when stateful IMAP sessions are
/// used, for example with:
///
/// https://github.com/pimalaya/sirup
#[arg(long = "select", default_value_t)]
#[arg(name = "mailbox_select")]
pub r#true: bool,
}
/// The required mailbox name argument parser. /// The required mailbox name argument parser.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct MailboxNameArg { pub struct MailboxNameArg {
@@ -53,7 +66,7 @@ pub struct SourceMailboxNameOptionalFlag {
} }
/// The target mailbox name argument parser. /// The target mailbox name argument parser.
#[derive(Debug, Parser)] #[derive(Debug, Clone, Parser)]
pub struct TargetMailboxNameArg { pub struct TargetMailboxNameArg {
/// The name of the target mailbox. /// The name of the target mailbox.
#[arg(name = "target_mailbox_name", value_name = "TARGET")] #[arg(name = "target_mailbox_name", value_name = "TARGET")]
+8 -11
View File
@@ -4,7 +4,11 @@ use io_imap::coroutines::{expunge::*, select::*};
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameArg, MailboxSelectFlag},
stream,
};
/// Expunge the given mailbox. /// Expunge the given mailbox.
/// ///
@@ -14,15 +18,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
pub struct ExpungeMailboxCommand { pub struct ExpungeMailboxCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameArg, pub mailbox: MailboxNameArg,
#[command(flatten)]
/// Select the given mailbox before expunging it. pub select: MailboxSelectFlag,
///
/// This argument can be omitted when stateful IMAP sessions are
/// used, for example with:
///
/// https://github.com/pimalaya/sirup
#[arg(short, long, default_value_t)]
pub select: bool,
} }
impl ExpungeMailboxCommand { impl ExpungeMailboxCommand {
@@ -31,7 +28,7 @@ impl ExpungeMailboxCommand {
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
if self.select { if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
+8 -11
View File
@@ -7,7 +7,11 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameArg, MailboxSelectFlag},
stream,
};
/// Shortcut for marking as deleted all envelopes then expunging the /// Shortcut for marking as deleted all envelopes then expunging the
/// given mailbox. /// given mailbox.
@@ -18,15 +22,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
pub struct PurgeMailboxCommand { pub struct PurgeMailboxCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameArg, pub mailbox: MailboxNameArg,
#[command(flatten)]
/// Select the given mailbox before purging it. pub select: MailboxSelectFlag,
///
/// This argument can be omitted when stateful IMAP sessions are
/// used, for example with:
///
/// https://github.com/pimalaya/sirup
#[arg(short, long, default_value_t)]
pub select: bool,
} }
impl PurgeMailboxCommand { impl PurgeMailboxCommand {
@@ -35,7 +32,7 @@ impl PurgeMailboxCommand {
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
if self.select { if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
+5 -8
View File
@@ -5,17 +5,16 @@ use pimalaya_toolbox::terminal::printer::Printer;
use crate::imap::{ use crate::imap::{
account::ImapAccount, account::ImapAccount,
message::{ message::{
copy::CopyMessageCommand, delete::DeleteMessageCommand, export::ExportMessageCommand, copy::CopyMessageCommand, export::ExportMessageCommand, get::GetMessageCommand,
get::GetMessageCommand, r#move::MoveMessageCommand, read::ReadMessageCommand, r#move::MoveMessageCommand, read::ReadMessageCommand, save::SaveMessageCommand,
save::SaveMessageCommand,
}, },
}; };
/// Manage messages. /// Manage IMAP messages.
/// ///
/// A message is a complete email including headers and body. This /// A message is a complete email including headers and body. This
/// subcommand allows you to save, get, read, export, copy, move, and /// subcommand allows you to save, get, read, export, copy, and move
/// delete messages. /// messages.
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum MessageCommand { pub enum MessageCommand {
Save(SaveMessageCommand), Save(SaveMessageCommand),
@@ -24,7 +23,6 @@ pub enum MessageCommand {
Export(ExportMessageCommand), Export(ExportMessageCommand),
Copy(CopyMessageCommand), Copy(CopyMessageCommand),
Move(MoveMessageCommand), Move(MoveMessageCommand),
Delete(DeleteMessageCommand),
} }
impl MessageCommand { impl MessageCommand {
@@ -36,7 +34,6 @@ impl MessageCommand {
Self::Export(cmd) => cmd.exec(printer, account), Self::Export(cmd) => cmd.exec(printer, account),
Self::Copy(cmd) => cmd.exec(printer, account), Self::Copy(cmd) => cmd.exec(printer, account),
Self::Move(cmd) => cmd.exec(printer, account), Self::Move(cmd) => cmd.exec(printer, account),
Self::Delete(cmd) => cmd.exec(printer, account),
} }
} }
} }
+25 -22
View File
@@ -7,24 +7,28 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag, TargetMailboxNameArg},
stream,
};
/// Copy messages to another mailbox. /// Copy IMAP message(s) to the given mailbox.
/// ///
/// This command copies messages identified by the given sequence set /// This command copies message(s) identified by the given sequence
/// from the source mailbox to the destination mailbox. /// set from the source mailbox to the destination mailbox.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct CopyMessageCommand { pub struct CopyMessageCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). /// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")] #[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String, pub sequence_set: String,
#[command(flatten)]
/// The destination mailbox. pub destination: TargetMailboxNameArg,
#[arg(name = "destination", value_name = "DESTINATION")]
pub destination: String,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
@@ -33,27 +37,26 @@ pub struct CopyMessageCommand {
impl CopyMessageCommand { impl CopyMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse sequence set and destination
let sequence_set = self.sequence_set.as_str().try_into()?; let sequence_set = self.sequence_set.as_str().try_into()?;
let destination: Mailbox<'static> = self.destination.try_into()?; let destination: Mailbox = self.destination.name.try_into()?;
// COPY
let mut arg = None; let mut arg = None;
let mut coroutine = ImapCopy::new(context, sequence_set, destination, !self.seq); let mut coroutine = ImapCopy::new(context, sequence_set, destination, !self.seq);
-86
View File
@@ -1,86 +0,0 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{expunge::*, select::*, store::*},
types::flag::{Flag, StoreType},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream};
/// Delete messages from a mailbox.
///
/// This command marks messages as deleted and expunges them from the
/// mailbox. The messages are permanently removed.
#[derive(Debug, Parser)]
pub struct DeleteMessageCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
pub seq: bool,
}
impl DeleteMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop {
match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err),
}
};
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// STORE +FLAGS \Deleted
let mut arg = None;
let mut coroutine = ImapStoreSilent::new(
context,
sequence_set,
StoreType::Add,
vec![Flag::Deleted],
!self.seq,
);
let context = loop {
match coroutine.resume(arg.take()) {
ImapStoreSilentResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapStoreSilentResult::Ok { context, .. } => break context,
ImapStoreSilentResult::Err { err, .. } => bail!(err),
}
};
// EXPUNGE
let mut arg = None;
let mut coroutine = ImapExpunge::new(context);
let expunged = loop {
match coroutine.resume(arg.take()) {
ImapExpungeResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapExpungeResult::Ok { expunged, .. } => break expunged,
ImapExpungeResult::Err { err, .. } => bail!(err),
}
};
printer.out(Message::new(format!(
"{} message(s) successfully deleted",
expunged.len()
)))
}
}
+31 -28
View File
@@ -8,11 +8,15 @@ use io_imap::{
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
}; };
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use mail_parser::{Addr, Address, ContentType, MessageParser, MimeHeaders}; use mail_parser::{Addr, Address, ContentType, Message, MessageParser, MimeHeaders};
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize; use serde::Serialize;
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Get a message and display its structure. /// Get a message and display its structure.
/// ///
@@ -22,11 +26,11 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct GetMessageCommand { pub struct GetMessageCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq). /// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")]
pub id: u32, pub id: u32,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
pub seq: bool, pub seq: bool,
@@ -34,24 +38,25 @@ pub struct GetMessageCommand {
impl GetMessageCommand { impl GetMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
let Some(id) = NonZeroU32::new(self.id) else {
// SELECT mailbox bail!("ID must be non-zero");
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop {
match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err),
}
}; };
// FETCH with BODY.PEEK[] to avoid marking as read if self.select.r#true {
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
context = loop {
match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err),
}
};
}
let item_names = let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt { MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
@@ -71,7 +76,6 @@ impl GetMessageCommand {
} }
}; };
// Extract raw message bytes
let mut raw_message: Option<Vec<u8>> = None; let mut raw_message: Option<Vec<u8>> = None;
for item in items.into_iter() { for item in items.into_iter() {
if let MessageDataItem::BodyExt { data, .. } = item { if let MessageDataItem::BodyExt { data, .. } = item {
@@ -81,17 +85,16 @@ impl GetMessageCommand {
} }
} }
let raw = raw_message.ok_or_else(|| anyhow::anyhow!("No message data returned"))?; let Some(raw) = raw_message else {
bail!("No message found");
};
// Parse message using mail-parser let Some(message) = MessageParser::new().parse(&raw) else {
let message = MessageParser::default() bail!("Invalid message");
.parse(&raw) };
.ok_or_else(|| anyhow::anyhow!("Failed to parse message"))?;
let structure = MessageStructure::from_parsed(&message); let structure = MessageStructure::from_parsed(&message);
printer.out(structure)
printer.out(structure)?;
Ok(())
} }
} }
@@ -124,7 +127,7 @@ pub struct MessageStructure {
} }
impl MessageStructure { impl MessageStructure {
pub fn from_parsed(message: &mail_parser::Message<'_>) -> Self { pub fn from_parsed(message: &Message<'_>) -> Self {
// Extract headers // Extract headers
let headers = MessageHeaders { let headers = MessageHeaders {
date: message.date().map(|d| d.to_rfc3339()), date: message.date().map(|d| d.to_rfc3339()),
-1
View File
@@ -1,6 +1,5 @@
pub mod command; pub mod command;
pub mod copy; pub mod copy;
pub mod delete;
pub mod export; pub mod export;
pub mod get; pub mod get;
pub mod r#move; pub mod r#move;
+23 -20
View File
@@ -7,9 +7,13 @@ use io_imap::{
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag, TargetMailboxNameArg},
stream,
};
/// Move messages to another mailbox. /// Move message(s) to the given mailbox.
/// ///
/// This command moves messages identified by the given sequence set /// This command moves messages identified by the given sequence set
/// from the source mailbox to the destination mailbox. Requires the /// from the source mailbox to the destination mailbox. Requires the
@@ -18,14 +22,14 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct MoveMessageCommand { pub struct MoveMessageCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). /// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")] #[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String, pub sequence_set: String,
#[command(flatten)]
/// The destination mailbox. pub destination: TargetMailboxNameArg,
#[arg(name = "destination", value_name = "DESTINATION")]
pub destination: String,
/// Use sequence numbers instead of UIDs. /// Use sequence numbers instead of UIDs.
#[arg(long)] #[arg(long)]
@@ -34,27 +38,26 @@ pub struct MoveMessageCommand {
impl MoveMessageCommand { impl MoveMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
}; };
}
// Parse sequence set and destination
let sequence_set = self.sequence_set.as_str().try_into()?; let sequence_set = self.sequence_set.as_str().try_into()?;
let destination: Mailbox<'static> = self.destination.try_into()?; let destination: Mailbox<'static> = self.destination.name.try_into()?;
// MOVE
let mut arg = None; let mut arg = None;
let mut coroutine = ImapMove::new(context, sequence_set, destination, !self.seq); let mut coroutine = ImapMove::new(context, sequence_set, destination, !self.seq);
+32 -28
View File
@@ -1,6 +1,6 @@
use std::{fmt, num::NonZeroU32}; use std::{fmt, num::NonZeroU32};
use anyhow::{bail, Result}; use anyhow::{anyhow, bail, Result};
use clap::Parser; use clap::Parser;
use io_imap::{ use io_imap::{
coroutines::{fetch::*, select::*}, coroutines::{fetch::*, select::*},
@@ -11,7 +11,11 @@ use mail_parser::MessageParser;
use pimalaya_toolbox::terminal::printer::Printer; use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize; use serde::Serialize;
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, stream}; use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Read message content. /// Read message content.
/// ///
@@ -21,6 +25,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct ReadMessageCommand { pub struct ReadMessageCommand {
#[command(flatten)] #[command(flatten)]
pub mailbox: MailboxNameOptionalFlag, pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq). /// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")] #[arg(name = "id", value_name = "ID")]
@@ -41,25 +47,27 @@ pub struct ReadMessageCommand {
impl ReadMessageCommand { impl ReadMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?; let (mut context, mut stream) = stream::connect(account.backend)?;
let mailbox = self.mailbox.name.try_into()?; let mailbox = self.mailbox.name.try_into()?;
// SELECT mailbox if self.select.r#true {
let mut arg = None; let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox); let mut coroutine = ImapSelect::new(context, mailbox);
let context = loop { context = loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context, ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err), ImapSelectResult::Err { err, .. } => bail!(err),
} }
};
}
let Some(id) = NonZeroU32::new(self.id) else {
bail!("ID must be non-zero");
}; };
// FETCH with BODY.PEEK[] to avoid marking as read
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt { MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
section: None, section: None,
@@ -78,8 +86,8 @@ impl ReadMessageCommand {
} }
}; };
// Extract raw message bytes
let mut raw_message: Option<Vec<u8>> = None; let mut raw_message: Option<Vec<u8>> = None;
for item in items.into_iter() { for item in items.into_iter() {
if let MessageDataItem::BodyExt { data, .. } = item { if let MessageDataItem::BodyExt { data, .. } = item {
if let Some(data) = data.0 { if let Some(data) = data.0 {
@@ -88,25 +96,23 @@ impl ReadMessageCommand {
} }
} }
let raw = raw_message.ok_or_else(|| anyhow::anyhow!("No message data returned"))?; let Some(raw) = raw_message else {
bail!("No message found");
};
// Parse message using mail-parser let Some(message) = MessageParser::new().parse(&raw) else {
let message = MessageParser::default() bail!("Invalid message");
.parse(&raw) };
.ok_or_else(|| anyhow::anyhow!("Failed to parse message"))?;
let content = if self.html { let content = if self.html {
// Get HTML content
message message
.body_html(0) .body_html(0)
.map(|s| s.to_string()) .map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("No HTML content found"))? .ok_or_else(|| anyhow!("No HTML content found"))?
} else { } else {
// Get plain text, or convert HTML to text
if let Some(text) = message.body_text(0) { if let Some(text) = message.body_text(0) {
text.to_string() text.to_string()
} else if let Some(html) = message.body_html(0) { } else if let Some(html) = message.body_html(0) {
// Convert HTML to text
html2text::from_read(html.as_bytes(), self.width) html2text::from_read(html.as_bytes(), self.width)
} else { } else {
bail!("No text or HTML content found"); bail!("No text or HTML content found");
@@ -114,9 +120,7 @@ impl ReadMessageCommand {
}; };
let output = MessageContent { content }; let output = MessageContent { content };
printer.out(output)
printer.out(output)?;
Ok(())
} }
} }
+40 -41
View File
@@ -1,18 +1,18 @@
use std::io::{stdin, Read}; use std::io::{stdin, BufRead, IsTerminal};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use io_imap::{ use io_imap::{
coroutines::{ coroutines::append::*,
append::*, types::{
select::{ImapSelect, ImapSelectResult}, core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox,
IntoStatic,
}, },
types::{core::Literal, extensions::binary::LiteralOrLiteral8, mailbox::Mailbox},
}; };
use io_stream::runtimes::std::handle; use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer}; use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, stream}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
/// Save a message to a mailbox. /// Save a message to a mailbox.
/// ///
@@ -20,51 +20,50 @@ use crate::imap::{account::ImapAccount, stream};
/// message is read from stdin in RFC 5322 format (raw email). /// message is read from stdin in RFC 5322 format (raw email).
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct SaveMessageCommand { pub struct SaveMessageCommand {
/// The mailbox to save the message to. #[command(flatten)]
#[arg(name = "mailbox", value_name = "MAILBOX")] pub mailbox: MailboxNameArg,
pub mailbox: String,
/// Select the given mailbox before saving message into it. /// The flags to add to the message.
/// #[arg(short, long, num_args = 0..)]
/// This argument can be omitted when stateful IMAP sessions are pub flag: Vec<String>,
/// used, for example with:
/// /// The raw message, including headers and body.
/// https://github.com/pimalaya/sirup #[arg(trailing_var_arg = true)]
#[arg(short, long, default_value_t)] #[arg(name = "message", value_name = "MESSAGE")]
pub select: bool, pub message: Vec<String>,
} }
impl SaveMessageCommand { impl SaveMessageCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (mut context, mut stream) = stream::connect(account.backend)?; let (context, mut stream) = stream::connect(account.backend)?;
// Read message from stdin let mailbox: Mailbox<'static> = self.mailbox.name.try_into()?;
let mut message = Vec::new();
stdin().read_to_end(&mut message)?;
if message.is_empty() { let message = if stdin().is_terminal() || printer.is_json() {
bail!("No message provided on stdin"); self.message
} .join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let message = Literal::try_from(message)?;
let message = LiteralOrLiteral8::Literal(message);
let mailbox: Mailbox<'static> = self.mailbox.try_into()?; let flags: Vec<_> = self
let literal = Literal::try_from(message)?; .flag
let message = LiteralOrLiteral8::Literal(literal); .iter()
.map(String::as_str)
if self.select { .map(|f| Flag::try_from(f).map(IntoStatic::into_static))
let mut arg = None; .collect::<Result<_, _>>()?;
let mut coroutine = ImapSelect::new(context, mailbox.clone());
context = loop {
match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err),
}
};
}
let mut arg = None; let mut arg = None;
let mut coroutine = ImapAppend::new(context, mailbox, vec![], None, message); let mut coroutine = ImapAppend::new(context, mailbox, flags, None, message);
loop { loop {
match coroutine.resume(arg.take()) { match coroutine.resume(arg.take()) {
+37 -37
View File
@@ -43,43 +43,6 @@ pub enum Stream {
NativeTls(native_tls::TlsStream<TcpStream>), NativeTls(native_tls::TlsStream<TcpStream>),
} }
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Tcp(s) => s.read(buf),
Self::Unix(s) => s.read(buf),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.read(buf),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.read(buf),
}
}
}
impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Tcp(s) => s.write(buf),
Self::Unix(s) => s.write(buf),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.write(buf),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Tcp(s) => s.flush(),
Self::Unix(s) => s.flush(),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.flush(),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.flush(),
}
}
}
pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, Stream)> { pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, Stream)> {
info!("connecting to IMAP server using {}", config.url); info!("connecting to IMAP server using {}", config.url);
@@ -376,3 +339,40 @@ pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, Stream)> {
Ok((context, stream)) Ok((context, stream))
} }
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Tcp(s) => s.read(buf),
Self::Unix(s) => s.read(buf),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.read(buf),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.read(buf),
}
}
}
impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Tcp(s) => s.write(buf),
Self::Unix(s) => s.write(buf),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.write(buf),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Tcp(s) => s.flush(),
Self::Unix(s) => s.flush(),
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
Self::Rustls(s) => s.flush(),
#[cfg(feature = "native-tls")]
Self::NativeTls(s) => s.flush(),
}
}
}