#[cfg(feature = "mbox")]
use std::fs::File;
use std::{fmt, path::PathBuf};
use anyhow::{Result, bail};
use clap::Parser;
#[cfg(feature = "imap")]
use io_sasl::mechanism::SaslMechanism;
use pimalaya_cli::printer::Printer;
use pimalaya_config::{secret::SecretResolver, toml::TomlConfig};
use schemars::JsonSchema;
use serde::Serialize;
#[cfg(feature = "imap")]
use crate::config::ImapConfig;
use crate::{
backend::Backend,
config::{AccountConfig, Config},
};
#[derive(Debug, Parser)]
pub struct AccountCheckCommand;
impl AccountCheckCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
backend: Backend,
) -> Result<()> {
let mut config = match Config::from_paths_or_default(config_paths)? {
Some(config) => config,
None => bail!(
"No configuration found. Run bare `himalaya` to launch the wizard \
and generate one."
),
};
let (name, account_config) = config
.take_account(account_name)?
.ok_or_else(|| anyhow::anyhow!("Cannot find account"))?;
let mut report = CheckReport {
account: name,
backends: Vec::new(),
};
#[cfg_attr(
not(any(
feature = "imap",
feature = "jmap",
feature = "gmail",
feature = "msgraph",
feature = "smtp",
feature = "sieve"
)),
allow(unused_mut, unused_variables)
)]
let mut resolver = SecretResolver::new();
#[cfg(feature = "imap")]
if backend.allows_imap()
&& let Some(imap_config) = &account_config.imap
{
report.backends.push(BackendCheck::from(
"imap",
connect_imap(imap_config, &mut resolver),
));
}
#[cfg(feature = "jmap")]
if backend.allows_jmap()
&& let Some(jmap_config) = &account_config.jmap
{
report.backends.push(BackendCheck::from(
"jmap",
connect_jmap(jmap_config, &mut resolver),
));
}
#[cfg(feature = "gmail")]
if backend.allows_gmail()
&& let Some(gmail_config) = &account_config.gmail
{
report.backends.push(BackendCheck::from(
"gmail",
connect_gmail(gmail_config, &mut resolver),
));
}
#[cfg(feature = "msgraph")]
if backend.allows_msgraph()
&& let Some(msgraph_config) = &account_config.msgraph
{
report.backends.push(BackendCheck::from(
"msgraph",
connect_msgraph(msgraph_config, &mut resolver),
));
}
#[cfg(feature = "maildir")]
if backend.allows_maildir()
&& let Some(maildir_config) = &account_config.maildir
{
report.backends.push(BackendCheck::from(
"maildir",
connect_maildir(maildir_config),
));
}
#[cfg(feature = "m2dir")]
if backend.allows_m2dir()
&& let Some(m2dir_config) = &account_config.m2dir
{
report
.backends
.push(BackendCheck::from("m2dir", connect_m2dir(m2dir_config)));
}
#[cfg(feature = "mbox")]
if backend.allows_mbox()
&& let Some(mbox_config) = &account_config.mbox
{
report
.backends
.push(BackendCheck::from("mbox", connect_mbox(mbox_config)));
}
#[cfg(feature = "smtp")]
if backend.allows_smtp()
&& let Some(smtp_config) = &account_config.smtp
{
report.backends.push(BackendCheck::from(
"smtp",
connect_smtp(smtp_config, &mut resolver),
));
}
#[cfg(feature = "sieve")]
if backend.allows_sieve()
&& let Some(sieve_config) = &account_config.sieve
{
report.backends.push(BackendCheck::from(
"sieve",
connect_sieve(sieve_config, &mut resolver),
));
}
if report.backends.is_empty() {
return Err(account_config.no_backend_error(
&report.account,
backend,
Backend::COMPILED,
));
}
printer.out(report)
}
}
pub fn test_account(account_config: &AccountConfig) -> Result<()> {
#[cfg_attr(
not(any(
feature = "imap",
feature = "jmap",
feature = "gmail",
feature = "msgraph",
feature = "smtp",
feature = "sieve"
)),
allow(unused_mut, unused_variables)
)]
let mut resolver = SecretResolver::new();
#[cfg(feature = "imap")]
if let Some(imap_config) = &account_config.imap {
connect_imap(imap_config, &mut resolver)?;
}
#[cfg(feature = "jmap")]
if let Some(jmap_config) = &account_config.jmap {
connect_jmap(jmap_config, &mut resolver)?;
}
#[cfg(feature = "gmail")]
if let Some(gmail_config) = &account_config.gmail {
connect_gmail(gmail_config, &mut resolver)?;
}
#[cfg(feature = "msgraph")]
if let Some(msgraph_config) = &account_config.msgraph {
connect_msgraph(msgraph_config, &mut resolver)?;
}
#[cfg(feature = "maildir")]
if let Some(maildir_config) = &account_config.maildir {
connect_maildir(maildir_config)?;
}
#[cfg(feature = "m2dir")]
if let Some(m2dir_config) = &account_config.m2dir {
connect_m2dir(m2dir_config)?;
}
#[cfg(feature = "mbox")]
if let Some(mbox_config) = &account_config.mbox {
connect_mbox(mbox_config)?;
}
#[cfg(feature = "smtp")]
if let Some(smtp_config) = &account_config.smtp {
connect_smtp(smtp_config, &mut resolver)?;
}
#[cfg(feature = "sieve")]
if let Some(sieve_config) = &account_config.sieve {
connect_sieve(sieve_config, &mut resolver)?;
}
Ok(())
}
#[cfg(feature = "imap")]
pub(crate) fn connect_imap(imap_config: &ImapConfig, resolver: &mut SecretResolver) -> Result<()> {
use io_imap::{
client::{ImapClientStd, ImapClientStdConnectOptions, default_port},
session::ImapSessionOpenOptions,
};
use io_sasl::mechanism::Sasl;
use crate::{
config::ProxyConfig,
imap::{client::parse_imap_server, id::resolve_auto_id_params},
};
let auto_id = resolve_auto_id_params(&imap_config.id)?;
let server = parse_imap_server(&imap_config.server)?;
let sasl: Option<Sasl> = imap_config
.sasl
.clone()
.map(|cfg| {
let host = server.host_str().unwrap_or_default();
let port = server.port().unwrap_or(default_port(server.scheme()));
cfg.try_into_sasl(host, port, resolver)
})
.transpose()?;
let opts = ImapClientStdConnectOptions {
tls: imap_config.tls.clone().into_tls(imap_config.alpn.clone()),
proxy: ProxyConfig::resolve(imap_config.proxy.clone(), resolver)?,
sasl,
session: ImapSessionOpenOptions {
starttls: imap_config.starttls,
auto_id,
sasl_ir: imap_config.sasl_ir,
},
};
let _ = ImapClientStd::connect(&server, opts)?;
Ok(())
}
#[cfg(feature = "imap")]
pub(crate) fn probe_imap_mechanisms(server: &str, starttls: bool) -> Result<Vec<SaslMechanism>> {
use io_imap::{
client::{ImapClientStd, ImapClientStdConnectOptions, default_alpn},
rfc3501::capability::available_auth_mechanisms,
session::ImapSessionOpenOptions,
};
use crate::{config::TlsConfig, imap::client::parse_imap_server};
let server = parse_imap_server(server)?;
let opts = ImapClientStdConnectOptions {
tls: TlsConfig::default().into_tls(default_alpn()),
session: ImapSessionOpenOptions {
starttls,
..Default::default()
},
..Default::default()
};
let (_client, capabilities) = ImapClientStd::connect(&server, opts)?;
Ok(available_auth_mechanisms(&capabilities))
}
#[cfg(feature = "jmap")]
fn connect_jmap(
jmap_config: &crate::config::JmapConfig,
resolver: &mut SecretResolver,
) -> Result<()> {
use io_jmap::client::JmapClientStd;
use crate::jmap::client::{connect_options, jmap_http_auth, parse_server_url};
let http_auth = jmap_http_auth(jmap_config.auth.clone(), resolver)?;
let opts = connect_options(jmap_config, resolver)?;
let url = parse_server_url(&jmap_config.server)?;
let mut client = JmapClientStd::connect(&url, http_auth, opts)?;
client.session_get(&url)?;
Ok(())
}
#[cfg(feature = "gmail")]
fn connect_gmail(
gmail_config: &crate::config::GmailConfig,
resolver: &mut SecretResolver,
) -> Result<()> {
use io_gmail::v1::client::{GmailClientStd, GmailClientStdConnectOptions};
use secrecy::ExposeSecret;
use crate::{config::ProxyConfig, gmail::client::gmail_token};
let token = gmail_token(gmail_config.auth.clone(), resolver)?;
let options = GmailClientStdConnectOptions {
tls: gmail_config.tls.clone().into_tls(gmail_config.alpn.clone()),
proxy: ProxyConfig::resolve(gmail_config.proxy.clone(), resolver)?,
user_id: gmail_config.user_id.clone(),
};
let mut client = GmailClientStd::connect(token.expose_secret(), options)?;
client.profile_get()?;
Ok(())
}
#[cfg(feature = "msgraph")]
fn connect_msgraph(
msgraph_config: &crate::config::MsgraphConfig,
resolver: &mut SecretResolver,
) -> Result<()> {
use io_msgraph::v1::client::{MsgraphClientStd, MsgraphClientStdConnectOptions};
use secrecy::ExposeSecret;
use crate::{config::ProxyConfig, msgraph::client::msgraph_token};
let token = msgraph_token(msgraph_config.auth.clone(), resolver)?;
let options = MsgraphClientStdConnectOptions {
tls: msgraph_config
.tls
.clone()
.into_tls(msgraph_config.alpn.clone()),
proxy: ProxyConfig::resolve(msgraph_config.proxy.clone(), resolver)?,
user_id: msgraph_config.user_id.clone(),
};
let mut client = MsgraphClientStd::connect(token.expose_secret(), options)?;
client.me()?;
Ok(())
}
#[cfg(feature = "maildir")]
fn connect_maildir(maildir_config: &crate::config::MaildirConfig) -> Result<()> {
if !maildir_config.root.is_dir() {
bail!(
"Maildir root `{}` does not exist or is not a directory",
maildir_config.root.display()
);
}
Ok(())
}
#[cfg(feature = "mbox")]
fn connect_mbox(mbox_config: &crate::config::MboxConfig) -> Result<()> {
if !mbox_config.root.is_dir() {
bail!(
"Mbox root `{}` does not exist or is not a directory",
mbox_config.root.display()
);
}
if let Some(inbox) = &mbox_config.inbox
&& let Err(err) = File::open(inbox)
{
bail!("Cannot open mbox spool `{}`: {err}", inbox.display());
}
Ok(())
}
#[cfg(feature = "m2dir")]
fn connect_m2dir(m2dir_config: &crate::config::M2dirConfig) -> Result<()> {
if !m2dir_config.root.is_dir() {
bail!(
"m2dir root `{}` does not exist or is not a directory",
m2dir_config.root.display()
);
}
Ok(())
}
#[cfg(feature = "smtp")]
pub(crate) fn connect_smtp(
smtp_config: &crate::config::SmtpConfig,
resolver: &mut SecretResolver,
) -> Result<()> {
use std::net::Ipv4Addr;
use io_sasl::mechanism::Sasl;
use io_smtp::{
client::{SmtpClientStd, SmtpClientStdConnectOptions},
rfc5321::SmtpEhloDomain,
session::SmtpSessionOpenOptions,
};
use crate::config::ProxyConfig;
let domain: SmtpEhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into();
let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?;
let sasl: Option<Sasl> = smtp_config
.sasl
.clone()
.map(|cfg| {
let host = server.host_str().unwrap_or_default();
let port = server
.port()
.unwrap_or(SmtpClientStd::default_port(server.scheme()));
cfg.try_into_sasl(host, port, resolver)
})
.transpose()?;
let opts = SmtpClientStdConnectOptions {
tls: smtp_config.tls.clone().into_tls(smtp_config.alpn.clone()),
proxy: ProxyConfig::resolve(smtp_config.proxy.clone(), resolver)?,
sasl,
session: SmtpSessionOpenOptions {
starttls: smtp_config.starttls,
},
};
let _client = SmtpClientStd::connect(&server, domain, opts)?;
Ok(())
}
#[cfg(feature = "sieve")]
pub(crate) fn connect_sieve(
sieve_config: &crate::config::SieveConfig,
resolver: &mut SecretResolver,
) -> Result<()> {
let _client = crate::sieve::client::SieveClient::new(sieve_config.clone(), resolver)?;
Ok(())
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct CheckReport {
pub account: String,
pub backends: Vec<BackendCheck>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct BackendCheck {
pub backend: &'static str,
pub ok: bool,
pub error: Option<String>,
}
impl BackendCheck {
fn from(backend: &'static str, result: Result<()>) -> Self {
match result {
Ok(()) => Self {
backend,
ok: true,
error: None,
},
Err(err) => Self {
backend,
ok: false,
error: Some(format!("{err:#}")),
},
}
}
}
impl fmt::Display for CheckReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Account: {}", self.account)?;
for check in &self.backends {
match &check.error {
None => writeln!(f, " {}: OK", check.backend)?,
Some(err) => writeln!(f, " {}: FAIL ({err})", check.backend)?,
}
}
Ok(())
}
}