Merge branch 'v1'

This commit is contained in:
Clément DOUIN 2024-08-16 14:29:46 +02:00
commit d85bc1e8ae
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
52 changed files with 1207 additions and 2761 deletions

1714
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
[package]
name = "himalaya"
description = "CLI to manage emails"
version = "1.0.0-beta.4"
version = "1.0.0"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021"
license = "MIT"
categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "smtp", "sync"]
categories = ["command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "maildir", "smtp"]
homepage = "https://pimalaya.org/"
documentation = "https://pimalaya.org/himalaya/cli/latest/"
repository = "https://github.com/soywod/himalaya/"
@ -23,8 +23,9 @@ default = [
"smtp",
"sendmail",
"account-discovery",
"account-sync",
"wizard",
# "keyring",
# "oauth2",
# "pgp-commands",
# "pgp-gpg",
@ -37,8 +38,9 @@ notmuch = ["email-lib/notmuch"]
smtp = ["email-lib/smtp"]
sendmail = ["email-lib/sendmail"]
account-discovery = ["email-lib/account-discovery"]
account-sync = ["email-lib/account-sync", "maildir"]
keyring = ["email-lib/keyring", "secret-lib?/keyring-tokio"]
oauth2 = ["dep:oauth-lib", "email-lib/oauth2"]
wizard = ["dep:secret-lib", "email-lib/autoconfig"]
pgp = []
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
@ -48,7 +50,7 @@ pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pgp"]
[dependencies]
ariadne = "0.2"
async-trait = "0.1"
clap = { version = "4.4", features = ["derive", "wrap_help", "env"] }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6.3"
@ -56,19 +58,19 @@ comfy-table = "7.1.1"
console = "0.15.2"
crossterm = "0.27"
dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] }
email_address = "0.2.4"
erased-serde = "0.3"
indicatif = "0.17"
inquire = "0.7.4"
mail-builder = "0.3"
md5 = "0.7"
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
oauth-lib = "=0.1.1"
mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"] }
oauth-lib = { version = "=0.1.1", optional = true }
once_cell = "1.16"
petgraph = "0.6"
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.4", features = ["derive"] }
secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true }
serde = { version = "1", features = ["derive"] }
serde-toml-merge = "0.3"
serde_json = "1"
@ -86,8 +88,3 @@ url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
# WIP: transition from `imap` to `imap-{types,codec,client}`
email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" }
imap-client = { git = "https://github.com/soywod/imap-client.git" }
imap-codec = { git = "https://github.com/duesee/imap-codec.git" }
imap-types = { git = "https://github.com/duesee/imap-codec.git" }

View file

@ -26,11 +26,7 @@ impl AccountCheckUpCommand {
printer.log("Checking configuration integrity…")?;
let (toml_account_config, account_config) = config.clone().into_account_configs(
account,
#[cfg(feature = "account-sync")]
true,
)?;
let (toml_account_config, account_config) = config.clone().into_account_configs(account)?;
let used_backends = toml_account_config.get_used_backends();
printer.log("Checking backend context integrity…")?;
@ -76,7 +72,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "notmuch")]
{
printer.print("Checking Notmuch integrity…")?;
printer.log("Checking Notmuch integrity…")?;
let notmuch = ctx_builder
.notmuch

View file

@ -42,6 +42,7 @@ impl AccountConfigureCommand {
if let Some(ref config) = account_config.imap {
let reset = match &config.auth {
ImapAuthConfig::Passwd(config) => config.reset().await,
#[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => config.reset().await,
};
if let Err(err) = reset {
@ -54,6 +55,7 @@ impl AccountConfigureCommand {
if let Some(ref config) = account_config.smtp {
let reset = match &config.auth {
SmtpAuthConfig::Passwd(config) => config.reset().await,
#[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => config.reset().await,
};
if let Err(err) = reset {
@ -74,6 +76,7 @@ impl AccountConfigureCommand {
ImapAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("IMAP password")).await
}
#[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("IMAP OAuth 2.0 client secret"))
@ -88,6 +91,7 @@ impl AccountConfigureCommand {
SmtpAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("SMTP password")).await
}
#[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("SMTP OAuth 2.0 client secret"))

View file

@ -1,16 +1,12 @@
mod check_up;
mod configure;
mod list;
#[cfg(feature = "account-sync")]
mod sync;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use crate::{config::TomlConfig, printer::Printer};
#[cfg(feature = "account-sync")]
use self::sync::AccountSyncCommand;
use self::{
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
};
@ -30,10 +26,6 @@ pub enum AccountSubcommand {
#[command(alias = "lst")]
List(AccountListCommand),
#[cfg(feature = "account-sync")]
#[command(alias = "synchronize", alias = "synchronise")]
Sync(AccountSyncCommand),
}
impl AccountSubcommand {
@ -43,8 +35,6 @@ impl AccountSubcommand {
Self::CheckUp(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, config).await,
#[cfg(feature = "account-sync")]
Self::Sync(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

@ -1,268 +0,0 @@
use clap::{ArgAction, Parser};
use color_eyre::{eyre::bail, eyre::eyre, Result};
use email::backend::context::BackendContextBuilder;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
use email::{
account::sync::AccountSyncBuilder,
backend::BackendBuilder,
folder::sync::config::FolderSyncStrategy,
sync::{hash::SyncHash, SyncEvent},
};
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
use once_cell::sync::Lazy;
use std::{
collections::{BTreeSet, HashMap},
sync::{Arc, Mutex},
};
use tracing::info;
use crate::{
account::arg::name::OptionalAccountNameArg, backend::BackendKind, config::TomlConfig,
printer::Printer,
};
static MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
});
static SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
)
.unwrap()
});
static SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
});
/// Synchronize an account.
///
/// This command allows you to synchronize all folders and emails
/// (including envelopes and messages) of a given account into a local
/// Maildir folder.
#[derive(Debug, Parser)]
pub struct AccountSyncCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
/// Run the synchronization without applying any changes.
///
/// Instead, a report will be printed to stdout containing all the
/// changes the synchronization plan to do.
#[arg(long, short)]
pub dry_run: bool,
/// Synchronize only specific folders.
///
/// Only the given folders will be synchronized (including
/// associated envelopes and messages). Useful when you need to
/// speed up the synchronization process. A good usecase is to
/// synchronize only the INBOX in order to quickly check for new
/// messages.
#[arg(long, short = 'f')]
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
#[arg(conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
pub include_folder: Vec<String>,
/// Omit specific folders from the synchronization.
///
/// The given folders will be excluded from the synchronization
/// (including associated envelopes and messages). Useful when you
/// have heavy folders that you do not want to take care of, or to
/// speed up the synchronization process.
#[arg(long, short = 'x')]
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
#[arg(conflicts_with = "include_folder", conflicts_with = "all_folders")]
pub exclude_folder: Vec<String>,
/// Synchronize all exsting folders.
#[arg(long, short = 'A')]
#[arg(conflicts_with = "include_folder", conflicts_with = "exclude_folder")]
pub all_folders: bool,
}
impl AccountSyncCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing sync account command");
let account = self.account.name.as_deref();
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, true)?;
let account_name = account_config.name.as_str();
match toml_account_config.sync_kind() {
Some(BackendKind::Imap) | Some(BackendKind::ImapCache) => {
let imap_config = toml_account_config
.imap
.as_ref()
.map(Clone::clone)
.map(Arc::new)
.ok_or_else(|| eyre!("imap config not found"))?;
let imap_ctx = ImapContextBuilder::new(account_config.clone(), imap_config)
.with_prebuilt_credentials()
.await?;
let imap = BackendBuilder::new(account_config.clone(), imap_ctx);
self.sync(printer, account_name, imap).await
}
Some(backend) => bail!("backend {backend:?} not supported for synchronization"),
None => bail!("no backend configured for synchronization"),
}
}
async fn sync(
self,
printer: &mut impl Printer,
account_name: &str,
right: BackendBuilder<impl BackendContextBuilder + SyncHash + 'static>,
) -> Result<()> {
let included_folders = BTreeSet::from_iter(self.include_folder);
let excluded_folders = BTreeSet::from_iter(self.exclude_folder);
let folder_filters = if !included_folders.is_empty() {
Some(FolderSyncStrategy::Include(included_folders))
} else if !excluded_folders.is_empty() {
Some(FolderSyncStrategy::Exclude(excluded_folders))
} else if self.all_folders {
Some(FolderSyncStrategy::All)
} else {
None
};
let sync_builder =
AccountSyncBuilder::try_new(right)?.with_some_folder_filters(folder_filters);
if self.dry_run {
let report = sync_builder.with_dry_run(true).sync().await?;
let mut hunks_count = report.folder.patch.len();
if !report.folder.patch.is_empty() {
printer.log("Folders patch:")?;
for (hunk, _) in report.folder.patch {
printer.log(format!(" - {hunk}"))?;
}
printer.log("")?;
}
if !report.email.patch.is_empty() {
printer.log("Envelopes patch:")?;
for (hunk, _) in report.email.patch {
hunks_count += 1;
printer.log(format!(" - {hunk}"))?;
}
printer.log("")?;
}
printer.out(format!(
"Estimated patch length for account {account_name} to be synchronized: {hunks_count}"
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.out(format!("Account {account_name} successfully synchronized!"))?;
} else {
let multi = MultiProgress::new();
let sub_progresses = Mutex::new(HashMap::new());
let main_progress = multi.add(
ProgressBar::new(100)
.with_style(MAIN_PROGRESS_STYLE.clone())
.with_message("Listing folders…"),
);
main_progress.tick();
let report = sync_builder
.with_handler(move |evt| {
match evt {
SyncEvent::ListedAllFolders => {
main_progress.set_message("Synchronizing folders…");
}
SyncEvent::ProcessedAllFolderHunks => {
main_progress.set_message("Listing envelopes…");
}
SyncEvent::GeneratedEmailPatch(patches) => {
let patches_len = patches.values().flatten().count();
main_progress.set_length(patches_len as u64);
main_progress.set_position(0);
main_progress.set_message("Synchronizing emails…");
let mut envelopes_progresses = sub_progresses.lock().unwrap();
for (folder, patch) in patches {
let progress = ProgressBar::new(patch.len() as u64)
.with_style(SUB_PROGRESS_STYLE.clone())
.with_prefix(folder.clone())
.with_finish(ProgressFinish::AndClear);
let progress = multi.add(progress);
envelopes_progresses.insert(folder, progress.clone());
}
}
SyncEvent::ProcessedEmailHunk(hunk) => {
main_progress.inc(1);
let mut progresses = sub_progresses.lock().unwrap();
if let Some(progress) = progresses.get_mut(hunk.folder()) {
progress.inc(1);
if progress.position() == (progress.length().unwrap() - 1) {
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
} else {
progress.set_message(format!("{hunk}"));
}
}
}
SyncEvent::ProcessedAllEmailHunks => {
let mut progresses = sub_progresses.lock().unwrap();
for progress in progresses.values() {
progress.finish_and_clear()
}
progresses.clear();
main_progress.set_length(100);
main_progress.set_position(100);
main_progress.set_message("Expunging folders…");
}
SyncEvent::ExpungedAllFolders => {
main_progress.finish_and_clear();
}
_ => {
main_progress.tick();
}
};
async { Ok(()) }
})
.sync()
.await?;
let folders_patch_err = report
.folder
.patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !folders_patch_err.is_empty() {
printer.log("")?;
printer.log("Errors occurred while applying the folders patch:")?;
folders_patch_err
.iter()
.try_for_each(|(hunk, err)| printer.log(format!(" - {hunk}: {err}")))?;
}
let envelopes_patch_err = report
.email
.patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() {
printer.log("")?;
printer.log("Errors occurred while applying the envelopes patch:")?;
for (hunk, err) in envelopes_patch_err {
printer.log(format!(" - {hunk}: {err}"))?;
}
}
printer.out(format!("Account {account_name} successfully synchronized!"))?;
}
Ok(())
}
}

View file

@ -24,25 +24,6 @@ use crate::{
folder::config::FolderConfig, message::config::MessageConfig,
};
#[cfg(feature = "account-sync")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SyncConfig {
pub backend: Option<BackendKind>,
pub enable: Option<bool>,
pub dir: Option<PathBuf>,
}
impl From<SyncConfig> for email::account::sync::config::SyncConfig {
fn from(config: SyncConfig) -> Self {
Self {
enable: config.enable,
dir: config.dir,
..Default::default()
}
}
}
/// Represents all existing kind of account config.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@ -55,8 +36,6 @@ pub struct TomlAccountConfig {
pub downloads_dir: Option<PathBuf>,
pub backend: Option<BackendKind>,
#[cfg(feature = "account-sync")]
pub sync: Option<SyncConfig>,
#[cfg(feature = "pgp")]
pub pgp: Option<PgpConfig>,
@ -79,13 +58,6 @@ pub struct TomlAccountConfig {
}
impl TomlAccountConfig {
pub fn sync_kind(&self) -> Option<&BackendKind> {
self.sync
.as_ref()
.and_then(|sync| sync.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn add_folder_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
@ -150,14 +122,6 @@ impl TomlAccountConfig {
.or(self.backend.as_ref())
}
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.watch.as_ref())
.and_then(|watch| watch.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn add_flags_kind(&self) -> Option<&BackendKind> {
self.flag
.as_ref()

View file

@ -1,6 +1,7 @@
pub mod arg;
pub mod command;
pub mod config;
#[cfg(feature = "wizard")]
pub(crate) mod wizard;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};

View file

@ -1,12 +1,8 @@
#[cfg(feature = "account-sync")]
use crate::account::config::SyncConfig;
use color_eyre::{eyre::OptionExt, Result};
#[cfg(feature = "account-sync")]
use email_address::EmailAddress;
use inquire::validator::{ErrorMessage, Validation};
use std::{path::PathBuf, str::FromStr};
#[cfg(feature = "account-discovery")]
use crate::wizard_warn;
use crate::{
backend::{self, config::BackendConfig, BackendKind},
@ -34,14 +30,11 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
let addr = EmailAddress::from_str(&config.email).unwrap();
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
let autoconfig_email = config.email.to_owned();
#[cfg(feature = "account-discovery")]
let autoconfig = tokio::spawn(async move {
email::account::discover::from_addr(&autoconfig_email)
.await
.ok()
});
#[cfg(feature = "wizard")]
let autoconfig =
tokio::spawn(async move { email::autoconfig::from_addr(&autoconfig_email).await.ok() });
let account_name = inquire::Text::new("Account name: ")
.with_default(
@ -65,12 +58,12 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
));
let email = &config.email;
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
let autoconfig = autoconfig.await?;
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
let autoconfig = autoconfig.as_ref();
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
if let Some(config) = autoconfig {
if config.is_gmail() {
println!();
@ -83,7 +76,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
match backend::wizard::configure(
&account_name,
email,
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
autoconfig,
)
.await?
@ -109,7 +102,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
match backend::wizard::configure_sender(
&account_name,
email,
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
autoconfig,
)
.await?
@ -139,21 +132,5 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
_ => (),
};
#[cfg(feature = "account-sync")]
{
let should_configure_sync =
inquire::Confirm::new("Do you need offline access for your account?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
if should_configure_sync {
config.sync = Some(SyncConfig {
enable: Some(true),
..Default::default()
});
}
}
Ok(Some((account_name, config)))
}

View file

@ -1,9 +1,12 @@
pub mod config;
#[cfg(feature = "wizard")]
pub(crate) mod wizard;
use async_trait::async_trait;
use color_eyre::Result;
use std::{fmt::Display, ops::Deref, sync::Arc};
use tracing::instrument;
#[cfg(feature = "imap")]
use email::imap::{ImapContextBuilder, ImapContextSync};
@ -24,7 +27,6 @@ use email::{
get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions},
thread::ThreadEnvelopes,
watch::WatchEnvelopes,
Id, SingleId,
},
flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags},
@ -59,8 +61,6 @@ pub enum BackendKind {
#[cfg(feature = "imap")]
Imap,
#[cfg(all(feature = "imap", feature = "account-sync"))]
ImapCache,
#[cfg(feature = "maildir")]
Maildir,
@ -85,8 +85,6 @@ impl Display for BackendKind {
#[cfg(feature = "imap")]
Self::Imap => "IMAP",
#[cfg(all(feature = "imap", feature = "account-sync"))]
Self::ImapCache => "IMAP cache",
#[cfg(feature = "maildir")]
Self::Maildir => "Maildir",
@ -112,9 +110,6 @@ pub struct BackendContextBuilder {
#[cfg(feature = "imap")]
pub imap: Option<ImapContextBuilder>,
#[cfg(all(feature = "imap", feature = "account-sync"))]
pub imap_cache: Option<MaildirContextBuilder>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextBuilder>,
@ -156,26 +151,6 @@ impl BackendContextBuilder {
}
},
#[cfg(all(feature = "imap", feature = "account-sync"))]
imap_cache: {
let builder = toml_account_config
.imap
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::ImapCache))
.map(Clone::clone)
.map(Arc::new)
.map(|imap_config| {
email::backend::context::BackendContextBuilder::try_to_sync_cache_builder(
&ImapContextBuilder::new(account_config.clone(), imap_config),
&account_config,
)
});
match builder {
Some(builder) => Some(builder?),
None => None,
}
},
#[cfg(feature = "maildir")]
maildir: toml_account_config
.maildir
@ -227,11 +202,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.add_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -244,11 +214,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.list_folders_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_folders_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.list_folders()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_folders_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -261,11 +226,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.expunge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.expunge_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.expunge_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.expunge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -278,11 +238,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.purge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.purge_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.purge_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.purge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -295,11 +250,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.delete_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.delete_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -312,11 +262,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.get_envelope_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_envelope_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.get_envelope()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_envelope_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -329,11 +274,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.list_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.list_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -346,11 +286,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.thread_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.thread_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.thread_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.thread_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -359,32 +294,10 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
}
}
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
match self.toml_account_config.watch_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.watch_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.watch_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.watch_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.watch_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn add_flags(&self) -> Option<BackendFeature<Self::Context, dyn AddFlags>> {
match self.toml_account_config.add_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -397,11 +310,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.set_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.set_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.set_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.set_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -414,11 +322,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.remove_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.remove_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.remove_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.remove_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -431,11 +334,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.add_message_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_message_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_message()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_message_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -458,11 +356,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.peek_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.peek_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.peek_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.peek_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -475,11 +368,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.get_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.get_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -492,11 +380,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.copy_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.copy_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.copy_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.copy_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -509,11 +392,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.move_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.move_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.move_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.move_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -526,11 +404,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
match self.toml_account_config.delete_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.delete_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
@ -547,11 +420,6 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
ctx.imap = Some(imap.build().await?);
}
#[cfg(all(feature = "imap", feature = "account-sync"))]
if let Some(maildir) = self.imap_cache {
ctx.imap_cache = Some(maildir.build().await?);
}
#[cfg(feature = "maildir")]
if let Some(maildir) = self.maildir {
ctx.maildir = Some(maildir.build().await?);
@ -581,9 +449,6 @@ pub struct BackendContext {
#[cfg(feature = "imap")]
pub imap: Option<ImapContextSync>,
#[cfg(all(feature = "imap", feature = "account-sync"))]
pub imap_cache: Option<MaildirContextSync>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextSync>,
@ -663,37 +528,28 @@ impl Backend {
})
}
#[instrument(skip(self))]
fn build_id_mapper(
&self,
folder: &str,
backend_kind: Option<&BackendKind>,
) -> Result<IdMapper> {
#[allow(unused_mut)]
let mut id_mapper = IdMapper::Dummy;
match backend_kind {
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => {
if let Some(_) = &self.toml_account_config.maildir {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
}
#[cfg(feature = "maildir")]
if let Some(BackendKind::Maildir) = backend_kind {
if let Some(_) = &self.toml_account_config.maildir {
return Ok(IdMapper::new(&self.backend.account_config, folder)?);
}
}
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
#[cfg(feature = "notmuch")]
if let Some(BackendKind::Notmuch) = backend_kind {
if let Some(_) = &self.toml_account_config.notmuch {
return Ok(IdMapper::new(&self.backend.account_config, folder)?);
}
}
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => {
if let Some(_) = &self.toml_account_config.notmuch {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
}
}
_ => (),
};
Ok(id_mapper)
Ok(IdMapper::Dummy)
}
pub async fn list_envelopes(
@ -868,11 +724,6 @@ impl Backend {
self.backend.send_message_then_save_copy(msg).await?;
Ok(())
}
pub async fn watch_envelopes(&self, folder: &str) -> Result<()> {
self.backend.watch_envelopes(folder).await?;
Ok(())
}
}
impl Deref for Backend {

View file

@ -1,6 +1,5 @@
use color_eyre::Result;
#[cfg(feature = "account-discovery")]
use email::account::discover::config::AutoConfig;
use email::autoconfig::config::AutoConfig;
use inquire::Select;
#[cfg(feature = "imap")]
@ -35,7 +34,7 @@ const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
pub(crate) async fn configure(
account_name: &str,
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec())
.with_starting_cursor(0)
@ -47,7 +46,7 @@ pub(crate) async fn configure(
imap::wizard::configure(
account_name,
email,
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
autoconfig,
)
.await?,
@ -65,7 +64,7 @@ pub(crate) async fn configure(
pub(crate) async fn configure_sender(
account_name: &str,
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::new(
"Backend for sending messages",
@ -80,7 +79,7 @@ pub(crate) async fn configure_sender(
smtp::wizard::configure(
account_name,
email,
#[cfg(feature = "account-discovery")]
#[cfg(feature = "wizard")]
autoconfig,
)
.await?,

View file

@ -1,3 +1,4 @@
#[cfg(feature = "wizard")]
pub mod wizard;
use color_eyre::{
@ -7,7 +8,7 @@ use color_eyre::{
use dirs::{config_dir, home_dir};
use email::{
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig,
folder::config::FolderConfig, message::config::MessageConfig,
};
use serde::{Deserialize, Serialize};
use serde_toml_merge::merge;
@ -16,9 +17,9 @@ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use toml::{self, Value};
use tracing::debug;
#[cfg(feature = "account-sync")]
use crate::backend::BackendKind;
use crate::{account::config::TomlAccountConfig, wizard_warn};
use crate::account::config::TomlAccountConfig;
#[cfg(feature = "wizard")]
use crate::wizard_warn;
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
@ -84,9 +85,8 @@ impl TomlConfig {
/// program stops.
///
/// NOTE: the wizard can only be used with interactive shells.
#[cfg(feature = "wizard")]
async fn from_wizard(path: &PathBuf) -> Result<Self> {
use std::process;
wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ")
@ -95,10 +95,15 @@ impl TomlConfig {
.unwrap_or_default();
if !confirm {
process::exit(0);
std::process::exit(0);
}
wizard::configure(path).await
return wizard::configure(path).await;
}
#[cfg(not(feature = "wizard"))]
async fn from_wizard(path: &PathBuf) -> Result<Self> {
bail!("Cannot find existing configuration at {path:?}.");
}
/// Read and parse the TOML configuration from default paths.
@ -182,14 +187,14 @@ impl TomlConfig {
.ok_or_else(|| eyre!("cannot find account {name}")),
}?;
#[cfg(feature = "imap")]
#[cfg(all(feature = "imap", feature = "keyring"))]
if let Some(imap_config) = toml_account_config.imap.as_mut() {
imap_config
.auth
.replace_undefined_keyring_entries(&account_name)?;
}
#[cfg(feature = "smtp")]
#[cfg(all(feature = "smtp", feature = "keyring"))]
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
smtp_config
.auth
@ -203,21 +208,8 @@ impl TomlConfig {
pub fn into_account_configs(
self,
account_name: Option<&str>,
#[cfg(feature = "account-sync")] disable_cache: bool,
) -> Result<(Arc<TomlAccountConfig>, Arc<AccountConfig>)> {
#[cfg_attr(not(feature = "account-sync"), allow(unused_mut))]
let (account_name, mut toml_account_config) =
self.into_toml_account_config(account_name)?;
#[cfg(feature = "account-sync")]
if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) {
if !disable_cache {
toml_account_config.backend = match toml_account_config.backend {
Some(BackendKind::Imap) => Some(BackendKind::ImapCache),
backend => backend,
}
}
}
let (account_name, toml_account_config) = self.into_toml_account_config(account_name)?;
let config = Config {
display_name: self.display_name,
@ -239,31 +231,19 @@ impl TomlConfig {
folder: config.folder.map(|c| FolderConfig {
aliases: c.alias,
list: c.list.map(|c| c.remote),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
envelope: config.envelope.map(|c| EnvelopeConfig {
list: c.list.map(|c| c.remote),
thread: c.thread.map(|c| c.remote),
watch: c.watch.map(|c| c.remote),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
flag: config.flag.map(|c| FlagConfig {
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
flag: None,
message: config.message.map(|c| MessageConfig {
read: c.read.map(|c| c.remote),
write: c.write.map(|c| c.remote),
send: c.send.map(|c| c.remote),
delete: c.delete.map(Into::into),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
template: config.template,
#[cfg(feature = "account-sync")]
sync: config.sync.map(Into::into),
#[cfg(feature = "pgp")]
pgp: config.pgp,
},

View file

@ -152,9 +152,6 @@ fn pretty_serialize(config: &TomlConfig) -> Result<String> {
#[cfg(feature = "sendmail")]
set_table_dotted(item, "sendmail");
#[cfg(feature = "account-sync")]
set_table_dotted(item, "sync");
#[cfg(feature = "pgp")]
set_table_dotted(item, "pgp");
})
@ -214,68 +211,6 @@ fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a
// )
// }
// #[cfg(feature = "account-sync")]
// #[test]
// fn pretty_serialize_sync_all() {
// use email::account::sync::config::SyncConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// sync: Some(SyncConfig {
// enable: Some(false),
// dir: Some("/tmp/test".into()),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// sync.enable = false
// sync.dir = "/tmp/test"
// "#,
// );
// }
// #[cfg(feature = "account-sync")]
// #[test]
// fn pretty_serialize_sync_include() {
// use email::{
// account::sync::config::SyncConfig,
// folder::sync::config::{FolderSyncConfig, FolderSyncStrategy},
// };
// use std::collections::BTreeSet;
// use crate::folder::config::FolderConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// sync: Some(SyncConfig {
// enable: Some(true),
// dir: Some("/tmp/test".into()),
// ..Default::default()
// }),
// folder: Some(FolderConfig {
// sync: Some(FolderSyncConfig {
// filter: FolderSyncStrategy::Include(BTreeSet::from_iter(["test".into()])),
// ..Default::default()
// }),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// sync.enable = true
// sync.dir = "/tmp/test"
// folder.sync.filter.include = ["test"]
// folder.sync.permissions.create = true
// folder.sync.permissions.delete = true
// "#,
// );
// }
// #[cfg(feature = "imap")]
// #[test]
// fn pretty_serialize_imap_passwd_cmd() {

View file

@ -8,8 +8,6 @@ use email::{
use std::process::exit;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
@ -37,10 +35,6 @@ pub struct ListEnvelopesCommand {
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
@ -130,8 +124,6 @@ impl Default for ListEnvelopesCommand {
folder: Default::default(),
page: 1,
page_size: Default::default(),
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
@ -143,11 +135,9 @@ impl ListEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list envelopes command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let folder = &self.folder.name;
let page = 1.max(self.page) - 1;

View file

@ -1,15 +1,12 @@
pub mod list;
pub mod thread;
pub mod watch;
use clap::Subcommand;
use color_eyre::Result;
use crate::{config::TomlConfig, printer::Printer};
use self::{
list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand, watch::WatchEnvelopesCommand,
};
use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand};
/// Manage envelopes.
///
@ -24,9 +21,6 @@ pub enum EnvelopeSubcommand {
#[command()]
Thread(ThreadEnvelopesCommand),
#[command()]
Watch(WatchEnvelopesCommand),
}
impl EnvelopeSubcommand {
@ -35,7 +29,6 @@ impl EnvelopeSubcommand {
match self {
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Watch(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

@ -8,8 +8,6 @@ use email::{
use std::process::exit;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::EnvelopesTree, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
@ -24,10 +22,6 @@ pub struct ThreadEnvelopesCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
@ -43,11 +37,9 @@ impl ThreadEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let folder = &self.folder.name;
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();

View file

@ -1,57 +0,0 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
};
/// Watch envelopes for changes.
///
/// This command allows you to watch a folder and execute hooks when
/// changes occur on envelopes.
#[derive(Debug, Parser)]
pub struct WatchEnvelopesCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl WatchEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing watch envelopes command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let watch_envelopes_kind = toml_account_config.watch_envelopes_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
watch_envelopes_kind,
|builder| builder.set_watch_envelopes(BackendFeatureSource::Context),
)
.await?;
printer.out(format!(
"Start watching folder {folder} for envelopes changes…"
))?;
backend.watch_envelopes(folder).await
}
}

View file

@ -1,5 +1,3 @@
#[cfg(feature = "account-sync")]
use email::envelope::sync::config::EnvelopeSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@ -9,10 +7,7 @@ use crate::backend::BackendKind;
pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>,
pub thread: Option<ThreadEnvelopesConfig>,
pub watch: Option<WatchEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<EnvelopeSyncConfig>,
}
impl EnvelopeConfig {
@ -23,10 +18,6 @@ impl EnvelopeConfig {
kinds.extend(list.get_used_backends());
}
if let Some(watch) = &self.watch {
kinds.extend(watch.get_used_backends());
}
if let Some(get) = &self.get {
kinds.extend(get.get_used_backends());
}
@ -75,26 +66,6 @@ impl ThreadEnvelopesConfig {
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct WatchEnvelopesConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::envelope::watch::config::WatchEnvelopeConfig,
}
impl WatchEnvelopesConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct GetEnvelopeConfig {
pub backend: Option<BackendKind>,

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -26,10 +24,6 @@ pub struct FlagAddCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl FlagAddCommand {
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_flags_kind = toml_account_config.add_flags_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -26,10 +24,6 @@ pub struct FlagRemoveCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl FlagRemoveCommand {
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let remove_flags_kind = toml_account_config.remove_flags_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -26,10 +24,6 @@ pub struct FlagSetCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl FlagSetCommand {
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let set_flags_kind = toml_account_config.set_flags_kind();

View file

@ -1,5 +1,3 @@
#[cfg(feature = "account-sync")]
use email::flag::sync::config::FlagSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@ -10,8 +8,6 @@ pub struct FlagConfig {
pub add: Option<FlagAddConfig>,
pub set: Option<FlagSetConfig>,
pub remove: Option<FlagRemoveConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<FlagSyncConfig>,
}
impl FlagConfig {

View file

@ -5,8 +5,6 @@ use std::{fs, path::PathBuf};
use tracing::info;
use uuid::Uuid;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
@ -25,10 +23,6 @@ pub struct AttachmentDownloadCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl AttachmentDownloadCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -26,10 +24,6 @@ pub struct MessageCopyCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -42,11 +36,9 @@ impl MessageCopyCommand {
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let copy_messages_kind = toml_account_config.copy_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
@ -25,10 +23,6 @@ pub struct MessageDeleteCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl MessageDeleteCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let delete_messages_kind = toml_account_config.delete_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -36,10 +34,6 @@ pub struct MessageForwardCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -50,11 +44,9 @@ impl MessageForwardCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();

View file

@ -5,8 +5,6 @@ use mail_builder::MessageBuilder;
use tracing::info;
use url::Url;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, printer::Printer,
ui::editor,
@ -24,10 +22,6 @@ pub struct MessageMailtoCommand {
#[arg()]
pub url: Url,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -36,8 +30,6 @@ impl MessageMailtoCommand {
pub fn new(url: &str) -> Result<Self> {
Ok(Self {
url: Url::parse(url)?,
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
})
}
@ -45,11 +37,9 @@ impl MessageMailtoCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing mailto message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag,
@ -27,10 +25,6 @@ pub struct MessageMoveCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -43,11 +37,9 @@ impl MessageMoveCommand {
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let move_messages_kind = toml_account_config.move_messages_kind();

View file

@ -4,8 +4,6 @@ use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
@ -70,10 +68,6 @@ pub struct MessageReadCommand {
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -85,11 +79,9 @@ impl MessageReadCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -39,10 +37,6 @@ pub struct MessageReplyCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -52,11 +46,9 @@ impl MessageReplyCommand {
info!("executing reply message command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();

View file

@ -4,8 +4,6 @@ use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
@ -23,10 +21,6 @@ pub struct MessageSaveCommand {
#[command(flatten)]
pub message: MessageRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -37,11 +31,9 @@ impl MessageSaveCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();

View file

@ -4,8 +4,6 @@ use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
message::arg::MessageRawArg, printer::Printer,
@ -20,10 +18,6 @@ pub struct MessageSendCommand {
#[command(flatten)]
pub message: MessageRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -32,11 +26,9 @@ impl MessageSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config

View file

@ -4,8 +4,6 @@ use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::envelope::arg::ids::EnvelopeIdArg;
#[allow(unused)]
use crate::{
@ -71,10 +69,6 @@ pub struct MessageThreadCommand {
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -86,11 +80,9 @@ impl MessageThreadCommand {
let folder = &self.folder.name;
let id = &self.envelope.id;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, message::Message};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -28,10 +26,6 @@ pub struct MessageWriteCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,11 +34,9 @@ impl MessageWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();

View file

@ -1,6 +1,4 @@
use email::message::delete::config::DeleteMessageStyle;
#[cfg(feature = "account-sync")]
use email::message::sync::config::MessageSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@ -15,8 +13,6 @@ pub struct MessageConfig {
pub copy: Option<MessageCopyConfig>,
pub r#move: Option<MessageMoveConfig>,
pub delete: Option<DeleteMessageConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<MessageSyncConfig>,
}
impl MessageConfig {

View file

@ -3,8 +3,6 @@ use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -34,10 +32,6 @@ pub struct TemplateForwardCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -48,11 +42,9 @@ impl TemplateForwardCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -38,10 +36,6 @@ pub struct TemplateReplyCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -53,11 +47,9 @@ impl TemplateReplyCommand {
let folder = &self.folder.name;
let id = self.envelope.id;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind();

View file

@ -5,8 +5,6 @@ use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag,
@ -27,10 +25,6 @@ pub struct TemplateSaveCommand {
#[command(flatten)]
pub template: TemplateRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -41,11 +35,9 @@ impl TemplateSaveCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();

View file

@ -5,8 +5,6 @@ use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
email::template::arg::TemplateRawArg, printer::Printer,
@ -23,10 +21,6 @@ pub struct TemplateSendCommand {
#[command(flatten)]
pub template: TemplateRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -35,11 +29,9 @@ impl TemplateSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send template command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::message::Message;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
@ -23,10 +21,6 @@ pub struct TemplateWriteCommand {
#[command(flatten)]
pub body: TemplateRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -35,11 +29,9 @@ impl TemplateWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write template command");
let (_, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (_, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let tpl = Message::new_tpl_builder(account_config)
.with_headers(self.headers.raw)

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
@ -19,10 +17,6 @@ pub struct AddFolderCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -32,11 +26,9 @@ impl AddFolderCommand {
info!("executing create folder command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_folder_kind = toml_account_config.add_folder_kind();

View file

@ -5,8 +5,6 @@ use inquire::Confirm;
use std::process;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
@ -21,10 +19,6 @@ pub struct FolderDeleteCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -42,11 +36,9 @@ impl FolderDeleteCommand {
process::exit(0);
};
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let delete_folder_kind = toml_account_config.delete_folder_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
@ -20,10 +18,6 @@ pub struct FolderExpungeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -33,11 +27,9 @@ impl FolderExpungeCommand {
info!("executing expunge folder command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let expunge_folder_kind = toml_account_config.expunge_folder_kind();

View file

@ -3,8 +3,6 @@ use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
@ -18,10 +16,6 @@ use crate::{
/// This command allows you to list all exsting folders.
#[derive(Debug, Parser)]
pub struct FolderListCommand {
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
@ -38,11 +32,9 @@ impl FolderListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list folders command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let list_folders_kind = toml_account_config.list_folders_kind();

View file

@ -4,8 +4,6 @@ use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
use std::process;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
@ -20,10 +18,6 @@ pub struct FolderPurgeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -42,11 +36,9 @@ impl FolderPurgeCommand {
process::exit(0);
};
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let purge_folder_kind = toml_account_config.purge_folder_kind();

View file

@ -1,5 +1,3 @@
#[cfg(feature = "account-sync")]
use email::folder::sync::config::FolderSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
@ -14,8 +12,6 @@ pub struct FolderConfig {
pub expunge: Option<FolderExpungeConfig>,
pub purge: Option<FolderPurgeConfig>,
pub delete: Option<FolderDeleteConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<FolderSyncConfig>,
}
impl FolderConfig {

View file

@ -1 +1,2 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

@ -1,18 +1,20 @@
use color_eyre::Result;
#[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::autoconfig::config::{AutoConfig, SecurityType, ServerType};
#[cfg(feature = "oauth2")]
use email::{
account::config::{
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
passwd::PasswdConfig,
},
account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
autoconfig::config::AuthenticationType,
};
use email::{
account::config::passwd::PasswdConfig,
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
#[cfg(feature = "oauth2")]
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
use crate::{backend::config::BackendConfig, ui::prompt};
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::Tls,
@ -20,11 +22,13 @@ const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::None,
];
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
const SECRETS: &[&str] = &[
#[cfg(feature = "keyring")]
KEYRING,
RAW,
CMD,
];
#[cfg(feature = "keyring")]
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
@ -49,16 +53,14 @@ impl StringValidator for U16Validator {
}
}
#[cfg(feature = "account-discovery")]
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
use inquire::{validator, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
.incoming_servers()
@ -93,7 +95,7 @@ pub(crate) async fn configure(
ImapEncryptionKind::None => 2,
};
let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
let encryption_kind = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(default_encryption_idx)
.prompt_skippable()?;
@ -101,28 +103,28 @@ pub(crate) async fn configure(
.and_then(|s| s.port())
.map(ToOwned::to_owned)
.unwrap_or_else(|| match &autoconfig_encryption {
ImapEncryptionKind::Tls => 993,
ImapEncryptionKind::StartTls => 143,
ImapEncryptionKind::None => 143,
ImapEncryptionKind::Tls => 465,
ImapEncryptionKind::StartTls => 587,
ImapEncryptionKind::None => 25,
});
let (encryption, default_port) = match encryption_idx {
Some(enc_kind)
if &enc_kind
let (encryption, default_port) = match encryption_kind {
Some(idx)
if &idx
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened while selecting the encryption of imap.",
"something impossible happened during finding default match for encryption.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port)
}
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
_ => (Some(ImapEncryptionKind::None), 143),
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 465),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 587),
_ => (Some(ImapEncryptionKind::None), 25),
};
let port = Text::new("IMAP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(validator::MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
@ -141,173 +143,167 @@ pub(crate) async fn configure(
.with_default(&default_login)
.prompt()?;
let default_oauth2_enabled = autoconfig_server
.and_then(|imap| {
imap.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
#[cfg(feature = "oauth2")]
let auth = {
use inquire::{Confirm, Password};
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret").prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("IMAP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => {
Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty())
}
let default_oauth2_enabled = autoconfig_server
.and_then(|imap| {
imap.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
};
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
Ok(confirm)
};
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
config.method = match method_idx {
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("IMAP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
})
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
Ok(confirm)
};
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
}
let mut auth_code_grant = AuthorizationCodeGrant::new()
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
.with_redirect_port(redirect_port)
.build()?;
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};
ImapAuthConfig::Passwd(PasswdConfig(secret))
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token = Secret::try_new_keyring_entry(format!(
"{account_name}-imap-oauth2-refresh-token"
))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
configure_passwd(account_name).await?
}
};
#[cfg(not(feature = "oauth2"))]
let auth = configure_passwd(account_name).await?;
let config = ImapConfig {
host,
port,
@ -320,191 +316,30 @@ pub(crate) async fn configure(
Ok(BackendConfig::Imap(config))
}
#[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{
validator::MinLengthValidator, Confirm, Password, PasswordDisplayMode, Select, Text,
};
pub(crate) async fn configure_passwd(account_name: &str) -> Result<ImapAuthConfig> {
use inquire::{Select, Text};
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
let host = Text::new("IMAP hostname")
.with_default(&default_host)
.prompt()?;
let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let (encryption, default_port) = match encryption_idx {
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
_ => (Some(ImapEncryptionKind::None), 143),
let secret = match secret_idx {
#[cfg(feature = "keyring")]
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};
let port = Text::new("IMAP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned();
let login = Text::new("IMAP login")
.with_default(&default_login)
.prompt()?;
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret")
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL").prompt()?;
config.token_url = Text::new("IMAP OAuth 2.0 token URL").prompt()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};
ImapAuthConfig::Passwd(PasswdConfig(secret))
};
let config = ImapConfig {
host,
port,
encryption,
login,
auth,
watch: None,
};
Ok(BackendConfig::Imap(config))
Ok(ImapAuthConfig::Passwd(PasswdConfig(secret)))
}

View file

@ -1 +1,2 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

@ -1 +1,2 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

@ -1,18 +1,20 @@
use color_eyre::Result;
#[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::autoconfig::config::{AutoConfig, SecurityType, ServerType};
#[cfg(feature = "oauth2")]
use email::{
account::config::{
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
passwd::PasswdConfig,
},
account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
autoconfig::config::AuthenticationType,
};
use email::{
account::config::passwd::PasswdConfig,
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
#[cfg(feature = "oauth2")]
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
use crate::{backend::config::BackendConfig, ui::prompt};
const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
SmtpEncryptionKind::Tls,
@ -20,11 +22,13 @@ const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
SmtpEncryptionKind::None,
];
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
const SECRETS: &[&str] = &[
#[cfg(feature = "keyring")]
KEYRING,
RAW,
CMD,
];
#[cfg(feature = "keyring")]
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
@ -49,16 +53,14 @@ impl StringValidator for U16Validator {
}
}
#[cfg(feature = "account-discovery")]
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator, Confirm, Password, Select, Text};
use inquire::{validator, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
.outgoing_servers()
@ -141,173 +143,167 @@ pub(crate) async fn configure(
.with_default(&default_login)
.prompt()?;
let default_oauth2_enabled = autoconfig_server
.and_then(|smtp| {
smtp.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
#[cfg(feature = "oauth2")]
let auth = {
use inquire::{Confirm, Password};
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("SMTP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
let default_oauth2_enabled = autoconfig_server
.and_then(|smtp| {
smtp.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
};
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
Ok(confirm)
};
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
config.method = match method_idx {
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? {
scopes.push(scope)
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("SMTP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
})
};
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
Ok(confirm)
};
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? {
scopes.push(scope)
}
let mut auth_code_grant = AuthorizationCodeGrant::new()
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
.with_redirect_port(redirect_port)
.build()?;
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
SmtpAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};
SmtpAuthConfig::Passwd(PasswdConfig(secret))
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token = Secret::try_new_keyring_entry(format!(
"{account_name}-smtp-oauth2-refresh-token"
))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
SmtpAuthConfig::OAuth2(config)
} else {
configure_passwd(account_name).await?
}
};
#[cfg(not(feature = "oauth2"))]
let auth = configure_passwd(account_name).await?;
let config = SmtpConfig {
host,
port,
@ -319,188 +315,30 @@ pub(crate) async fn configure(
Ok(BackendConfig::Smtp(config))
}
#[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
pub(crate) async fn configure_passwd(account_name: &str) -> Result<SmtpAuthConfig> {
use inquire::{Select, Text};
let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1);
let host = Text::new("SMTP hostname")
.with_default(&default_host)
.prompt()?;
let encryption_idx = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let (encryption, default_port) = match encryption_idx {
Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
_ => (Some(SmtpEncryptionKind::None), 25),
let secret = match secret_idx {
#[cfg(feature = "keyring")]
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
}
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};
let port = Text::new("SMTP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned();
let login = Text::new("SMTP login")
.with_default(&default_login)
.prompt()?;
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL").prompt()?;
config.token_url = Text::new("SMTP OAuth 2.0 token URL").prompt()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
};
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? {
scopes.push(scope)
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
SmtpAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
}
Some(RAW) => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};
SmtpAuthConfig::Passwd(PasswdConfig(secret))
};
let config = SmtpConfig {
host,
port,
encryption,
login,
auth,
};
Ok(BackendConfig::Smtp(config))
Ok(SmtpAuthConfig::Passwd(PasswdConfig(secret)))
}

View file

@ -14,6 +14,7 @@ pub(crate) fn passwd(prompt: &str) -> io::Result<String> {
})
}
#[cfg(feature = "oauth2")]
pub(crate) fn secret(prompt: &str) -> io::Result<String> {
inquire::Password::new(prompt)
.with_display_mode(inquire::PasswordDisplayMode::Masked)