mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 20:07:57 +08:00
clean implem part 1
This commit is contained in:
Generated
+61
@@ -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
@@ -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
@@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user