mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 21:29:24 +08:00
list imap mailboxes
This commit is contained in:
+102
-3
@@ -1,3 +1,102 @@
|
||||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
// pub mod arg;
|
||||
// pub mod command;
|
||||
// pub mod config;
|
||||
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::{sasl::SaslConfig, tls::TlsConfig};
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapConfig {
|
||||
/// The IMAP server host name.
|
||||
pub host: String,
|
||||
/// The IMAP server host port.
|
||||
pub port: Option<u16>,
|
||||
|
||||
#[serde(default)]
|
||||
pub tls: TlsConfig,
|
||||
#[serde(default)]
|
||||
pub starttls: bool,
|
||||
#[serde(default)]
|
||||
pub sasl: SaslConfig,
|
||||
|
||||
/// The IMAP extensions configuration.
|
||||
#[serde(default)]
|
||||
pub extensions: ImapExtensionsConfig,
|
||||
}
|
||||
|
||||
/// The IMAP configuration dedicated to extensions.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapExtensionsConfig {
|
||||
#[serde(default)]
|
||||
id: ImapIdExtensionConfig,
|
||||
}
|
||||
|
||||
/// The IMAP configuration dedicated to the ID extension.
|
||||
///
|
||||
/// https://www.rfc-editor.org/rfc/rfc2971.html
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapIdExtensionConfig {
|
||||
/// Automatically sends the ID command straight after
|
||||
/// authentication.
|
||||
#[serde(default)]
|
||||
send_after_auth: bool,
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct Account {
|
||||
#[serde(default)]
|
||||
pub default: bool,
|
||||
|
||||
pub imap: Option<ImapConfig>,
|
||||
|
||||
#[serde(deserialize_with = "shell_expanded_string")]
|
||||
pub email: String,
|
||||
pub display_name: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
// pub backend: Option<Backend>,
|
||||
// #[cfg(feature = "pgp")]
|
||||
// pub pgp: Option<PgpConfig>,
|
||||
// #[cfg(not(feature = "pgp"))]
|
||||
// #[serde(default)]
|
||||
// #[serde(skip_serializing, deserialize_with = "missing_pgp_feature")]
|
||||
// pub pgp: Option<()>,
|
||||
|
||||
// pub folder: Option<FolderConfig>,
|
||||
// pub envelope: Option<EnvelopeConfig>,
|
||||
// pub message: Option<MessageConfig>,
|
||||
// pub template: Option<TemplateConfig>,
|
||||
}
|
||||
|
||||
struct ShellExpandedStringVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ShellExpandedStringVisitor {
|
||||
type Value = String;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an string containing environment variable(s)")
|
||||
}
|
||||
|
||||
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
|
||||
match shellexpand::full(&v) {
|
||||
Ok(v) => Ok(v.to_string()),
|
||||
Err(_) => Ok(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shell_expanded_string<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<String, D::Error> {
|
||||
deserializer.deserialize_string(ShellExpandedStringVisitor)
|
||||
}
|
||||
|
||||
+123
-116
@@ -1,40 +1,59 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
use anyhow::Result;
|
||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
use pimalaya_toolbox::{
|
||||
config::TomlConfig,
|
||||
long_version,
|
||||
terminal::{
|
||||
cli::{
|
||||
arg::path_parser,
|
||||
printer::{OutputFmt, Printer},
|
||||
clap::{
|
||||
args::{AccountArg, JsonFlag, LogFlags},
|
||||
commands::{CompletionCommand, ManualCommand},
|
||||
parsers::path_parser,
|
||||
},
|
||||
config::TomlConfig as _,
|
||||
printer::Printer,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
account::command::AccountSubcommand,
|
||||
completion::command::CompletionGenerateCommand,
|
||||
config::TomlConfig,
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
flag::command::FlagSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
manual::command::ManualGenerateCommand,
|
||||
message::{
|
||||
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||
template::command::TemplateSubcommand,
|
||||
},
|
||||
// account::command::AccountSubcommand,
|
||||
account::Account,
|
||||
config::Config,
|
||||
folder::command::MailboxCommand, // message::{
|
||||
// attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||
// template::command::TemplateSubcommand,
|
||||
// },
|
||||
};
|
||||
|
||||
/// IMAP CLI (requires `imap` cargo feature).
|
||||
///
|
||||
/// This command gives you access to the IMAP CLI API, and allows
|
||||
/// you to manage IMAP mailboxes: list mailboxes, read messages,
|
||||
/// add flags etc.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "lowercase")]
|
||||
pub enum ImapCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(aliases = ["mboxes", "mbox"])]
|
||||
Mailboxes(MailboxCommand),
|
||||
}
|
||||
|
||||
impl ImapCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
match self {
|
||||
Self::Mailboxes(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = env!("CARGO_PKG_NAME"))]
|
||||
#[command(author, version, about)]
|
||||
#[command(long_version = long_version!())]
|
||||
#[command(propagate_version = true, infer_subcommands = true)]
|
||||
pub struct Cli {
|
||||
pub struct HimalayaCli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<HimalayaCommand>,
|
||||
pub command: BackendCommand,
|
||||
|
||||
/// Override the default configuration file path.
|
||||
///
|
||||
@@ -49,123 +68,111 @@ pub struct Cli {
|
||||
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
|
||||
#[arg(value_name = "PATH", value_parser = path_parser, value_delimiter = ':')]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
#[command(flatten)]
|
||||
pub account: AccountArg,
|
||||
#[command(flatten)]
|
||||
pub json: JsonFlag,
|
||||
#[command(flatten)]
|
||||
pub log: LogFlags,
|
||||
}
|
||||
|
||||
/// Customize the output format.
|
||||
///
|
||||
/// The output format determine how to display commands output to
|
||||
/// the terminal.
|
||||
///
|
||||
/// The possible values are:
|
||||
///
|
||||
/// - json: output will be in a form of a JSON-compatible object
|
||||
///
|
||||
/// - plain: output will be in a form of either a plain text or
|
||||
/// table, depending on the command
|
||||
#[arg(long, short, global = true)]
|
||||
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||
pub output: OutputFmt,
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum BackendCommand {
|
||||
#[command(subcommand)]
|
||||
Imap(ImapCommand),
|
||||
}
|
||||
|
||||
/// Disable all logs.
|
||||
///
|
||||
/// Same as running command with `RUST_LOG=off` environment
|
||||
/// variable.
|
||||
#[arg(long, global = true)]
|
||||
#[arg(conflicts_with = "debug")]
|
||||
#[arg(conflicts_with = "trace")]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Enable debug logs.
|
||||
///
|
||||
/// Same as running command with `RUST_LOG=debug` environment
|
||||
/// variable.
|
||||
#[arg(long, global = true)]
|
||||
#[arg(conflicts_with = "quiet")]
|
||||
#[arg(conflicts_with = "trace")]
|
||||
pub debug: bool,
|
||||
|
||||
/// Enable verbose trace logs with backtrace.
|
||||
///
|
||||
/// Same as running command with `RUST_LOG=trace` and
|
||||
/// `RUST_BACKTRACE=1` environment variables.
|
||||
#[arg(long, global = true)]
|
||||
#[arg(conflicts_with = "quiet")]
|
||||
#[arg(conflicts_with = "debug")]
|
||||
pub trace: bool,
|
||||
impl BackendCommand {
|
||||
pub fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config_paths: &[PathBuf],
|
||||
account_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Imap(cmd) => {
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
let (_, account) = config.get_account(account_name)?;
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum HimalayaCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "accounts")]
|
||||
Account(AccountSubcommand),
|
||||
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "accounts")]
|
||||
// Account(AccountSubcommand),
|
||||
#[command(subcommand)]
|
||||
#[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])]
|
||||
#[command(alias = "folders")]
|
||||
Folder(FolderSubcommand),
|
||||
Folder(MailboxCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "envelopes")]
|
||||
Envelope(EnvelopeSubcommand),
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "envelopes")]
|
||||
// Envelope(EnvelopeSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "flags")]
|
||||
Flag(FlagSubcommand),
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "flags")]
|
||||
// Flag(FlagSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "messages", alias = "msgs", alias = "msg")]
|
||||
Message(MessageSubcommand),
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "messages", alias = "msgs", alias = "msg")]
|
||||
// Message(MessageSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "attachments")]
|
||||
Attachment(AttachmentSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "templates", alias = "tpls", alias = "tpl")]
|
||||
Template(TemplateSubcommand),
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "attachments")]
|
||||
// Attachment(AttachmentSubcommand),
|
||||
|
||||
// #[command(subcommand)]
|
||||
// #[command(alias = "templates", alias = "tpls", alias = "tpl")]
|
||||
// Template(TemplateSubcommand),
|
||||
#[command(arg_required_else_help = true, alias = "mans")]
|
||||
Manuals(ManualCommand),
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "manuals", alias = "mans")]
|
||||
Manual(ManualGenerateCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "completions")]
|
||||
Completion(CompletionGenerateCommand),
|
||||
Completions(CompletionCommand),
|
||||
}
|
||||
|
||||
impl HimalayaCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
|
||||
pub fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config_paths: &[PathBuf],
|
||||
account_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Account(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, config, config_paths.first()).await
|
||||
}
|
||||
// Self::Account(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, config, config_paths.first()).await
|
||||
// }
|
||||
Self::Folder(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
let (_, account) = config.get_account(account_name)?;
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
Self::Envelope(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Flag(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Message(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Attachment(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Template(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Manual(cmd) => cmd.execute(printer).await,
|
||||
Self::Completion(cmd) => cmd.execute().await,
|
||||
// Self::Envelope(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, &config).await
|
||||
// }
|
||||
// Self::Flag(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, &config).await
|
||||
// }
|
||||
// Self::Message(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, &config).await
|
||||
// }
|
||||
// Self::Attachment(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, &config).await
|
||||
// }
|
||||
// Self::Template(cmd) => {
|
||||
// let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
// cmd.execute(printer, &config).await
|
||||
// }
|
||||
Self::Manuals(cmd) => cmd.execute(printer, HimalayaCli::command()),
|
||||
Self::Completions(cmd) => cmd.execute(printer, HimalayaCli::command()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-2
@@ -1,3 +1,39 @@
|
||||
use pimalaya_tui::himalaya::config::HimalayaTomlConfig;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
pub type TomlConfig = HimalayaTomlConfig;
|
||||
use pimalaya_toolbox::config::TomlConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::account::Account;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub accounts: HashMap<String, Account>,
|
||||
// pub account: Option<AccountsConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig for Config {
|
||||
type Account = Account;
|
||||
|
||||
fn project_name() -> &'static str {
|
||||
env!("CARGO_PKG_NAME")
|
||||
}
|
||||
|
||||
fn find_default_account(&self) -> Option<(String, Self::Account)> {
|
||||
self.accounts
|
||||
.iter()
|
||||
.find(|(_, account)| account.default)
|
||||
.map(|(name, account)| (name.to_owned(), account.clone()))
|
||||
}
|
||||
|
||||
fn find_account(&self, name: &str) -> Option<(String, Self::Account)> {
|
||||
self.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
+106
-56
@@ -1,73 +1,123 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::list::ListFolders},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{
|
||||
backend::BackendBuilder,
|
||||
config::{Folders, FoldersTable},
|
||||
use io_imap::{
|
||||
coroutines::{
|
||||
authenticate::*, authenticate_anonymous::*, authenticate_plain::*, list::*, login::*,
|
||||
},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
types::response::Capability,
|
||||
};
|
||||
use tracing::info;
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
use crate::{account::Account, sasl::SaslMechanism, stream};
|
||||
|
||||
/// List all folders.
|
||||
/// List all mailboxes.
|
||||
///
|
||||
/// This command allows you to list all exsting folders.
|
||||
/// This command allows you to list all exsting mailboxes from your
|
||||
/// IMAP account.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderListCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width, in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
pub struct ListMailboxesCommand {
|
||||
// /// The maximum width the table should not exceed.
|
||||
// ///
|
||||
// /// This argument will force the table not to exceed the given
|
||||
// /// width, in pixels. Columns may shrink with ellipsis in order to
|
||||
// /// fit the width.
|
||||
// #[arg(long = "max-width", short = 'w')]
|
||||
// #[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
// pub table_max_width: Option<u16>,
|
||||
}
|
||||
|
||||
impl FolderListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list folders command");
|
||||
impl ListMailboxesCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
let imap = account.imap.unwrap();
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (mut context, mut stream) = if imap.tls.disable {
|
||||
let port = imap.port.unwrap_or(143);
|
||||
stream::tcp(imap.host, port)?
|
||||
} else {
|
||||
let port = imap.port.unwrap_or(if imap.starttls { 143 } else { 993 });
|
||||
stream::rustls(imap.host, port, imap.starttls, imap.tls.cert)?
|
||||
};
|
||||
|
||||
let toml_account_config = Arc::new(toml_account_config);
|
||||
let ir = context.capability.contains(&Capability::SaslIr);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_list_folders(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
let mut candidates = vec![];
|
||||
|
||||
let folders = Folders::from(backend.list_folders().await?);
|
||||
let table = FoldersTable::from(folders)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(toml_account_config.folder_list_table_preset())
|
||||
.with_some_name_color(toml_account_config.folder_list_table_name_color())
|
||||
.with_some_desc_color(toml_account_config.folder_list_table_desc_color());
|
||||
for mechanism in imap.sasl.mechanisms {
|
||||
match mechanism {
|
||||
SaslMechanism::Login => {
|
||||
let Some(ref auth) = imap.sasl.login else {
|
||||
warn!("missing SASL LOGIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
printer.out(table)?;
|
||||
let params = ImapLoginParams::new(&auth.username, auth.password.get()?)?;
|
||||
candidates.push(ImapAuthenticateCandidate::Login(params));
|
||||
}
|
||||
SaslMechanism::Plain => {
|
||||
let Some(ref auth) = imap.sasl.plain else {
|
||||
warn!("missing SASL PLAIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
let params = ImapAuthenticatePlainParams::new(
|
||||
auth.authzid.as_ref(),
|
||||
&auth.authcid,
|
||||
auth.passwd.get()?,
|
||||
ir,
|
||||
);
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Plain(params))
|
||||
}
|
||||
SaslMechanism::Anonymous => {
|
||||
let msg = imap
|
||||
.sasl
|
||||
.anonymous
|
||||
.as_ref()
|
||||
.and_then(|auth| auth.message.as_ref());
|
||||
|
||||
let params = ImapAuthenticateAnonymousParams::new(msg, ir);
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Anonymous(params))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = ImapAuthenticate::new(context, candidates);
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapAuthenticateResult::Io(io) => arg = Some(handle(&mut stream, io)?),
|
||||
ImapAuthenticateResult::Ok { context: c } => break context = c,
|
||||
ImapAuthenticateResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = ImapList::new(context, "".try_into().unwrap(), "*".try_into().unwrap());
|
||||
|
||||
let mailboxes = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapListResult::Io(io) => arg = Some(handle(&mut stream, io)?),
|
||||
ImapListResult::Ok(ok) => break ok.mailboxes,
|
||||
ImapListResult::Err(err) => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
println!("mailboxes: {mailboxes:#?}");
|
||||
|
||||
// TODO: list folders
|
||||
|
||||
// let folders = Folders::from(backend.list_folders().await?);
|
||||
// let table = FoldersTable::from(folders)
|
||||
// .with_some_width(self.table_max_width)
|
||||
// .with_some_preset(toml_account_config.folder_list_table_preset())
|
||||
// .with_some_name_color(toml_account_config.folder_list_table_name_color())
|
||||
// .with_some_desc_color(toml_account_config.folder_list_table_desc_color());
|
||||
|
||||
// printer.out(table)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+27
-35
@@ -1,51 +1,43 @@
|
||||
mod add;
|
||||
mod delete;
|
||||
mod expunge;
|
||||
// mod add;
|
||||
// mod delete;
|
||||
// mod expunge;
|
||||
mod list;
|
||||
mod purge;
|
||||
// mod purge;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{account::Account, folder::command::list::ListMailboxesCommand};
|
||||
|
||||
use self::{
|
||||
add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
|
||||
list::FolderListCommand, purge::FolderPurgeCommand,
|
||||
};
|
||||
|
||||
/// Create, list and purge your folders (as known as mailboxes).
|
||||
/// Create, list and purge mailboxes.
|
||||
///
|
||||
/// A folder (as known as mailbox, or directory) is a messages
|
||||
/// container. This subcommand allows you to manage them.
|
||||
/// A mailbox is a message container. This subcommand allows you to
|
||||
/// manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FolderSubcommand {
|
||||
#[command(visible_alias = "create", alias = "new")]
|
||||
Add(FolderAddCommand),
|
||||
pub enum MailboxCommand {
|
||||
// #[command(visible_alias = "create", alias = "new")]
|
||||
// Add(FolderAddCommand),
|
||||
List(ListMailboxesCommand),
|
||||
// #[command()]
|
||||
// Expunge(FolderExpungeCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
List(FolderListCommand),
|
||||
// #[command()]
|
||||
// Purge(FolderPurgeCommand),
|
||||
|
||||
#[command()]
|
||||
Expunge(FolderExpungeCommand),
|
||||
|
||||
#[command()]
|
||||
Purge(FolderPurgeCommand),
|
||||
|
||||
#[command(alias = "remove", alias = "rm")]
|
||||
Delete(FolderDeleteCommand),
|
||||
// #[command(alias = "remove", alias = "rm")]
|
||||
// Delete(FolderDeleteCommand),
|
||||
}
|
||||
|
||||
impl FolderSubcommand {
|
||||
impl MailboxCommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Expunge(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Purge(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Delete(cmd) => cmd.execute(printer, config).await,
|
||||
// Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, account),
|
||||
// Self::Expunge(cmd) => cmd.execute(printer, config).await,
|
||||
// Self::Purge(cmd) => cmd.execute(printer, config).await,
|
||||
// Self::Delete(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
pub mod arg;
|
||||
// pub mod arg;
|
||||
pub mod command;
|
||||
|
||||
+17
-16
@@ -1,24 +1,25 @@
|
||||
pub mod account;
|
||||
pub mod cli;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod email;
|
||||
pub mod folder;
|
||||
pub mod manual;
|
||||
pub mod sasl;
|
||||
pub mod stream;
|
||||
pub mod tls;
|
||||
// pub mod email;
|
||||
|
||||
use std::path::PathBuf;
|
||||
// use std::path::PathBuf;
|
||||
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
// use shellexpand_utils::{canonicalize, expand};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::email::{envelope, flag, message};
|
||||
// #[doc(inline)]
|
||||
// pub use crate::email::{envelope, flag, message};
|
||||
|
||||
/// Parse the given [`str`] as [`PathBuf`].
|
||||
///
|
||||
/// The path is first shell expanded, then canonicalized (if
|
||||
/// applicable).
|
||||
fn dir_parser(path: &str) -> Result<PathBuf, String> {
|
||||
expand::try_path(path)
|
||||
.map(canonicalize::path)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
// /// Parse the given [`str`] as [`PathBuf`].
|
||||
// ///
|
||||
// /// The path is first shell expanded, then canonicalized (if
|
||||
// /// applicable).
|
||||
// fn dir_parser(path: &str) -> Result<PathBuf, String> {
|
||||
// expand::try_path(path)
|
||||
// .map(canonicalize::path)
|
||||
// .map_err(|err| err.to_string())
|
||||
// }
|
||||
|
||||
+48
-40
@@ -1,47 +1,55 @@
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use himalaya::{
|
||||
cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand,
|
||||
message::command::mailto::MessageMailtoCommand,
|
||||
};
|
||||
use pimalaya_tui::terminal::{
|
||||
cli::{printer::StdoutPrinter, tracing},
|
||||
config::TomlConfig as _,
|
||||
};
|
||||
use himalaya::cli::HimalayaCli;
|
||||
use pimalaya_toolbox::terminal::{error::ErrorReport, log::Logger, printer::StdoutPrinter};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let tracing = tracing::install()?;
|
||||
fn main() {
|
||||
let cli = HimalayaCli::parse();
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
secret::keyring::set_global_service_name("himalaya-cli");
|
||||
Logger::init(&cli.log);
|
||||
|
||||
// if the first argument starts by "mailto:", execute straight the
|
||||
// mailto message command
|
||||
let mailto = std::env::args()
|
||||
.nth(1)
|
||||
.filter(|arg| arg.starts_with("mailto:"));
|
||||
let mut printer = StdoutPrinter::new(&cli.json);
|
||||
let config_paths = cli.config_paths.as_ref();
|
||||
let account_name = cli.account.name.as_deref();
|
||||
|
||||
if let Some(ref url) = mailto {
|
||||
let mut printer = StdoutPrinter::default();
|
||||
let config = TomlConfig::from_default_paths().await?;
|
||||
let result = cli
|
||||
.command
|
||||
.execute(&mut printer, config_paths, account_name);
|
||||
|
||||
return MessageMailtoCommand::new(url)?
|
||||
.execute(&mut printer, &config)
|
||||
.await;
|
||||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
let mut printer = StdoutPrinter::new(cli.output);
|
||||
let res = match cli.command {
|
||||
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
|
||||
None => {
|
||||
let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
|
||||
EnvelopeListCommand::default()
|
||||
.execute(&mut printer, &config)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
tracing.with_debug_and_trace_notes(res)
|
||||
ErrorReport::eval(&mut printer, result)
|
||||
}
|
||||
|
||||
// fn main() {
|
||||
// let tracing = tracing::install()?;
|
||||
|
||||
// #[cfg(feature = "keyring")]
|
||||
// secret::keyring::set_global_service_name("himalaya-cli");
|
||||
|
||||
// // if the first argument starts by "mailto:", execute straight the
|
||||
// // mailto message command
|
||||
// let mailto = std::env::args()
|
||||
// .nth(1)
|
||||
// .filter(|arg| arg.starts_with("mailto:"));
|
||||
|
||||
// if let Some(ref url) = mailto {
|
||||
// let mut printer = StdoutPrinter::default();
|
||||
// let config = TomlConfig::from_default_paths().await?;
|
||||
|
||||
// return MessageMailtoCommand::new(url)?
|
||||
// .execute(&mut printer, &config)
|
||||
// .await;
|
||||
// }
|
||||
|
||||
// let cli = Cli::parse();
|
||||
// let mut printer = StdoutPrinter::new(cli.output);
|
||||
// let res = match cli.command {
|
||||
// Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
|
||||
// None => {
|
||||
// let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
|
||||
// EnvelopeListCommand::default()
|
||||
// .execute(&mut printer, &config)
|
||||
// .await
|
||||
// }
|
||||
// };
|
||||
|
||||
// tracing.with_debug_and_trace_notes(res)
|
||||
// }
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
use pimalaya_toolbox::secret::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslLoginConfig {
|
||||
pub username: String,
|
||||
pub password: Secret,
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslPlainConfig {
|
||||
pub authzid: Option<String>,
|
||||
pub authcid: String,
|
||||
pub passwd: Secret,
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslAnonymousConfig {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslConfig {
|
||||
#[serde(default = "default_sasl_mechanisms")]
|
||||
pub mechanisms: Vec<SaslMechanism>,
|
||||
pub anonymous: Option<SaslAnonymousConfig>,
|
||||
pub login: Option<SaslLoginConfig>,
|
||||
pub plain: Option<SaslPlainConfig>,
|
||||
}
|
||||
|
||||
impl Default for SaslConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mechanisms: vec![SaslMechanism::Anonymous],
|
||||
anonymous: None,
|
||||
login: None,
|
||||
plain: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum SaslMechanism {
|
||||
Anonymous,
|
||||
Login,
|
||||
Plain,
|
||||
}
|
||||
|
||||
fn default_sasl_mechanisms() -> Vec<SaslMechanism> {
|
||||
vec![SaslMechanism::Plain, SaslMechanism::Login]
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use io_stream::runtimes::std::handle;
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls::{
|
||||
pki_types::{pem::PemObject, CertificateDer},
|
||||
ClientConfig, ClientConnection, StreamOwned,
|
||||
};
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls_platform_verifier::{ConfigVerifierExt, Verifier};
|
||||
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use io_imap::coroutines::starttls::*;
|
||||
use io_imap::{
|
||||
context::ImapContext,
|
||||
coroutines::{capability::*, greeting_with_capability::*},
|
||||
};
|
||||
|
||||
/// Creates an insecure client, using TCP.
|
||||
///
|
||||
/// This constructor creates a client based on an raw
|
||||
/// [`TcpStream`], receives greeting then saves server
|
||||
/// capabilities.
|
||||
pub fn tcp(host: impl AsRef<str>, port: u16) -> Result<(ImapContext, Stream)> {
|
||||
let mut context = ImapContext::new();
|
||||
let mut tcp = TcpStream::connect((host.as_ref(), port))?;
|
||||
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Ok(out) => break context = out.context,
|
||||
GetImapGreetingWithCapabilityResult::Io(io) => {
|
||||
arg = Some(handle(&mut tcp, io).unwrap())
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok((context, Stream::Tcp(tcp)))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
pub fn rustls(
|
||||
host: impl ToString,
|
||||
port: u16,
|
||||
starttls: bool,
|
||||
cert: Option<PathBuf>,
|
||||
) -> Result<(ImapContext, Stream)> {
|
||||
let host = host.to_string();
|
||||
let mut context = ImapContext::new();
|
||||
let mut tcp = TcpStream::connect((host.as_str(), port))?;
|
||||
|
||||
if starttls {
|
||||
let mut coroutine = ImapStartTls::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapStartTlsResult::Ok(out) => break context = out.context,
|
||||
ImapStartTlsResult::Io(io) => arg = Some(handle(&mut tcp, io)?),
|
||||
ImapStartTlsResult::Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = if let Some(pem_path) = cert {
|
||||
let pem = fs::read(&pem_path)?;
|
||||
|
||||
let Some(cert) = CertificateDer::pem_slice_iter(&pem).next() else {
|
||||
bail!("empty cert at {}", pem_path.display())
|
||||
};
|
||||
|
||||
let verifier = Verifier::new_with_extra_roots(vec![cert?])?;
|
||||
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
.with_no_client_auth()
|
||||
} else {
|
||||
ClientConfig::with_platform_verifier()
|
||||
};
|
||||
|
||||
// See <https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids>
|
||||
config.alpn_protocols = vec![b"imap".to_vec()];
|
||||
|
||||
let server_name = host.try_into()?;
|
||||
let conn = ClientConnection::new(Arc::new(config), server_name)?;
|
||||
let mut tls = StreamOwned::new(conn, tcp);
|
||||
|
||||
if starttls {
|
||||
let mut coroutine = GetImapCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapCapabilityResult::Io(io) => arg = Some(handle(&mut tls, io)?),
|
||||
GetImapCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Ok(out) => break context = out.context,
|
||||
GetImapGreetingWithCapabilityResult::Io(io) => arg = Some(handle(&mut tls, io)?),
|
||||
GetImapGreetingWithCapabilityResult::Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((context, Stream::Rustls(tls)))
|
||||
}
|
||||
|
||||
pub enum Stream {
|
||||
Tcp(TcpStream),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Rustls(StreamOwned<ClientConnection, TcpStream>),
|
||||
}
|
||||
|
||||
impl Read for Stream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Stream::Tcp(stream) => stream.read(buf),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Stream::Rustls(stream) => stream.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Stream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Stream::Tcp(stream) => stream.write(buf),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Stream::Rustls(stream) => stream.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Stream::Tcp(stream) => stream.flush(),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Stream::Rustls(stream) => stream.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct TlsConfig {
|
||||
pub disable: bool,
|
||||
pub provider: Option<TlsProviderConfig>,
|
||||
pub cert: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum TlsProviderConfig {
|
||||
RustlsAws,
|
||||
RustlsRing,
|
||||
NativeTls,
|
||||
}
|
||||
Reference in New Issue
Block a user