fix wizard

This commit is contained in:
Clément DOUIN 2023-12-03 22:31:43 +01:00
parent f24a0475cc
commit c54ada730b
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
29 changed files with 292 additions and 177 deletions

19
src/backend/config.rs Normal file
View file

@ -0,0 +1,19 @@
#[cfg(feature = "imap-backend")]
use email::imap::ImapConfig;
#[cfg(feature = "notmuch-backend")]
use email::notmuch::NotmuchConfig;
#[cfg(feature = "smtp-sender")]
use email::smtp::SmtpConfig;
use email::{maildir::MaildirConfig, sendmail::SendmailConfig};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendConfig {
Maildir(MaildirConfig),
#[cfg(feature = "imap-backend")]
Imap(ImapConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(NotmuchConfig),
#[cfg(feature = "smtp-sender")]
Smtp(SmtpConfig),
Sendmail(SendmailConfig),
}

View file

@ -1,3 +1,6 @@
pub mod config;
pub(crate) mod wizard;
use anyhow::Result;
use async_trait::async_trait;
use std::ops::Deref;
@ -47,11 +50,9 @@ use serde::{Deserialize, Serialize};
use crate::{account::TomlAccountConfig, Envelopes, IdMapper};
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
#[default]
None,
Maildir,
#[serde(skip_deserializing)]
MaildirForSync,
@ -64,6 +65,24 @@ pub enum BackendKind {
Sendmail,
}
impl ToString for BackendKind {
fn to_string(&self) -> String {
let kind = match self {
Self::Maildir => "Maildir",
Self::MaildirForSync => "Maildir",
#[cfg(feature = "imap-backend")]
Self::Imap => "IMAP",
#[cfg(feature = "notmuch-backend")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp-sender")]
Self::Smtp => "SMTP",
Self::Sendmail => "Sendmail",
};
kind.to_string()
}
}
#[derive(Clone, Default)]
pub struct BackendContextBuilder {
maildir: Option<MaildirSessionBuilder>,

71
src/backend/wizard.rs Normal file
View file

@ -0,0 +1,71 @@
use anyhow::Result;
use dialoguer::Select;
#[cfg(feature = "imap-backend")]
use crate::imap;
#[cfg(feature = "notmuch-backend")]
use crate::notmuch;
#[cfg(feature = "smtp-sender")]
use crate::smtp;
use crate::{config::wizard::THEME, maildir, sendmail};
use super::{config::BackendConfig, BackendKind};
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Maildir,
#[cfg(feature = "imap-backend")]
BackendKind::Imap,
#[cfg(feature = "notmuch-backend")]
BackendKind::Notmuch,
];
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Sendmail,
#[cfg(feature = "smtp-sender")]
BackendKind::Smtp,
];
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(DEFAULT_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
let config = match kind {
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
#[cfg(feature = "imap-backend")]
Some(kind) if kind == BackendKind::Imap => {
Some(imap::wizard::configure(account_name, email).await?)
}
#[cfg(feature = "notmuch-backend")]
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
_ => None,
};
Ok(config)
}
pub(crate) async fn configure_sender(
account_name: &str,
email: &str,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(SEND_MESSAGE_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));
let config = match kind {
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
#[cfg(feature = "smtp-sender")]
Some(kind) if kind == BackendKind::Smtp => {
Some(smtp::wizard::configure(account_name, email).await?)
}
_ => None,
};
Ok(config)
}

View file

@ -11,9 +11,13 @@ use email::{
config::Config,
email::{EmailHooks, EmailTextPlainFormat},
};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf, process::exit};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
process,
};
use toml;
use crate::{
@ -52,51 +56,95 @@ pub struct TomlConfig {
}
impl TomlConfig {
/// Tries to create a config from an optional path.
pub async fn from_maybe_path(path: Option<&str>) -> Result<Self> {
debug!("path: {:?}", path);
let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) {
let content = fs::read_to_string(path).context("cannot read config file")?;
toml::from_str(&content).context("cannot parse config file")?
} else {
wizard_warn!("Himalaya could not find an already existing configuration file.");
if !Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default()
{
exit(0);
}
wizard::configure().await?
};
if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account"));
}
trace!("config: {:#?}", config);
Ok(config)
/// Read and parse the TOML configuration at the given path.
///
/// Returns an error if the configuration file cannot be read or
/// if its content cannot be parsed.
fn from_path(path: &Path) -> Result<Self> {
let content =
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
}
/// Tries to return a config path from a few default settings.
/// Create and save a TOML configuration using the wizard.
///
/// If the user accepts the confirmation, the wizard starts and
/// help him to create his configuration file. Otherwise the
/// program stops.
///
/// NOTE: the wizard can only be used with interactive shells.
async fn from_wizard(path: PathBuf) -> Result<Self> {
wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create it with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default();
if !confirm {
process::exit(0);
}
wizard::configure().await
}
/// Read and parse the TOML configuration from default paths.
pub async fn from_default_paths() -> Result<Self> {
match Self::first_valid_default_path() {
Some(path) => Self::from_path(&path),
None => Self::from_wizard(Self::default_path()?).await,
}
}
/// Read and parse the TOML configuration at the optional given
/// path.
///
/// If the given path exists, then read and parse the TOML
/// configuration from it.
///
/// If the given path does not exist, then create it using the
/// wizard.
///
/// If no path is given, then either read and parse the TOML
/// configuration at the first valid default path, otherwise
/// create it using the wizard. wizard.
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
match path.map(Into::into) {
Some(ref path) if path.exists() => Self::from_path(path),
Some(path) => Self::from_wizard(path).await,
None => match Self::first_valid_default_path() {
Some(path) => Self::from_path(&path),
None => Self::from_wizard(Self::default_path()?).await,
},
}
}
/// Get the default configuration path.
///
/// Returns an error if the XDG configuration directory cannot be
/// found.
pub fn default_path() -> Result<PathBuf> {
Ok(config_dir()
.ok_or(anyhow!("cannot get XDG config directory"))?
.join("himalaya")
.join("config.toml"))
}
/// Get the first default configuration path that points to a
/// valid file.
///
/// Tries paths in this order:
///
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
/// OSes.)
/// - `"$HOME/.config/himalaya/config.toml"`
/// - `"$HOME/.himalayarc"`
///
/// Returns `Some(path)` if the path exists, otherwise `None`.
pub fn path() -> Option<PathBuf> {
config_dir()
.map(|p| p.join("himalaya").join("config.toml"))
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
/// `$XDG_CONFIG_DIR` in other OSes.)
/// - `$HOME/.config/himalaya/config.toml`
/// - `$HOME/.himalayarc`
pub fn first_valid_default_path() -> Option<PathBuf> {
Self::default_path()
.ok()
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
.filter(|p| p.exists())
@ -104,6 +152,7 @@ impl TomlConfig {
.filter(|p| p.exists())
}
/// Build account configurations from a given account name.
pub fn into_account_configs(
self,
account_name: Option<&str>,
@ -232,7 +281,7 @@ mod tests {
async fn make_config(config: &str) -> Result<TomlConfig> {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", config).unwrap();
TomlConfig::from_maybe_path(file.into_temp_path().to_str()).await
TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await
}
#[tokio::test]

View file

@ -57,7 +57,10 @@ pub(crate) async fn configure() -> Result<TomlConfig> {
// accounts are setup, decide which will be the default. If no
// accounts are setup, exit the process.
let default_account = match config.accounts.len() {
0 => process::exit(0),
0 => {
wizard_warn!("No account configured, exiting.");
process::exit(0);
}
1 => Some(config.accounts.values_mut().next().unwrap()),
_ => {
let accounts = config.accounts.clone();

View file

@ -1,8 +1,12 @@
use anyhow::{anyhow, Result};
use anyhow::{bail, Result};
use dialoguer::Input;
use email_address::EmailAddress;
use crate::config::wizard::THEME;
use crate::{
backend::{self, config::BackendConfig, BackendKind},
config::wizard::THEME,
message::config::{MessageConfig, MessageSendConfig},
};
use super::TomlAccountConfig;
@ -20,7 +24,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
if EmailAddress::is_valid(email) {
Ok(())
} else {
Err(anyhow!("Invalid email address: {email}"))
bail!("Invalid email address: {email}")
}
})
.interact()?;
@ -31,9 +35,48 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
.interact()?,
);
// config.backend = backend::wizard::configure(&account_name, &config.email).await?;
match backend::wizard::configure(&account_name, &config.email).await? {
Some(BackendConfig::Maildir(mdir_config)) => {
config.maildir = Some(mdir_config);
config.backend = Some(BackendKind::Maildir);
}
#[cfg(feature = "imap-backend")]
Some(BackendConfig::Imap(imap_config)) => {
config.imap = Some(imap_config);
config.backend = Some(BackendKind::Imap);
}
#[cfg(feature = "notmuch-backend")]
Some(BackendConfig::Notmuch(notmuch_config)) => {
config.notmuch = Some(notmuch_config);
config.backend = Some(BackendKind::Notmuch);
}
_ => (),
};
// config.sender = sender::wizard::configure(&account_name, &config.email).await?;
match backend::wizard::configure_sender(&account_name, &config.email).await? {
Some(BackendConfig::Sendmail(sendmail_config)) => {
config.sendmail = Some(sendmail_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Sendmail),
..Default::default()
}),
..Default::default()
});
}
#[cfg(feature = "smtp-sender")]
Some(BackendConfig::Smtp(smtp_config)) => {
config.smtp = Some(smtp_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Smtp),
..Default::default()
}),
..Default::default()
});
}
_ => (),
};
Ok(Some((account_name, config)))
}

View file

@ -1,3 +0,0 @@
pub mod args;
// pub mod handlers;
// pub(crate) mod wizard;

View file

@ -1 +0,0 @@
// pub(crate) mod wizard;

View file

@ -1,6 +0,0 @@
#[cfg(feature = "imap-backend")]
pub mod imap;
pub mod maildir;
#[cfg(feature = "notmuch-backend")]
pub mod notmuch;
// pub(crate) mod wizard;

View file

@ -1,44 +0,0 @@
use anyhow::Result;
use dialoguer::Select;
use email::backend::BackendConfig;
use crate::config::wizard::THEME;
#[cfg(feature = "imap-backend")]
use super::imap;
use super::maildir;
#[cfg(feature = "notmuch-backend")]
use super::notmuch;
#[cfg(feature = "imap-backend")]
const IMAP: &str = "IMAP";
const MAILDIR: &str = "Maildir";
#[cfg(feature = "notmuch-backend")]
const NOTMUCH: &str = "Notmuch";
const NONE: &str = "None";
const BACKENDS: &[&str] = &[
#[cfg(feature = "imap-backend")]
IMAP,
MAILDIR,
#[cfg(feature = "notmuch-backend")]
NOTMUCH,
NONE,
];
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
let backend = Select::with_theme(&*THEME)
.with_prompt("Email backend")
.items(BACKENDS)
.default(0)
.interact_opt()?;
match backend {
#[cfg(feature = "imap-backend")]
Some(idx) if BACKENDS[idx] == IMAP => imap::wizard::configure(account_name, email).await,
Some(idx) if BACKENDS[idx] == MAILDIR => maildir::wizard::configure(),
#[cfg(feature = "notmuch-backend")]
Some(idx) if BACKENDS[idx] == NOTMUCH => notmuch::wizard::configure(),
_ => Ok(BackendConfig::None),
}
}

View file

@ -1,14 +1,11 @@
pub mod account;
pub mod backend;
pub mod email;
pub mod envelope;
pub mod flag;
pub mod folder;
pub mod sender;
pub mod tpl;
pub use self::account::{args, handlers, Account, Accounts};
pub use self::backend::*;
pub use self::email::*;
pub use self::envelope::*;
pub use self::flag::*;

View file

@ -1,4 +0,0 @@
pub mod sendmail;
#[cfg(feature = "smtp-sender")]
pub mod smtp;
// pub(crate) mod wizard;

View file

@ -1 +0,0 @@
// pub(crate) mod wizard;

View file

@ -1 +0,0 @@
// pub(crate) mod wizard;

View file

@ -1,35 +0,0 @@
use anyhow::Result;
use dialoguer::Select;
use crate::config::wizard::THEME;
use super::sendmail;
#[cfg(feature = "smtp-sender")]
use super::smtp;
#[cfg(feature = "smtp-sender")]
const SMTP: &str = "SMTP";
const SENDMAIL: &str = "Sendmail";
const NONE: &str = "None";
const SENDERS: &[&str] = &[
#[cfg(feature = "smtp-sender")]
SMTP,
SENDMAIL,
NONE,
];
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
let sender = Select::with_theme(&*THEME)
.with_prompt("Email sender")
.items(SENDERS)
.default(0)
.interact_opt()?;
match sender {
#[cfg(feature = "smtp-sender")]
Some(n) if SENDERS[n] == SMTP => smtp::wizard::configure(account_name, email).await,
Some(n) if SENDERS[n] == SENDMAIL => sendmail::wizard::configure(),
_ => Ok(SenderConfig::None),
}
}

3
src/imap/mod.rs Normal file
View file

@ -0,0 +1,3 @@
// pub mod args;
// pub mod handlers;
pub(crate) mod wizard;

View file

@ -2,12 +2,13 @@ use anyhow::Result;
use dialoguer::{Confirm, Input, Password, Select};
use email::{
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
backend::{BackendConfig, ImapAuthConfig, ImapConfig},
imap::{ImapAuthConfig, ImapConfig},
};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{
backend::config::BackendConfig,
config::wizard::{prompt_passwd, THEME},
wizard_log, wizard_prompt,
};

View file

@ -3,9 +3,17 @@ pub mod cache;
pub mod compl;
pub mod config;
pub mod domain;
#[cfg(feature = "imap-backend")]
pub mod imap;
pub mod maildir;
pub mod man;
#[cfg(feature = "notmuch-backend")]
pub mod notmuch;
pub mod output;
pub mod printer;
pub mod sendmail;
#[cfg(feature = "smtp-sender")]
pub mod smtp;
pub mod ui;
pub use cache::IdMapper;

View file

@ -1,9 +1,9 @@
use anyhow::Result;
use dialoguer::Input;
use dirs::home_dir;
use email::backend::{BackendConfig, MaildirConfig};
use email::maildir::MaildirConfig;
use crate::config::wizard::THEME;
use crate::{backend::config::BackendConfig, config::wizard::THEME};
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = MaildirConfig::default();

View file

@ -5,8 +5,6 @@ use log::{debug, warn};
use std::env;
use url::Url;
#[cfg(feature = "imap-backend")]
use himalaya::imap;
use himalaya::{
account,
backend::{Backend, BackendBuilder},
@ -18,7 +16,7 @@ use himalaya::{
};
fn create_app() -> Command {
let app = Command::new(env!("CARGO_PKG_NAME"))
Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
@ -33,12 +31,7 @@ fn create_app() -> Command {
.subcommand(man::args::subcmd())
.subcommand(account::args::subcmd())
.subcommand(folder::args::subcmd())
.subcommands(email::args::subcmds());
#[cfg(feature = "imap-backend")]
let app = app.subcommands(imap::args::subcmds());
app
.subcommands(email::args::subcmds())
}
#[allow(clippy::single_match)]
@ -57,7 +50,7 @@ async fn main() -> Result<()> {
let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let url = Url::parse(&raw_args[1])?;
let (toml_account_config, account_config) = TomlConfig::from_maybe_path(None)
let (toml_account_config, account_config) = TomlConfig::from_default_paths()
.await?
.into_account_configs(None, false)?;
let backend_builder =
@ -88,12 +81,12 @@ async fn main() -> Result<()> {
_ => (),
}
let maybe_config_path = config::args::parse_arg(&m);
let maybe_account_name = account::args::parse_arg(&m);
let some_config_path = config::args::parse_arg(&m);
let some_account_name = account::args::parse_arg(&m);
let disable_cache = cache::args::parse_disable_cache_flag(&m);
let folder = folder::args::parse_source_arg(&m);
let toml_config = TomlConfig::from_maybe_path(maybe_config_path).await?;
let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
let mut printer = StdoutPrinter::try_from(&m)?;
@ -122,13 +115,13 @@ async fn main() -> Result<()> {
Some(account::args::Cmd::List(max_width)) => {
let (_, account_config) = toml_config
.clone()
.into_account_configs(maybe_account_name, disable_cache)?;
.into_account_configs(some_account_name, disable_cache)?;
return account::handlers::list(max_width, &account_config, &toml_config, &mut printer);
}
Some(account::args::Cmd::Sync(strategy, dry_run)) => {
let (toml_account_config, account_config) = toml_config
.clone()
.into_account_configs(maybe_account_name, true)?;
.into_account_configs(some_account_name, true)?;
let backend_builder =
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
@ -140,7 +133,7 @@ async fn main() -> Result<()> {
Some(account::args::Cmd::Configure(reset)) => {
let (_, account_config) = toml_config
.clone()
.into_account_configs(maybe_account_name, disable_cache)?;
.into_account_configs(some_account_name, disable_cache)?;
return account::handlers::configure(&account_config, reset).await;
}
_ => (),
@ -148,7 +141,7 @@ async fn main() -> Result<()> {
let (toml_account_config, account_config) = toml_config
.clone()
.into_account_configs(maybe_account_name, disable_cache)?;
.into_account_configs(some_account_name, disable_cache)?;
// checks folder commands
match folder::args::matches(&m)? {

1
src/notmuch/mod.rs Normal file
View file

@ -0,0 +1 @@
pub(crate) mod wizard;

1
src/sendmail/mod.rs Normal file
View file

@ -0,0 +1 @@
pub(crate) mod wizard;

View file

@ -1,10 +1,10 @@
use anyhow::Result;
use dialoguer::Input;
use email::sender::{SenderConfig, SendmailConfig};
use email::sendmail::SendmailConfig;
use crate::config::wizard::THEME;
use crate::{backend::config::BackendConfig, config::wizard::THEME};
pub(crate) fn configure() -> Result<SenderConfig> {
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = SendmailConfig::default();
config.cmd = Input::with_theme(&*THEME)
@ -13,5 +13,5 @@ pub(crate) fn configure() -> Result<SenderConfig> {
.interact()?
.into();
Ok(SenderConfig::Sendmail(config))
Ok(BackendConfig::Sendmail(config))
}

1
src/smtp/mod.rs Normal file
View file

@ -0,0 +1 @@
pub(crate) mod wizard;

View file

@ -2,12 +2,13 @@ use anyhow::Result;
use dialoguer::{Confirm, Input, Select};
use email::{
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
sender::{SenderConfig, SmtpAuthConfig, SmtpConfig},
smtp::{SmtpAuthConfig, SmtpConfig},
};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{
backend::config::BackendConfig,
config::wizard::{prompt_passwd, THEME},
wizard_log, wizard_prompt,
};
@ -30,7 +31,7 @@ const KEYRING: &str = "Ask the password, then save it in my system's global keyr
const RAW: &str = "Ask the password, then save it in the configuration file (not safe)";
const CMD: &str = "Use a shell command that exposes the password";
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
let mut config = SmtpConfig::default();
config.host = Input::with_theme(&*THEME)
@ -218,5 +219,5 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<SenderC
_ => SmtpAuthConfig::default(),
};
Ok(SenderConfig::Smtp(config))
Ok(BackendConfig::Smtp(config))
}