add support for native tls

This commit is contained in:
Clément DOUIN
2026-03-03 16:29:33 +01:00
parent 0ec0159a28
commit 297f5773aa
21 changed files with 771 additions and 855 deletions
-1
View File
@@ -1 +0,0 @@
pub mod name;
-37
View File
@@ -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>,
}
-52
View File
@@ -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");
}
}
-233
View File
@@ -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(())
}
}
-41
View File
@@ -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)
}
}
-41
View File
@@ -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,
}
}
}
-3
View File
@@ -1,3 +0,0 @@
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
pub type TomlAccountConfig = HimalayaTomlAccountConfig;
-102
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
}
}
}
+103 -81
View File
@@ -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)
// }
// }
+3 -3
View File
@@ -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
View File
@@ -1,2 +1,3 @@
pub mod command;
pub mod mailbox;
pub mod stream;
+367
View File
@@ -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))
}
-2
View File
@@ -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
View File
@@ -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(),
}
}
}