mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-16 20:57:53 +08:00
add support for native tls
This commit is contained in:
@@ -1 +0,0 @@
|
||||
pub mod name;
|
||||
@@ -1,37 +0,0 @@
|
||||
use clap::Parser;
|
||||
|
||||
/// The account name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountNameArg {
|
||||
/// The name of the account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
#[arg(name = "account_name", value_name = "ACCOUNT")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The optional account name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct OptionalAccountNameArg {
|
||||
/// The name of the account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
///
|
||||
/// If omitted, the account marked as default will be used.
|
||||
#[arg(name = "account_name", value_name = "ACCOUNT")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// The account name flag parser.
|
||||
#[derive(Debug, Default, Parser)]
|
||||
pub struct AccountNameFlag {
|
||||
/// Override the default account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
#[arg(long = "account", short = 'a')]
|
||||
#[arg(name = "account_name", value_name = "NAME")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Configure the given account.
|
||||
///
|
||||
/// This command allows you to configure an existing account or to
|
||||
/// create a new one, using the wizard. The `wizard` cargo feature is
|
||||
/// required.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountConfigureCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameArg,
|
||||
}
|
||||
|
||||
impl AccountConfigureCommand {
|
||||
#[cfg(feature = "wizard")]
|
||||
pub async fn execute(
|
||||
self,
|
||||
mut config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
info!("executing account configure command");
|
||||
|
||||
let path = match config_path {
|
||||
Some(path) => path.clone(),
|
||||
None => TomlConfig::default_path()?,
|
||||
};
|
||||
|
||||
let account_name = Some(self.account.name.as_str());
|
||||
|
||||
let account_config = config
|
||||
.accounts
|
||||
.remove(&self.account.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
wizard::edit(path, config, account_name, account_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wizard"))]
|
||||
pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> {
|
||||
color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work");
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use std::{
|
||||
io::{stdout, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{Result, Section};
|
||||
#[cfg(all(feature = "keyring", feature = "imap"))]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::ImapContextBuilder;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::MaildirContextBuilder;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::NotmuchContextBuilder;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::SendmailContextBuilder;
|
||||
#[cfg(all(feature = "keyring", feature = "smtp"))]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::SmtpContextBuilder;
|
||||
use email::{backend::BackendBuilder, config::Config};
|
||||
#[cfg(feature = "keyring")]
|
||||
use pimalaya_tui::terminal::prompt;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Backend, SendingBackend},
|
||||
terminal::config::TomlConfig as _,
|
||||
};
|
||||
|
||||
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Diagnose and fix the given account.
|
||||
///
|
||||
/// This command diagnoses the given account and can even try to fix
|
||||
/// it. It mostly checks if the configuration is valid, if backends
|
||||
/// can be instanciated and if sessions work as expected.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountDoctorCommand {
|
||||
#[command(flatten)]
|
||||
pub account: OptionalAccountNameArg,
|
||||
|
||||
/// Try to fix the given account.
|
||||
///
|
||||
/// This argument can be used to (re)configure keyring entries for
|
||||
/// example.
|
||||
#[arg(long, short)]
|
||||
pub fix: bool,
|
||||
}
|
||||
|
||||
impl AccountDoctorCommand {
|
||||
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
|
||||
let mut stdout = stdout();
|
||||
|
||||
if let Some(name) = self.account.name.as_ref() {
|
||||
print!("Checking TOML configuration integrity for account {name}… ");
|
||||
} else {
|
||||
print!("Checking TOML configuration integrity for default account… ");
|
||||
}
|
||||
|
||||
stdout.flush()?;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
println!("OK");
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if self.fix {
|
||||
if prompt::bool("Would you like to reset existing keyring entries?", false)? {
|
||||
print!("Resetting keyring entries… ");
|
||||
stdout.flush()?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("IMAP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("SMTP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config
|
||||
.configure(&toml_account_config.email, || {
|
||||
Ok(prompt::password("PGP secret key password")?)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
match toml_account_config.backend {
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(Backend::Maildir(mdir_config)) => {
|
||||
print!("Checking Maildir integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(Backend::Imap(imap_config)) => {
|
||||
print!("Checking IMAP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
|
||||
.with_pool_size(1);
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(Backend::Notmuch(notmuch_config)) => {
|
||||
print!("Checking Notmuch integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let sending_backend = toml_account_config
|
||||
.message
|
||||
.and_then(|msg| msg.send)
|
||||
.and_then(|send| send.backend);
|
||||
|
||||
match sending_backend {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(SendingBackend::Smtp(smtp_config)) => {
|
||||
print!("Checking SMTP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(SendingBackend::Sendmail(sendmail_config)) => {
|
||||
print!("Checking Sendmail integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Accounts, AccountsTable},
|
||||
terminal::cli::printer::Printer,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
/// List all existing accounts.
|
||||
///
|
||||
/// This command lists all the accounts defined in your TOML
|
||||
/// configuration file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountListCommand {
|
||||
/// 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 AccountListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list accounts command");
|
||||
|
||||
let accounts = Accounts::from(config.accounts.iter());
|
||||
let table = AccountsTable::from(accounts)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(config.account_list_table_preset())
|
||||
.with_some_name_color(config.account_list_table_name_color())
|
||||
.with_some_backends_color(config.account_list_table_backends_color())
|
||||
.with_some_default_color(config.account_list_table_default_color());
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
mod configure;
|
||||
mod doctor;
|
||||
mod list;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
|
||||
};
|
||||
|
||||
/// Configure, list and diagnose your accounts.
|
||||
///
|
||||
/// An account is a group of settings, identified by a unique
|
||||
/// name. This subcommand allows you to manage your accounts.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountSubcommand {
|
||||
Configure(AccountConfigureCommand),
|
||||
Doctor(AccountDoctorCommand),
|
||||
List(AccountListCommand),
|
||||
}
|
||||
|
||||
impl AccountSubcommand {
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Configure(cmd) => cmd.execute(config, config_path).await,
|
||||
Self::Doctor(cmd) => cmd.execute(&config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, &config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
|
||||
|
||||
pub type TomlAccountConfig = HimalayaTomlAccountConfig;
|
||||
@@ -1,102 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
+9
-5
@@ -1,13 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use pimalaya_toolbox::{
|
||||
config::TomlConfig,
|
||||
long_version,
|
||||
terminal::{
|
||||
clap::{
|
||||
args::{AccountArg, JsonFlag, LogFlags},
|
||||
args::{AccountFlag, JsonFlag, LogFlags},
|
||||
parsers::path_parser,
|
||||
},
|
||||
printer::Printer,
|
||||
@@ -41,7 +41,7 @@ pub struct HimalayaCli {
|
||||
#[arg(value_name = "PATH", value_parser = path_parser, value_delimiter = ':')]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
#[command(flatten)]
|
||||
pub account: AccountArg,
|
||||
pub account: AccountFlag,
|
||||
#[command(flatten)]
|
||||
pub json: JsonFlag,
|
||||
#[command(flatten)]
|
||||
@@ -65,8 +65,12 @@ impl BackendCommand {
|
||||
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)
|
||||
let (_, account_config) = config.get_account(account_name)?;
|
||||
let Some(imap_config) = account_config.imap else {
|
||||
bail!("IMAP config is missing for this account")
|
||||
};
|
||||
|
||||
cmd.execute(printer, imap_config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+206
-7
@@ -1,11 +1,12 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{collections::HashMap, fmt, path::PathBuf, process::Command};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use pimalaya_toolbox::config::TomlConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use secrecy::SecretString;
|
||||
use serde::{de::Visitor, Deserialize, Deserializer};
|
||||
use url::Url;
|
||||
|
||||
use crate::account::Account;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(alias = "name")]
|
||||
@@ -13,12 +14,12 @@ pub struct Config {
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub accounts: HashMap<String, Account>,
|
||||
pub accounts: HashMap<String, AccountConfig>,
|
||||
// pub account: Option<AccountsConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig for Config {
|
||||
type Account = Account;
|
||||
type Account = AccountConfig;
|
||||
|
||||
fn project_name() -> &'static str {
|
||||
env!("CARGO_PKG_NAME")
|
||||
@@ -37,3 +38,201 @@ impl TomlConfig for Config {
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct AccountConfig {
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// The account configuration.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapConfig {
|
||||
pub url: Url,
|
||||
|
||||
#[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, 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, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapIdExtensionConfig {
|
||||
/// Automatically sends the ID command straight after
|
||||
/// authentication.
|
||||
#[serde(default)]
|
||||
always_after_auth: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct TlsConfig {
|
||||
pub provider: Option<TlsProviderConfig>,
|
||||
#[serde(default)]
|
||||
pub rustls: RustlsConfig,
|
||||
pub cert: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum TlsProviderConfig {
|
||||
Rustls,
|
||||
NativeTls,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct RustlsConfig {
|
||||
pub crypto: Option<RustlsCryptoConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum RustlsCryptoConfig {
|
||||
Aws,
|
||||
Ring,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslConfig {
|
||||
#[serde(default = "default_sasl_mechanisms")]
|
||||
pub mechanisms: Vec<SaslMechanismConfig>,
|
||||
pub login: Option<SaslLoginConfig>,
|
||||
pub plain: Option<SaslPlainConfig>,
|
||||
pub anonymous: Option<SaslAnonymousConfig>,
|
||||
}
|
||||
|
||||
fn default_sasl_mechanisms() -> Vec<SaslMechanismConfig> {
|
||||
vec![SaslMechanismConfig::Plain, SaslMechanismConfig::Login]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SaslMechanismConfig {
|
||||
Login,
|
||||
Plain,
|
||||
Anonymous,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SecretConfig {
|
||||
Raw(SecretString),
|
||||
Command(Vec<String>),
|
||||
}
|
||||
|
||||
impl SecretConfig {
|
||||
pub fn get(&self) -> Result<SecretString> {
|
||||
match self {
|
||||
Self::Raw(secret) => Ok(secret.clone()),
|
||||
Self::Command(args) => {
|
||||
let Some((program, args)) = args.split_first() else {
|
||||
bail!("Secret command cannot be empty")
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
let out = cmd.output()?;
|
||||
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
bail!("Cannot read secret from command: {err}");
|
||||
}
|
||||
|
||||
let secret = String::from_utf8_lossy(&out.stdout);
|
||||
let secret = secret.trim_matches(['\r', '\n']);
|
||||
let secret = match secret.split_once('\n') {
|
||||
Some((secret, _)) => secret.trim_matches(['\r', '\n']),
|
||||
None => secret,
|
||||
};
|
||||
|
||||
Ok(SecretString::from(secret))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslLoginConfig {
|
||||
pub username: String,
|
||||
pub password: SecretConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslPlainConfig {
|
||||
pub authzid: Option<String>,
|
||||
pub authcid: String,
|
||||
pub passwd: SecretConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SaslAnonymousConfig {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::{account::Account, imap::mailbox::command::MailboxCommand};
|
||||
use crate::{config::ImapConfig, imap::mailbox::command::MailboxCommand};
|
||||
|
||||
/// IMAP CLI (requires `imap` cargo feature).
|
||||
///
|
||||
@@ -18,9 +18,9 @@ pub enum ImapCommand {
|
||||
}
|
||||
|
||||
impl ImapCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Mailboxes(cmd) => cmd.execute(printer, account),
|
||||
Self::Mailboxes(cmd) => cmd.execute(printer, config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_imap::{
|
||||
coroutines::{
|
||||
authenticate::*, authenticate_anonymous::*, authenticate_plain::*, list::*, login::*,
|
||||
},
|
||||
types::response::Capability,
|
||||
};
|
||||
use io_imap::coroutines::list::*;
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::{account::Account, sasl::SaslMechanism, stream};
|
||||
use crate::{config::ImapConfig, imap::stream};
|
||||
|
||||
/// List all mailboxes.
|
||||
///
|
||||
@@ -29,71 +23,8 @@ pub struct ListMailboxesCommand {
|
||||
}
|
||||
|
||||
impl ListMailboxesCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
let imap = account.imap.unwrap();
|
||||
|
||||
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 ir = context.capability.contains(&Capability::SaslIr);
|
||||
|
||||
let mut candidates = vec![];
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
pub fn execute(self, _printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
|
||||
let (context, mut stream) = stream::connect(config)?;
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = ImapList::new(context, "".try_into().unwrap(), "*".try_into().unwrap());
|
||||
@@ -101,23 +32,114 @@ impl ListMailboxesCommand {
|
||||
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),
|
||||
ImapListResult::Ok { mailboxes, .. } => break mailboxes,
|
||||
ImapListResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
println!("mailboxes: {mailboxes:#?}");
|
||||
|
||||
// TODO: list folders
|
||||
// TODO: list mailboxs
|
||||
|
||||
// let folders = Folders::from(backend.list_folders().await?);
|
||||
// let table = FoldersTable::from(folders)
|
||||
// let mailboxs = Mailboxs::from(backend.list_mailboxs().await?);
|
||||
// let table = MailboxsTable::from(mailboxs)
|
||||
// .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());
|
||||
// .with_some_preset(toml_account_config.mailbox_list_table_preset())
|
||||
// .with_some_name_color(toml_account_config.mailbox_list_table_name_color())
|
||||
// .with_some_desc_color(toml_account_config.mailbox_list_table_desc_color());
|
||||
|
||||
// printer.out(table)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
// #[serde(rename_all = "kebab-case")]
|
||||
// pub struct ListMailboxesTableConfig {
|
||||
// pub preset: Option<String>,
|
||||
// pub name_color: Option<Color>,
|
||||
// pub desc_color: Option<Color>,
|
||||
// }
|
||||
|
||||
// impl ListMailboxesTableConfig {
|
||||
// pub fn preset(&self) -> &str {
|
||||
// self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
|
||||
// }
|
||||
|
||||
// pub fn name_color(&self) -> comfy_table::Color {
|
||||
// map_color(self.name_color.unwrap_or(Color::Blue))
|
||||
// }
|
||||
|
||||
// pub fn desc_color(&self) -> comfy_table::Color {
|
||||
// map_color(self.desc_color.unwrap_or(Color::Green))
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub struct MailboxesTable {
|
||||
// mailboxes: Vec<Mailbox<'static>>,
|
||||
// width: Option<u16>,
|
||||
// config: ListMailboxesTableConfig,
|
||||
// }
|
||||
|
||||
// impl MailboxesTable {
|
||||
// pub fn with_some_width(mut self, width: Option<u16>) -> Self {
|
||||
// self.width = width;
|
||||
// self
|
||||
// }
|
||||
|
||||
// pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
|
||||
// self.config.preset = preset;
|
||||
// self
|
||||
// }
|
||||
|
||||
// pub fn with_some_name_color(mut self, color: Option<Color>) -> Self {
|
||||
// self.config.name_color = color;
|
||||
// self
|
||||
// }
|
||||
|
||||
// pub fn with_some_desc_color(mut self, color: Option<Color>) -> Self {
|
||||
// self.config.desc_color = color;
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<Mailboxes> for MailboxesTable {
|
||||
// fn from(mailboxes: Mailboxes) -> Self {
|
||||
// Self {
|
||||
// mailboxes,
|
||||
// width: None,
|
||||
// config: Default::default(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl fmt::Display for MailboxesTable {
|
||||
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// let mut table = Table::new();
|
||||
|
||||
// table
|
||||
// .load_preset(self.config.preset())
|
||||
// .set_content_arrangement(ContentArrangement::DynamicFullWidth)
|
||||
// .set_header(Row::from([Cell::new("NAME"), Cell::new("DESC")]))
|
||||
// .add_rows(
|
||||
// self.mailboxes
|
||||
// .iter()
|
||||
// .map(|mailbox| mailbox.to_row(&self.config)),
|
||||
// );
|
||||
|
||||
// if let Some(width) = self.width {
|
||||
// table.set_width(width);
|
||||
// }
|
||||
|
||||
// writeln!(f)?;
|
||||
// write!(f, "{table}")?;
|
||||
// writeln!(f)?;
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Serialize for MailboxesTable {
|
||||
// fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
// self.mailboxes.serialize(serializer)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::{account::Account, imap::mailbox::command::list::ListMailboxesCommand};
|
||||
use crate::{config::ImapConfig, imap::mailbox::command::list::ListMailboxesCommand};
|
||||
|
||||
/// Create, list and purge mailboxes.
|
||||
///
|
||||
@@ -31,10 +31,10 @@ pub enum MailboxCommand {
|
||||
|
||||
impl MailboxCommand {
|
||||
#[allow(unused)]
|
||||
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
|
||||
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
|
||||
match self {
|
||||
// Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, account),
|
||||
Self::List(cmd) => cmd.execute(printer, config),
|
||||
// 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,2 +1,3 @@
|
||||
pub mod command;
|
||||
pub mod mailbox;
|
||||
pub mod stream;
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
net::TcpStream,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use io_imap::{
|
||||
context::ImapContext,
|
||||
coroutines::{
|
||||
authenticate::*, authenticate_anonymous::ImapAuthenticateAnonymousParams,
|
||||
authenticate_plain::ImapAuthenticatePlainParams, capability::*,
|
||||
greeting_with_capability::*, login::ImapLoginParams, starttls::*,
|
||||
},
|
||||
types::{auth::AuthMechanism, response::Capability},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::{debug, info};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsConnector;
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls::{
|
||||
crypto::{self, CryptoProvider},
|
||||
pki_types::{pem::PemObject, CertificateDer},
|
||||
ClientConfig, ClientConnection, StreamOwned,
|
||||
};
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls_platform_verifier::{ConfigVerifierExt, Verifier};
|
||||
|
||||
use crate::config::{ImapConfig, RustlsCryptoConfig, SaslMechanismConfig, TlsProviderConfig};
|
||||
|
||||
pub enum Stream {
|
||||
Plain(TcpStream),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Rustls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(native_tls::TlsStream<TcpStream>),
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Plain(s) => s.set_read_timeout(dur),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Self::Rustls(s) => s.get_ref().set_read_timeout(dur),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls(s) => s.get_ref().set_read_timeout(dur),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Stream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Plain(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::Plain(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::Plain(s) => s.flush(),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Self::Rustls(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls(s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, Stream)> {
|
||||
info!("connecting to IMAP server using {}", config.url);
|
||||
|
||||
let mut context = ImapContext::new();
|
||||
let host = config.url.host_str().unwrap_or("127.0.0.1");
|
||||
|
||||
let (mut context, mut stream) = match config.url.scheme() {
|
||||
scheme if scheme.eq_ignore_ascii_case("imap") => {
|
||||
let port = config.url.port().unwrap_or(143);
|
||||
let mut stream = TcpStream::connect((host, port))?;
|
||||
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Io(io) => {
|
||||
arg = Some(handle(&mut stream, io)?)
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapGreetingWithCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
(context, Stream::Plain(stream))
|
||||
}
|
||||
scheme if scheme.eq_ignore_ascii_case("imaps") => {
|
||||
let port = config.url.port().unwrap_or(993);
|
||||
let mut tcp = TcpStream::connect((host, port))?;
|
||||
|
||||
if config.starttls {
|
||||
let mut coroutine = ImapStartTls::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapStartTlsResult::Io(io) => arg = Some(handle(&mut tcp, io)?),
|
||||
ImapStartTlsResult::Ok { context: c } => break context = c,
|
||||
ImapStartTlsResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tls_provider = match config.tls.provider {
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Some(TlsProviderConfig::Rustls) => TlsProviderConfig::Rustls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
Some(TlsProviderConfig::Rustls) => {
|
||||
bail!("Required cargo feature: `rustls-aws` or `rustls-ring`")
|
||||
}
|
||||
#[cfg(feature = "native-tls")]
|
||||
Some(TlsProviderConfig::NativeTls) => TlsProviderConfig::NativeTls,
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
Some(TlsProviderConfig::NativeTls) => {
|
||||
bail!("Required cargo feature: `native-tls`")
|
||||
}
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
None => TlsProviderConfig::Rustls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(feature = "native-tls")]
|
||||
None => TlsProviderConfig::NativeTls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
None => {
|
||||
bail!("Required cargo feature: `rustls-aws`, `rustls-ring` or `native-tls`")
|
||||
}
|
||||
};
|
||||
|
||||
debug!("using TLS provider: {tls_provider:?}");
|
||||
|
||||
let mut stream = match tls_provider {
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
TlsProviderConfig::Rustls => {
|
||||
let crypto_provider = match config.tls.rustls.crypto {
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
Some(RustlsCryptoConfig::Aws) => RustlsCryptoConfig::Aws,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
Some(RustlsCryptoConfig::Aws) => {
|
||||
bail!("Required cargo feature: `rustls-aws`");
|
||||
}
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
Some(RustlsCryptoConfig::Ring) => RustlsCryptoConfig::Ring,
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
Some(RustlsCryptoConfig::Ring) => {
|
||||
bail!("Required cargo feature: `rustls-ring`");
|
||||
}
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
None => RustlsCryptoConfig::Ring,
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
None => RustlsCryptoConfig::Aws,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
None => {
|
||||
bail!("Required cargo feature: `rustls-aws` or `rustls-ring`");
|
||||
}
|
||||
};
|
||||
|
||||
debug!("using rustls crypto provider: {crypto_provider:?}");
|
||||
|
||||
let crypto_provider = match crypto_provider {
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
RustlsCryptoConfig::Aws => crypto::aws_lc_rs::default_provider(),
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
RustlsCryptoConfig::Ring => crypto::ring::default_provider(),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let crypto_provider = match crypto_provider.install_default() {
|
||||
Ok(()) => CryptoProvider::get_default().unwrap().clone(),
|
||||
Err(crypto_provider) => crypto_provider,
|
||||
};
|
||||
|
||||
let mut config = if let Some(pem_path) = &config.tls.cert {
|
||||
debug!("using TLS cert at {}", pem_path.display());
|
||||
let pem = fs::read(pem_path)?;
|
||||
|
||||
let Some(cert) = CertificateDer::pem_slice_iter(&pem).next() else {
|
||||
bail!("empty TLS cert at {}", pem_path.display())
|
||||
};
|
||||
|
||||
let verifier =
|
||||
Verifier::new_with_extra_roots(vec![cert?], crypto_provider)?;
|
||||
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
.with_no_client_auth()
|
||||
} else {
|
||||
debug!("using OS TLS certs");
|
||||
ClientConfig::with_platform_verifier()?
|
||||
};
|
||||
|
||||
config.alpn_protocols = vec![b"imap".to_vec()];
|
||||
|
||||
let server_name = host.to_string().try_into()?;
|
||||
let conn = ClientConnection::new(Arc::new(config), server_name)?;
|
||||
Stream::Rustls(StreamOwned::new(conn, tcp))
|
||||
}
|
||||
#[cfg(feature = "native-tls")]
|
||||
TlsProviderConfig::NativeTls => {
|
||||
let mut builder = TlsConnector::builder();
|
||||
|
||||
if let Some(pem_path) = &config.tls.cert {
|
||||
debug!("using TLS cert at {}", pem_path.display());
|
||||
let pem = fs::read(pem_path)?;
|
||||
let cert = native_tls::Certificate::from_pem(&pem)?;
|
||||
builder.add_root_certificate(cert);
|
||||
}
|
||||
|
||||
let connector = builder.build()?;
|
||||
Stream::NativeTls(connector.connect(host, tcp)?)
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if config.starttls {
|
||||
let mut coroutine = GetImapCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapCapabilityResult::Io(io) => arg = Some(handle(&mut stream, io)?),
|
||||
GetImapCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Io(io) => {
|
||||
arg = Some(handle(&mut stream, io)?)
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Ok { context: c } => {
|
||||
break context = c
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(context, stream)
|
||||
}
|
||||
scheme if scheme.eq_ignore_ascii_case("unix") => {
|
||||
todo!()
|
||||
}
|
||||
scheme => {
|
||||
bail!("Unknown scheme {scheme}, expected imap, imaps or unix");
|
||||
}
|
||||
};
|
||||
|
||||
if !context.authenticated {
|
||||
let mut candidates = vec![];
|
||||
|
||||
let ir = context.capability.contains(&Capability::SaslIr);
|
||||
|
||||
for mechanism in config.sasl.mechanisms {
|
||||
match mechanism {
|
||||
SaslMechanismConfig::Login => {
|
||||
let Some(auth) = config.sasl.login.take() else {
|
||||
debug!("missing SASL LOGIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
if context.capability.contains(&Capability::LoginDisabled) {
|
||||
debug!("SASL LOGIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
let login = Capability::Auth(AuthMechanism::Login);
|
||||
if !context.capability.contains(&login) {
|
||||
debug!("SASL LOGIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Login(ImapLoginParams::new(
|
||||
auth.username,
|
||||
auth.password.get()?,
|
||||
)?));
|
||||
}
|
||||
SaslMechanismConfig::Plain => {
|
||||
let Some(auth) = config.sasl.plain.take() else {
|
||||
debug!("missing SASL PLAIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
let plain = Capability::Auth(AuthMechanism::Plain);
|
||||
if !context.capability.contains(&plain) {
|
||||
debug!("SASL PLAIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Plain(
|
||||
ImapAuthenticatePlainParams::new(
|
||||
auth.authzid,
|
||||
auth.authcid,
|
||||
auth.passwd.get()?,
|
||||
ir,
|
||||
),
|
||||
));
|
||||
}
|
||||
SaslMechanismConfig::Anonymous => {
|
||||
// TODO: check if capability available
|
||||
|
||||
let message = config
|
||||
.sasl
|
||||
.anonymous
|
||||
.take()
|
||||
.and_then(|auth| auth.message)
|
||||
.unwrap_or_default();
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Anonymous(
|
||||
ImapAuthenticateAnonymousParams::new(message, ir),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((context, stream))
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
pub mod account;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
#[cfg(feature = "imap")]
|
||||
pub mod imap;
|
||||
pub mod sasl;
|
||||
pub mod stream;
|
||||
pub mod tls;
|
||||
// pub mod email;
|
||||
|
||||
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user