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"
checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
dependencies = [
"crossterm",
"unicode-segmentation",
"unicode-width 0.2.2",
]
@@ -322,6 +323,29 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "dirs"
version = "6.0.0"
@@ -354,6 +378,15 @@ dependencies = [
"syn",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "dunce"
version = "1.0.5"
@@ -942,6 +975,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2092,6 +2131,22 @@ dependencies = [
"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]]
name = "winapi-util"
version = "0.1.11"
@@ -2101,6 +2156,12 @@ dependencies = [
"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]]
name = "windows-link"
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"
chrono = { version = "0.4", default-features = false }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
comfy-table = { version = "7", default-features = false }
comfy-table = "7"
dirs = "6"
gethostname = "1"
html2text = "0.12"
+8 -1
View File
@@ -2,14 +2,16 @@ use std::{env::temp_dir, path::PathBuf};
use crate::config::{AccountConfig, Config};
use anyhow::Result;
use comfy_table::presets;
use comfy_table::{presets, ContentArrangement};
use dirs::download_dir;
#[derive(Debug)]
pub struct Account<B> {
pub backend: B,
pub downloads_dir: PathBuf,
pub table_preset: String,
pub table_arrangement: ContentArrangement,
}
impl<B> Account<B> {
@@ -36,6 +38,11 @@ impl<B> Account<B> {
.table_preset
.or(account_config.table_preset)
.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 anyhow::{bail, Result};
use comfy_table::ContentArrangement;
use pimalaya_toolbox::config::TomlConfig;
use secrecy::SecretString;
use serde::{de::Visitor, Deserialize, Deserializer};
@@ -14,6 +15,7 @@ use url::Url;
pub struct Config {
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
pub accounts: HashMap<String, AccountConfig>,
}
@@ -47,11 +49,31 @@ pub struct AccountConfig {
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
pub imap: Option<ImapConfig>,
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.
#[derive(Clone, Debug, Deserialize)]
#[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,
};
/// 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
/// you to manage IMAP mailboxes: list mailboxes, read messages,
/// add flags etc.
/// This command gives you access to the IMAP CLI API, and allows you
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
#[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")]
pub enum ImapCommand {
Id(IdCommand),
#[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)]
Envelopes(EnvelopeCommand),
#[command(subcommand)]
Flags(FlagCommand),
#[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)]
#[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand),
Id(IdCommand),
}
impl ImapCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
match self {
Self::Id(cmd) => cmd.exec(printer, account),
Self::Envelopes(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::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
/// 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)]
pub enum EnvelopeCommand {
List(ListEnvelopesCommand),
Get(GetEnvelopeCommand),
List(ListEnvelopesCommand),
Search(SearchEnvelopesCommand),
Sort(SortEnvelopesCommand),
Thread(ThreadEnvelopesCommand),
@@ -27,8 +27,8 @@ pub enum EnvelopeCommand {
impl EnvelopeCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
match self {
Self::List(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::Sort(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 clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, Row, Table};
use io_imap::{
coroutines::{fetch::*, select::*},
types::{
@@ -12,16 +12,16 @@ use io_imap::{
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
envelope::list::{decode_mime, format_addresses},
mailbox::arg::MailboxNameOptionalFlag,
envelope::list::{decode_mime, format_address},
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream,
};
/// Get a single message envelope.
/// Get a single IMAP envelope.
///
/// This command displays detailed envelope information for a specific
/// message, including all header fields like date, subject, from, to,
@@ -30,11 +30,12 @@ use crate::imap::{
pub struct GetEnvelopeCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")]
pub id: u32,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
pub seq: bool,
@@ -42,25 +43,27 @@ pub struct GetEnvelopeCommand {
impl GetEnvelopeCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
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 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 =
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)?;
Ok(())
printer.out(table)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct EnvelopeDetail {
pub date: String,
pub subject: String,
pub message_id: String,
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 EnvelopeTable {
#[serde(skip)]
pub preset: String,
pub envelope: EnvelopeTableItems,
}
pub struct EnvelopeDetailTable {
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 {
impl fmt::Display for EnvelopeTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([Cell::new("FIELD"), Cell::new("VALUE")]));
.load_preset(&self.preset)
.set_header(Row::from([Cell::new("HEADER"), Cell::new("VALUE")]));
let fields = [
("Date", &self.detail.date),
("Subject", &self.detail.subject),
("Message-ID", &self.detail.message_id),
("From", &self.detail.from),
("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),
];
table.add_row(Row::from([
Cell::new("Message ID"),
match &self.envelope.message_id {
Some(id) => Cell::new(id),
None => Cell::new(""),
},
]));
for (name, value) in fields {
table.add_row(Row::from([Cell::new(name), Cell::new(value)]));
table.add_row(Row::from([
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)?;
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)
table
}
}
+109 -116
View File
@@ -1,8 +1,8 @@
use std::fmt;
use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
coroutines::{fetch::*, select::*},
types::{
@@ -16,23 +16,15 @@ use io_stream::runtimes::std::handle;
use log::debug;
use pimalaya_toolbox::terminal::printer::Printer;
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.
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.
/// List IMAP envelopes from the given mailbox.
///
/// This command displays envelopes for messages in the specified
/// 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 {
#[command(flatten)]
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:*")]
pub sequence: String,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
pub seq: bool,
@@ -53,26 +46,24 @@ pub struct ListEnvelopesCommand {
impl ListEnvelopesCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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: SequenceSet = self.sequence.parse()?;
// FETCH envelopes
let item_names =
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)?;
Ok(())
printer.out(table)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct EnvelopeEntry {
pub id: u32,
pub date: String,
pub from: String,
pub subject: String,
}
pub struct EnvelopesTable {
entries: Vec<EnvelopeEntry>,
#[serde(skip)]
preset: String,
#[serde(skip)]
arrangement: ContentArrangement,
envelopes: Vec<EnvelopesTableEntry>,
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
@@ -163,35 +106,94 @@ impl fmt::Display for EnvelopesTable {
let id_header = if self.uid_mode { "UID" } else { "SEQ" };
table
.load_preset(presets::ASCII_FULL)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([
Cell::new(id_header),
Cell::new("DATE"),
Cell::new("FROM"),
Cell::new("SUBJECT"),
Cell::new("Subject"),
Cell::new("From"),
Cell::new("Date"),
]));
for entry in &self.entries {
for entry in &self.envelopes {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(entry.id));
row.add_cell(Cell::new(&entry.date));
row.add_cell(Cell::new(&entry.from));
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);
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
writeln!(f, "{table}")
}
}
impl Serialize for EnvelopesTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.entries.serialize(serializer)
#[derive(Clone, Debug, Serialize)]
pub struct EnvelopesTableEntry {
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<_>>()
.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 anyhow::{bail, Result};
use anyhow::{anyhow, bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
coroutines::{search::*, select::*},
types::{
@@ -13,11 +13,15 @@ use io_imap::{
};
use io_stream::runtimes::std::handle;
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
/// 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)]
pub struct SearchEnvelopesCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalArg,
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// Search query (e.g., "from:alice unseen").
#[arg(name = "query", value_name = "QUERY", default_value = "all")]
@@ -58,26 +64,25 @@ pub struct SearchEnvelopesCommand {
impl SearchEnvelopesCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 query into search criteria
let criteria = parse_query(&self.query)?;
// SEARCH
let mut arg = None;
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(())
}
}
@@ -206,67 +256,18 @@ fn parse_date(s: &str) -> Result<NaiveDate> {
let year: i32 = parts[0]
.parse()
.map_err(|_| anyhow::anyhow!("Invalid year in date '{s}'"))?;
.map_err(|_| anyhow!("Invalid year in date '{s}'"))?;
let month: u32 = parts[1]
.parse()
.map_err(|_| anyhow::anyhow!("Invalid month in date '{s}'"))?;
.map_err(|_| anyhow!("Invalid month in date '{s}'"))?;
let day: u32 = parts[2]
.parse()
.map_err(|_| anyhow::anyhow!("Invalid day in date '{s}'"))?;
.map_err(|_| anyhow!("Invalid day in date '{s}'"))?;
// Create chrono::NaiveDate first
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
NaiveDate::try_from(chrono_date).map_err(|e| anyhow::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)
}
NaiveDate::try_from(chrono_date).map_err(|e| anyhow!("Invalid date '{s}': {e}"))
}
+25 -32
View File
@@ -3,6 +3,7 @@ use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
context::ImapContext,
coroutines::{fetch::*, select::*, thread::*},
types::{
extensions::thread::{Thread, ThreadingAlgorithm},
@@ -17,11 +18,11 @@ use serde::{Serialize, Serializer};
use crate::imap::{
account::ImapAccount,
envelope::{list::decode_mime, search::parse_query},
mailbox::arg::MailboxNameOptionalArg,
stream,
mailbox::arg::{MailboxNameOptionalFlag, MailboxSelectFlag},
stream::{self, Stream},
};
/// Thread messages by algorithm.
/// Thread IMAP messages by algorithm.
///
/// This command groups messages into conversation threads using the
/// specified threading algorithm. Requires the THREAD IMAP extension.
@@ -32,7 +33,9 @@ use crate::imap::{
#[derive(Debug, Parser)]
pub struct ThreadEnvelopesCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalArg,
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// Threading algorithm (orderedsubject or references).
#[arg(short = 'A', long, default_value = "references")]
@@ -49,29 +52,26 @@ pub struct ThreadEnvelopesCommand {
impl ThreadEnvelopesCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 threading algorithm
let algorithm = parse_algorithm(&self.algorithm)?;
// Parse search criteria
let search_criteria = parse_query(&self.query)?;
// THREAD
let mut arg = None;
let mut coroutine = ImapThread::new(context, algorithm, search_criteria, !self.seq);
@@ -95,10 +95,9 @@ impl ThreadEnvelopesCommand {
HashMap::new()
};
let table = ThreadResultsTable::new(threads, subjects, !self.seq);
let table = ThreadResultsTable::new(threads, subjects);
printer.out(table)?;
Ok(())
printer.out(table)
}
}
@@ -139,8 +138,8 @@ fn collect_thread_ids_recursive(thread: &Thread, ids: &mut Vec<NonZeroU32>) {
}
fn fetch_subjects(
stream: &mut stream::Stream,
context: io_imap::context::ImapContext,
stream: &mut Stream,
context: ImapContext,
ids: &[NonZeroU32],
uid: bool,
) -> Result<HashMap<u32, String>> {
@@ -211,17 +210,11 @@ pub struct ThreadEntry {
pub struct ThreadResultsTable {
threads: Vec<Thread>,
subjects: HashMap<u32, String>,
#[allow(dead_code)]
uid_mode: bool,
}
impl ThreadResultsTable {
pub fn new(threads: Vec<Thread>, subjects: HashMap<u32, String>, uid_mode: bool) -> Self {
Self {
threads,
subjects,
uid_mode,
}
pub fn new(threads: Vec<Thread>, subjects: HashMap<u32, String>) -> Self {
Self { threads, subjects }
}
fn build_entries(&self) -> Vec<ThreadEntry> {
+26 -24
View File
@@ -10,24 +10,29 @@ use io_imap::{
use io_stream::runtimes::std::handle;
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
/// the given sequence set.
/// This command adds the given flags to messages identified by the
/// given sequence set.
#[derive(Debug, Parser)]
pub struct AddFlagsCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// 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,
/// The flags to add (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
@@ -36,33 +41,30 @@ pub struct AddFlagsCommand {
impl AddFlagsCommand {
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()?;
// First, SELECT the mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 flags
let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self
.flags
.flag
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
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
/// you to manage them: list available flags, add flags to messages,
/// remove flags from messages, etc.
/// you to manage them.
#[derive(Debug, Subcommand)]
pub enum FlagCommand {
List(ListFlagsCommand),
+49 -35
View File
@@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt};
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
coroutines::select::*,
types::flag::{Flag, FlagPerm},
@@ -11,9 +11,9 @@ use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
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
/// 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)]
pub struct ListFlagsCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalArg,
pub mailbox: MailboxNameArg,
}
impl ListFlagsCommand {
@@ -47,36 +47,36 @@ impl ListFlagsCommand {
};
let table = FlagsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
flags,
permanent_flags,
};
printer.out(table)?;
Ok(())
printer.out(table)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct FlagEntry {
pub name: String,
pub permanent: bool,
pub struct FlagsTable<'a> {
#[serde(skip_serializing)]
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 {
flags: Vec<Flag<'static>>,
permanent_flags: Vec<FlagPerm<'static>>,
}
impl FlagsTable {
fn build_entries(&self) -> Vec<FlagEntry> {
impl FlagsTable<'_> {
fn build_entries(&self) -> Vec<(String, bool)> {
let mut entries: BTreeMap<String, bool> = BTreeMap::new();
// Add flags
for flag in &self.flags {
entries.entry(flag.to_string()).or_insert(false);
}
// Mark permanent flags
for flag in &self.permanent_flags {
let name = match flag {
FlagPerm::Flag(f) => f.to_string(),
@@ -85,38 +85,52 @@ impl FlagsTable {
entries.insert(name, true);
}
entries
.into_iter()
.map(|(name, permanent)| FlagEntry { name, permanent })
.collect()
entries.into_iter().collect()
}
}
impl fmt::Display for FlagsTable {
impl fmt::Display for FlagsTable<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.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([
Cell::new(&entry.name),
Cell::new(if entry.permanent { "true" } else { "" }),
Cell::new(&flag),
Cell::new(if perm { "true" } else { "" }),
]));
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
writeln!(f, "{table}")
}
}
impl Serialize for FlagsTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.build_entries().serialize(serializer)
}
pub fn serialize_flags<S: Serializer>(
flags: &Vec<Flag<'_>>,
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 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
/// by the given sequence set.
/// This command removes the specified flag(s) from message(s)
/// identified by the given sequence set.
#[derive(Debug, Parser)]
pub struct RemoveFlagsCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String,
/// The flags to remove (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
@@ -36,33 +41,30 @@ pub struct RemoveFlagsCommand {
impl RemoveFlagsCommand {
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()?;
// First, SELECT the mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 flags
let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self
.flags
.flag
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
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 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
/// the given sequence set with the specified flags.
@@ -20,14 +24,15 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct SetFlagsCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String,
/// The flags to set (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
pub flag: Vec<String>,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
@@ -36,33 +41,30 @@ pub struct SetFlagsCommand {
impl SetFlagsCommand {
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()?;
// First, SELECT the mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 flags
let sequence_set = self.sequence_set.as_str().try_into()?;
let flags: Vec<Flag<'static>> = self
.flags
.flag
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
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.
#[derive(Debug, Parser)]
pub struct MailboxNameArg {
@@ -53,7 +66,7 @@ pub struct SourceMailboxNameOptionalFlag {
}
/// The target mailbox name argument parser.
#[derive(Debug, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct TargetMailboxNameArg {
/// The name of the target mailbox.
#[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 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.
///
@@ -14,15 +18,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
pub struct ExpungeMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
/// Select the given mailbox before expunging it.
///
/// 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,
#[command(flatten)]
pub select: MailboxSelectFlag,
}
impl ExpungeMailboxCommand {
@@ -31,7 +28,7 @@ impl ExpungeMailboxCommand {
let mailbox = self.mailbox.name.try_into()?;
if self.select {
if self.select.r#true {
let mut arg = None;
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 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
/// given mailbox.
@@ -18,15 +22,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg, stream};
pub struct PurgeMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
/// Select the given mailbox before purging it.
///
/// 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,
#[command(flatten)]
pub select: MailboxSelectFlag,
}
impl PurgeMailboxCommand {
@@ -35,7 +32,7 @@ impl PurgeMailboxCommand {
let mailbox = self.mailbox.name.try_into()?;
if self.select {
if self.select.r#true {
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
+5 -8
View File
@@ -5,17 +5,16 @@ use pimalaya_toolbox::terminal::printer::Printer;
use crate::imap::{
account::ImapAccount,
message::{
copy::CopyMessageCommand, delete::DeleteMessageCommand, export::ExportMessageCommand,
get::GetMessageCommand, r#move::MoveMessageCommand, read::ReadMessageCommand,
save::SaveMessageCommand,
copy::CopyMessageCommand, export::ExportMessageCommand, get::GetMessageCommand,
r#move::MoveMessageCommand, read::ReadMessageCommand, save::SaveMessageCommand,
},
};
/// Manage messages.
/// Manage IMAP messages.
///
/// A message is a complete email including headers and body. This
/// subcommand allows you to save, get, read, export, copy, move, and
/// delete messages.
/// subcommand allows you to save, get, read, export, copy, and move
/// messages.
#[derive(Debug, Subcommand)]
pub enum MessageCommand {
Save(SaveMessageCommand),
@@ -24,7 +23,6 @@ pub enum MessageCommand {
Export(ExportMessageCommand),
Copy(CopyMessageCommand),
Move(MoveMessageCommand),
Delete(DeleteMessageCommand),
}
impl MessageCommand {
@@ -36,7 +34,6 @@ impl MessageCommand {
Self::Export(cmd) => cmd.exec(printer, account),
Self::Copy(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 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
/// from the source mailbox to the destination mailbox.
/// This command copies message(s) identified by the given sequence
/// set from the source mailbox to the destination mailbox.
#[derive(Debug, Parser)]
pub struct CopyMessageCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String,
/// The destination mailbox.
#[arg(name = "destination", value_name = "DESTINATION")]
pub destination: String,
#[command(flatten)]
pub destination: TargetMailboxNameArg,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
@@ -33,27 +37,26 @@ pub struct CopyMessageCommand {
impl CopyMessageCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 and destination
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 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},
};
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 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.
///
@@ -22,11 +26,11 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct GetMessageCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")]
pub id: u32,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
pub seq: bool,
@@ -34,24 +38,25 @@ pub struct GetMessageCommand {
impl GetMessageCommand {
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()?;
// 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),
}
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"))?;
if self.select.r#true {
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 =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
@@ -71,7 +76,6 @@ impl GetMessageCommand {
}
};
// Extract raw message bytes
let mut raw_message: Option<Vec<u8>> = None;
for item in items.into_iter() {
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 message = MessageParser::default()
.parse(&raw)
.ok_or_else(|| anyhow::anyhow!("Failed to parse message"))?;
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Invalid message");
};
let structure = MessageStructure::from_parsed(&message);
printer.out(structure)?;
Ok(())
printer.out(structure)
}
}
@@ -124,7 +127,7 @@ pub struct MessageStructure {
}
impl MessageStructure {
pub fn from_parsed(message: &mail_parser::Message<'_>) -> Self {
pub fn from_parsed(message: &Message<'_>) -> Self {
// Extract headers
let headers = MessageHeaders {
date: message.date().map(|d| d.to_rfc3339()),
-1
View File
@@ -1,6 +1,5 @@
pub mod command;
pub mod copy;
pub mod delete;
pub mod export;
pub mod get;
pub mod r#move;
+23 -20
View File
@@ -7,9 +7,13 @@ use io_imap::{
use io_stream::runtimes::std::handle;
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
/// 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 {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The sequence set of messages (e.g., "1", "1,2,3", "1:*").
#[arg(name = "sequence_set", value_name = "SEQUENCE")]
pub sequence_set: String,
/// The destination mailbox.
#[arg(name = "destination", value_name = "DESTINATION")]
pub destination: String,
#[command(flatten)]
pub destination: TargetMailboxNameArg,
/// Use sequence numbers instead of UIDs.
#[arg(long)]
@@ -34,27 +38,26 @@ pub struct MoveMessageCommand {
impl MoveMessageCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
};
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 and destination
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 coroutine = ImapMove::new(context, sequence_set, destination, !self.seq);
+32 -28
View File
@@ -1,6 +1,6 @@
use std::{fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use anyhow::{anyhow, bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{fetch::*, select::*},
@@ -11,7 +11,11 @@ use mail_parser::MessageParser;
use pimalaya_toolbox::terminal::printer::Printer;
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.
///
@@ -21,6 +25,8 @@ use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag, s
pub struct ReadMessageCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalFlag,
#[command(flatten)]
pub select: MailboxSelectFlag,
/// The message UID (or sequence number with --seq).
#[arg(name = "id", value_name = "ID")]
@@ -41,25 +47,27 @@ pub struct ReadMessageCommand {
impl ReadMessageCommand {
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()?;
// SELECT mailbox
let mut arg = None;
let mut coroutine = ImapSelect::new(context, mailbox);
if self.select.r#true {
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),
}
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 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 =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
section: None,
@@ -78,8 +86,8 @@ impl ReadMessageCommand {
}
};
// Extract raw message bytes
let mut raw_message: Option<Vec<u8>> = None;
for item in items.into_iter() {
if let MessageDataItem::BodyExt { data, .. } = item {
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 message = MessageParser::default()
.parse(&raw)
.ok_or_else(|| anyhow::anyhow!("Failed to parse message"))?;
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Invalid message");
};
let content = if self.html {
// Get HTML content
message
.body_html(0)
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("No HTML content found"))?
.ok_or_else(|| anyhow!("No HTML content found"))?
} else {
// Get plain text, or convert HTML to text
if let Some(text) = message.body_text(0) {
text.to_string()
} else if let Some(html) = message.body_html(0) {
// Convert HTML to text
html2text::from_read(html.as_bytes(), self.width)
} else {
bail!("No text or HTML content found");
@@ -114,9 +120,7 @@ impl ReadMessageCommand {
};
let output = MessageContent { content };
printer.out(output)?;
Ok(())
printer.out(output)
}
}
+40 -41
View File
@@ -1,18 +1,18 @@
use std::io::{stdin, Read};
use std::io::{stdin, BufRead, IsTerminal};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{
append::*,
select::{ImapSelect, ImapSelectResult},
coroutines::append::*,
types::{
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 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.
///
@@ -20,51 +20,50 @@ use crate::imap::{account::ImapAccount, stream};
/// message is read from stdin in RFC 5322 format (raw email).
#[derive(Debug, Parser)]
pub struct SaveMessageCommand {
/// The mailbox to save the message to.
#[arg(name = "mailbox", value_name = "MAILBOX")]
pub mailbox: String,
#[command(flatten)]
pub mailbox: MailboxNameArg,
/// Select the given mailbox before saving message into it.
///
/// 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,
/// The flags to add to the message.
#[arg(short, long, num_args = 0..)]
pub flag: Vec<String>,
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
}
impl SaveMessageCommand {
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 mut message = Vec::new();
stdin().read_to_end(&mut message)?;
let mailbox: Mailbox<'static> = self.mailbox.name.try_into()?;
if message.is_empty() {
bail!("No message provided on stdin");
}
let message = if stdin().is_terminal() || printer.is_json() {
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 literal = Literal::try_from(message)?;
let message = LiteralOrLiteral8::Literal(literal);
if self.select {
let mut arg = None;
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 flags: Vec<_> = self
.flag
.iter()
.map(String::as_str)
.map(|f| Flag::try_from(f).map(IntoStatic::into_static))
.collect::<Result<_, _>>()?;
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 {
match coroutine.resume(arg.take()) {
+37 -37
View File
@@ -43,43 +43,6 @@ pub enum Stream {
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)> {
info!("connecting to IMAP server using {}", config.url);
@@ -376,3 +339,40 @@ pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, 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(),
}
}
}