mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-25 12:30:22 +00:00
reorganize folder and cli structure
This commit is contained in:
parent
8b1a289f4d
commit
7a10a7fc25
49 changed files with 1555 additions and 1658 deletions
|
@ -11,7 +11,7 @@ use crate::{folder, ui::table};
|
|||
const ARG_ACCOUNT: &str = "account";
|
||||
const ARG_DRY_RUN: &str = "dry-run";
|
||||
const ARG_RESET: &str = "reset";
|
||||
const CMD_ACCOUNTS: &str = "accounts";
|
||||
const CMD_ACCOUNT: &str = "account";
|
||||
const CMD_CONFIGURE: &str = "configure";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_SYNC: &str = "sync";
|
||||
|
@ -32,7 +32,7 @@ pub enum Cmd {
|
|||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_SYNC) {
|
||||
info!("sync account subcommand matched");
|
||||
let dry_run = parse_dry_run_arg(m);
|
||||
|
@ -73,8 +73,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
|
||||
/// Represents the account subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ACCOUNTS)
|
||||
.about("Manage accounts")
|
||||
Command::new(CMD_ACCOUNT)
|
||||
.about("Subcommand to manage accounts")
|
||||
.subcommands([
|
||||
Command::new(CMD_LIST)
|
||||
.about("List all accounts from the config file")
|
|
@ -22,11 +22,8 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
backend::BackendKind,
|
||||
config::prelude::*,
|
||||
domain::config::FolderConfig,
|
||||
email::envelope::{config::EnvelopeConfig, flag::config::FlagConfig},
|
||||
message::config::MessageConfig,
|
||||
backend::BackendKind, config::prelude::*, envelope::config::EnvelopeConfig,
|
||||
flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig,
|
||||
};
|
||||
|
||||
/// Represents all existing kind of account config.
|
|
@ -17,13 +17,13 @@ use once_cell::sync::Lazy;
|
|||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
backend::BackendContextBuilder,
|
||||
config::{
|
||||
wizard::{prompt_passwd, prompt_secret},
|
||||
TomlConfig,
|
||||
},
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Accounts,
|
||||
};
|
||||
|
||||
use super::TomlAccountConfig;
|
|
@ -1,19 +1,62 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::{collections::hash_map::Iter, ops::Deref};
|
||||
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
ui::table::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::{Account, TomlAccountConfig};
|
||||
use self::config::TomlAccountConfig;
|
||||
|
||||
/// Represents the printable account.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Account {
|
||||
/// Represents the account name.
|
||||
pub name: String,
|
||||
/// Represents the backend name of the account.
|
||||
pub backend: String,
|
||||
/// Represents the default state of the account.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
backend: backend.into(),
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Account {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKEND").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let default = if self.default { "yes" } else { "" };
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).shrinkable().green())
|
||||
.cell(Cell::new(&self.backend).blue())
|
||||
.cell(Cell::new(default).white())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of printable accounts.
|
||||
#[derive(Debug, Default, Serialize)]
|
|
@ -46,7 +46,7 @@ use email::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{account::TomlAccountConfig, Envelopes, IdMapper};
|
||||
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
|
166
src/cache/id_mapper.rs
vendored
166
src/cache/id_mapper.rs
vendored
|
@ -1,166 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use email::account::config::AccountConfig;
|
||||
use log::{debug, trace};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IdMapper {
|
||||
Dummy,
|
||||
Mapper(String, rusqlite::Connection),
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
|
||||
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
let mut db_parent_dir = dir.as_ref().parent();
|
||||
|
||||
while !db_path.is_file() {
|
||||
match db_parent_dir {
|
||||
Some(dir) => {
|
||||
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
|
||||
db_parent_dir = dir.parent();
|
||||
}
|
||||
None => {
|
||||
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db_path
|
||||
}
|
||||
|
||||
pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result<Self> {
|
||||
let folder = account_config.get_folder_alias(folder)?;
|
||||
let digest = md5::compute(account_config.name.clone() + &folder);
|
||||
let table = format!("id_mapper_{digest:x}");
|
||||
debug!("creating id mapper table {table} at {db_path:?}…");
|
||||
|
||||
let db_path = Self::find_closest_db_path(db_path);
|
||||
let conn = rusqlite::Connection::open(&db_path)
|
||||
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
||||
|
||||
let query = format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
internal_id TEXT UNIQUE
|
||||
)",
|
||||
);
|
||||
trace!("create table query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [])
|
||||
.context("cannot create id mapper table")?;
|
||||
|
||||
Ok(Self::Mapper(table, conn))
|
||||
}
|
||||
|
||||
pub fn create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("creating alias for id {id}…");
|
||||
|
||||
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
|
||||
trace!("insert query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [id])
|
||||
.with_context(|| format!("cannot create id alias for id {id}"))?;
|
||||
|
||||
let alias = conn.last_insert_rowid().to_string();
|
||||
debug!("created alias {alias} for id {id}");
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("getting alias for id {id}…");
|
||||
|
||||
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let aliases: Vec<i64> = stmt
|
||||
.query_map([id], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let alias = match aliases.first() {
|
||||
Some(alias) => {
|
||||
debug!("found alias {alias} for id {id}");
|
||||
alias.to_string()
|
||||
}
|
||||
None => {
|
||||
debug!("alias not found, creating it…");
|
||||
self.create_alias(id)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id<A>(&self, alias: A) -> Result<String>
|
||||
where
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let alias = alias.as_ref();
|
||||
let alias = alias
|
||||
.parse::<i64>()
|
||||
.context(format!("cannot parse id mapper alias {alias}"))?;
|
||||
|
||||
match self {
|
||||
Self::Dummy => Ok(alias.to_string()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("getting id from alias {alias}…");
|
||||
|
||||
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let ids: Vec<String> = stmt
|
||||
.query_map([alias], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let id = ids
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
|
||||
.to_owned();
|
||||
debug!("found id {id} from alias {alias}");
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
|
||||
where
|
||||
A: AsRef<str>,
|
||||
I: IntoIterator<Item = A>,
|
||||
{
|
||||
aliases
|
||||
.into_iter()
|
||||
.map(|alias| self.get_id(alias))
|
||||
.collect()
|
||||
}
|
||||
}
|
168
src/cache/mod.rs
vendored
168
src/cache/mod.rs
vendored
|
@ -1,4 +1,168 @@
|
|||
pub mod args;
|
||||
mod id_mapper;
|
||||
|
||||
pub use id_mapper::IdMapper;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use email::account::config::AccountConfig;
|
||||
use log::{debug, trace};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IdMapper {
|
||||
Dummy,
|
||||
Mapper(String, rusqlite::Connection),
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
|
||||
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
let mut db_parent_dir = dir.as_ref().parent();
|
||||
|
||||
while !db_path.is_file() {
|
||||
match db_parent_dir {
|
||||
Some(dir) => {
|
||||
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
|
||||
db_parent_dir = dir.parent();
|
||||
}
|
||||
None => {
|
||||
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db_path
|
||||
}
|
||||
|
||||
pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result<Self> {
|
||||
let folder = account_config.get_folder_alias(folder)?;
|
||||
let digest = md5::compute(account_config.name.clone() + &folder);
|
||||
let table = format!("id_mapper_{digest:x}");
|
||||
debug!("creating id mapper table {table} at {db_path:?}…");
|
||||
|
||||
let db_path = Self::find_closest_db_path(db_path);
|
||||
let conn = rusqlite::Connection::open(&db_path)
|
||||
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
||||
|
||||
let query = format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
internal_id TEXT UNIQUE
|
||||
)",
|
||||
);
|
||||
trace!("create table query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [])
|
||||
.context("cannot create id mapper table")?;
|
||||
|
||||
Ok(Self::Mapper(table, conn))
|
||||
}
|
||||
|
||||
pub fn create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("creating alias for id {id}…");
|
||||
|
||||
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
|
||||
trace!("insert query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [id])
|
||||
.with_context(|| format!("cannot create id alias for id {id}"))?;
|
||||
|
||||
let alias = conn.last_insert_rowid().to_string();
|
||||
debug!("created alias {alias} for id {id}");
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("getting alias for id {id}…");
|
||||
|
||||
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let aliases: Vec<i64> = stmt
|
||||
.query_map([id], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let alias = match aliases.first() {
|
||||
Some(alias) => {
|
||||
debug!("found alias {alias} for id {id}");
|
||||
alias.to_string()
|
||||
}
|
||||
None => {
|
||||
debug!("alias not found, creating it…");
|
||||
self.create_alias(id)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id<A>(&self, alias: A) -> Result<String>
|
||||
where
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let alias = alias.as_ref();
|
||||
let alias = alias
|
||||
.parse::<i64>()
|
||||
.context(format!("cannot parse id mapper alias {alias}"))?;
|
||||
|
||||
match self {
|
||||
Self::Dummy => Ok(alias.to_string()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("getting id from alias {alias}…");
|
||||
|
||||
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let ids: Vec<String> = stmt
|
||||
.query_map([alias], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let id = ids
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
|
||||
.to_owned();
|
||||
debug!("found id {id} from alias {alias}");
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
|
||||
where
|
||||
A: AsRef<str>,
|
||||
I: IntoIterator<Item = A>,
|
||||
{
|
||||
aliases
|
||||
.into_iter()
|
||||
.map(|alias| self.get_id(alias))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
/// Completion subcommands.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_COMPLETION)
|
||||
.about("Generates the completion script for the given shell")
|
||||
.about("Generate the completion script for the given shell")
|
||||
.args(&[Arg::new(ARG_SHELL)
|
||||
.value_parser(value_parser!(Shell))
|
||||
.required(true)])
|
|
@ -1,758 +0,0 @@
|
|||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use dirs::{config_dir, home_dir};
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
config::Config,
|
||||
email::config::{EmailHooks, EmailTextPlainFormat},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
use toml;
|
||||
|
||||
use crate::{
|
||||
account::TomlAccountConfig,
|
||||
backend::BackendKind,
|
||||
config::{prelude::*, wizard},
|
||||
wizard_prompt, wizard_warn,
|
||||
};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TomlConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_listing_datetime_fmt: Option<String>,
|
||||
pub email_listing_datetime_local_tz: Option<bool>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionEmailTextPlainFormatDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
pub email_sending_save_copy: Option<bool>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionEmailHooksDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, TomlAccountConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig {
|
||||
/// Read and parse the TOML configuration at the given path.
|
||||
///
|
||||
/// Returns an error if the configuration file cannot be read or
|
||||
/// if its content cannot be parsed.
|
||||
fn from_path(path: &Path) -> Result<Self> {
|
||||
let content =
|
||||
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
|
||||
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
|
||||
}
|
||||
|
||||
/// Create and save a TOML configuration using the wizard.
|
||||
///
|
||||
/// If the user accepts the confirmation, the wizard starts and
|
||||
/// help him to create his configuration file. Otherwise the
|
||||
/// program stops.
|
||||
///
|
||||
/// NOTE: the wizard can only be used with interactive shells.
|
||||
async fn from_wizard(path: PathBuf) -> Result<Self> {
|
||||
wizard_warn!("Cannot find existing configuration at {path:?}.");
|
||||
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to create one with the wizard?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if !confirm {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
wizard::configure(path).await
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration from default paths.
|
||||
pub async fn from_default_paths() -> Result<Self> {
|
||||
match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration at the optional given
|
||||
/// path.
|
||||
///
|
||||
/// If the given path exists, then read and parse the TOML
|
||||
/// configuration from it.
|
||||
///
|
||||
/// If the given path does not exist, then create it using the
|
||||
/// wizard.
|
||||
///
|
||||
/// If no path is given, then either read and parse the TOML
|
||||
/// configuration at the first valid default path, otherwise
|
||||
/// create it using the wizard. wizard.
|
||||
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
|
||||
match path.map(Into::into) {
|
||||
Some(ref path) if path.exists() => Self::from_path(path),
|
||||
Some(path) => Self::from_wizard(path).await,
|
||||
None => match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default configuration path.
|
||||
///
|
||||
/// Returns an error if the XDG configuration directory cannot be
|
||||
/// found.
|
||||
pub fn default_path() -> Result<PathBuf> {
|
||||
Ok(config_dir()
|
||||
.ok_or(anyhow!("cannot get XDG config directory"))?
|
||||
.join("himalaya")
|
||||
.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Get the first default configuration path that points to a
|
||||
/// valid file.
|
||||
///
|
||||
/// Tries paths in this order:
|
||||
///
|
||||
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
|
||||
/// `$XDG_CONFIG_DIR` in other OSes.)
|
||||
/// - `$HOME/.config/himalaya/config.toml`
|
||||
/// - `$HOME/.himalayarc`
|
||||
pub fn first_valid_default_path() -> Option<PathBuf> {
|
||||
Self::default_path()
|
||||
.ok()
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
account_name: Option<&str>,
|
||||
disable_cache: bool,
|
||||
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||
let (account_name, mut toml_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(name, account)| {
|
||||
account
|
||||
.default
|
||||
.filter(|default| *default == true)
|
||||
.map(|_| (name.to_owned(), account.clone()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
.ok_or_else(|| anyhow!("cannot find account {name}")),
|
||||
}?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap_config) = toml_account_config.imap.as_mut() {
|
||||
imap_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
|
||||
smtp_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
if let Some(true) = toml_account_config.sync {
|
||||
if !disable_cache {
|
||||
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||
}
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
display_name: self.display_name,
|
||||
signature_delim: self.signature_delim,
|
||||
signature: self.signature,
|
||||
downloads_dir: self.downloads_dir,
|
||||
|
||||
folder_listing_page_size: self.folder_listing_page_size,
|
||||
folder_aliases: self.folder_aliases,
|
||||
|
||||
email_listing_page_size: self.email_listing_page_size,
|
||||
email_listing_datetime_fmt: self.email_listing_datetime_fmt,
|
||||
email_listing_datetime_local_tz: self.email_listing_datetime_local_tz,
|
||||
email_reading_headers: self.email_reading_headers,
|
||||
email_reading_format: self.email_reading_format,
|
||||
email_writing_headers: self.email_writing_headers,
|
||||
email_sending_save_copy: self.email_sending_save_copy,
|
||||
email_hooks: self.email_hooks,
|
||||
|
||||
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|
||||
|(name, config)| {
|
||||
(
|
||||
name.clone(),
|
||||
AccountConfig {
|
||||
name,
|
||||
email: config.email,
|
||||
display_name: config.display_name,
|
||||
signature_delim: config.signature_delim,
|
||||
signature: config.signature,
|
||||
downloads_dir: config.downloads_dir,
|
||||
|
||||
folder_listing_page_size: config.folder_listing_page_size,
|
||||
folder_aliases: config.folder_aliases.unwrap_or_default(),
|
||||
|
||||
email_listing_page_size: config.email_listing_page_size,
|
||||
email_listing_datetime_fmt: config.email_listing_datetime_fmt,
|
||||
email_listing_datetime_local_tz: config.email_listing_datetime_local_tz,
|
||||
|
||||
email_reading_headers: config.email_reading_headers,
|
||||
email_reading_format: config.email_reading_format.unwrap_or_default(),
|
||||
email_writing_headers: config.email_writing_headers,
|
||||
email_sending_save_copy: config.email_sending_save_copy,
|
||||
email_hooks: config.email_hooks.unwrap_or_default(),
|
||||
|
||||
sync: config.sync.unwrap_or_default(),
|
||||
sync_dir: config.sync_dir,
|
||||
sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(),
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
pgp: config.pgp,
|
||||
},
|
||||
)
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let account_config = config.account(&account_name)?;
|
||||
|
||||
Ok((toml_account_config, account_config))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig, maildir::config::MaildirConfig,
|
||||
sendmail::config::SendmailConfig,
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::backend::NotmuchConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::{ImapAuthConfig, ImapConfig};
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::{SmtpAuthConfig, SmtpConfig};
|
||||
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn make_config(config: &str) -> Result<TomlConfig> {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
write!(file, "{}", config).unwrap();
|
||||
TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_config() {
|
||||
let config = make_config("").await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"config file must contain at least one account"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_email_field() {
|
||||
let config = make_config("[account]").await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `email`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `backend`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_invalid_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"bad\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imap_account_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-host`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-port`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-login`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-auth`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_maildir_missing_root_dir_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `maildir-root-dir`"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[tokio::test]
|
||||
async fn account_backend_notmuch_missing_db_path_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `notmuch-db-path`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `sender`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_invalid_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"bad\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-host`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-port`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-login`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_auth_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-auth`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_sendmail_sender_missing_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||
cmd: "/usr/sbin/sendmail".into()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_minimum_config() {
|
||||
use email::sender::SenderConfig;
|
||||
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"
|
||||
smtp-auth = \"passwd\"
|
||||
smtp-passwd = { cmd = \"echo password\" }",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Smtp(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 25,
|
||||
login: "login".into(),
|
||||
auth: SmtpAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..SmtpConfig::default()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_sendmail_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"
|
||||
sendmail-cmd = \"echo send\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||
cmd: Cmd::from("echo send")
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"
|
||||
imap-auth = \"passwd\"
|
||||
imap-passwd = { cmd = \"echo password\" }",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Imap(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 993,
|
||||
login: "login".into(),
|
||||
auth: ImapAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..ImapConfig::default()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_maildir_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"
|
||||
maildir-root-dir = \"/tmp/maildir\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Maildir(MaildirConfig {
|
||||
root_dir: "/tmp/maildir".into(),
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[tokio::test]
|
||||
async fn account_backend_notmuch_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"
|
||||
notmuch-db-path = \"/tmp/notmuch.db\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Notmuch(NotmuchConfig {
|
||||
db_path: "/tmp/notmuch.db".into(),
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,760 @@
|
|||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod prelude;
|
||||
pub mod wizard;
|
||||
|
||||
pub use config::*;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use dirs::{config_dir, home_dir};
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
config::Config,
|
||||
email::config::{EmailHooks, EmailTextPlainFormat},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
use toml;
|
||||
|
||||
use crate::{
|
||||
account::config::TomlAccountConfig, backend::BackendKind, config::prelude::*, wizard_prompt,
|
||||
wizard_warn,
|
||||
};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TomlConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_listing_datetime_fmt: Option<String>,
|
||||
pub email_listing_datetime_local_tz: Option<bool>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionEmailTextPlainFormatDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
pub email_sending_save_copy: Option<bool>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionEmailHooksDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, TomlAccountConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig {
|
||||
/// Read and parse the TOML configuration at the given path.
|
||||
///
|
||||
/// Returns an error if the configuration file cannot be read or
|
||||
/// if its content cannot be parsed.
|
||||
fn from_path(path: &Path) -> Result<Self> {
|
||||
let content =
|
||||
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
|
||||
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
|
||||
}
|
||||
|
||||
/// Create and save a TOML configuration using the wizard.
|
||||
///
|
||||
/// If the user accepts the confirmation, the wizard starts and
|
||||
/// help him to create his configuration file. Otherwise the
|
||||
/// program stops.
|
||||
///
|
||||
/// NOTE: the wizard can only be used with interactive shells.
|
||||
async fn from_wizard(path: PathBuf) -> Result<Self> {
|
||||
wizard_warn!("Cannot find existing configuration at {path:?}.");
|
||||
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to create one with the wizard?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if !confirm {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
wizard::configure(path).await
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration from default paths.
|
||||
pub async fn from_default_paths() -> Result<Self> {
|
||||
match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration at the optional given
|
||||
/// path.
|
||||
///
|
||||
/// If the given path exists, then read and parse the TOML
|
||||
/// configuration from it.
|
||||
///
|
||||
/// If the given path does not exist, then create it using the
|
||||
/// wizard.
|
||||
///
|
||||
/// If no path is given, then either read and parse the TOML
|
||||
/// configuration at the first valid default path, otherwise
|
||||
/// create it using the wizard. wizard.
|
||||
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
|
||||
match path.map(Into::into) {
|
||||
Some(ref path) if path.exists() => Self::from_path(path),
|
||||
Some(path) => Self::from_wizard(path).await,
|
||||
None => match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default configuration path.
|
||||
///
|
||||
/// Returns an error if the XDG configuration directory cannot be
|
||||
/// found.
|
||||
pub fn default_path() -> Result<PathBuf> {
|
||||
Ok(config_dir()
|
||||
.ok_or(anyhow!("cannot get XDG config directory"))?
|
||||
.join("himalaya")
|
||||
.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Get the first default configuration path that points to a
|
||||
/// valid file.
|
||||
///
|
||||
/// Tries paths in this order:
|
||||
///
|
||||
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
|
||||
/// `$XDG_CONFIG_DIR` in other OSes.)
|
||||
/// - `$HOME/.config/himalaya/config.toml`
|
||||
/// - `$HOME/.himalayarc`
|
||||
pub fn first_valid_default_path() -> Option<PathBuf> {
|
||||
Self::default_path()
|
||||
.ok()
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
account_name: Option<&str>,
|
||||
disable_cache: bool,
|
||||
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||
let (account_name, mut toml_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(name, account)| {
|
||||
account
|
||||
.default
|
||||
.filter(|default| *default == true)
|
||||
.map(|_| (name.to_owned(), account.clone()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
.ok_or_else(|| anyhow!("cannot find account {name}")),
|
||||
}?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap_config) = toml_account_config.imap.as_mut() {
|
||||
imap_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
|
||||
smtp_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
if let Some(true) = toml_account_config.sync {
|
||||
if !disable_cache {
|
||||
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||
}
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
display_name: self.display_name,
|
||||
signature_delim: self.signature_delim,
|
||||
signature: self.signature,
|
||||
downloads_dir: self.downloads_dir,
|
||||
|
||||
folder_listing_page_size: self.folder_listing_page_size,
|
||||
folder_aliases: self.folder_aliases,
|
||||
|
||||
email_listing_page_size: self.email_listing_page_size,
|
||||
email_listing_datetime_fmt: self.email_listing_datetime_fmt,
|
||||
email_listing_datetime_local_tz: self.email_listing_datetime_local_tz,
|
||||
email_reading_headers: self.email_reading_headers,
|
||||
email_reading_format: self.email_reading_format,
|
||||
email_writing_headers: self.email_writing_headers,
|
||||
email_sending_save_copy: self.email_sending_save_copy,
|
||||
email_hooks: self.email_hooks,
|
||||
|
||||
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|
||||
|(name, config)| {
|
||||
(
|
||||
name.clone(),
|
||||
AccountConfig {
|
||||
name,
|
||||
email: config.email,
|
||||
display_name: config.display_name,
|
||||
signature_delim: config.signature_delim,
|
||||
signature: config.signature,
|
||||
downloads_dir: config.downloads_dir,
|
||||
|
||||
folder_listing_page_size: config.folder_listing_page_size,
|
||||
folder_aliases: config.folder_aliases.unwrap_or_default(),
|
||||
|
||||
email_listing_page_size: config.email_listing_page_size,
|
||||
email_listing_datetime_fmt: config.email_listing_datetime_fmt,
|
||||
email_listing_datetime_local_tz: config.email_listing_datetime_local_tz,
|
||||
|
||||
email_reading_headers: config.email_reading_headers,
|
||||
email_reading_format: config.email_reading_format.unwrap_or_default(),
|
||||
email_writing_headers: config.email_writing_headers,
|
||||
email_sending_save_copy: config.email_sending_save_copy,
|
||||
email_hooks: config.email_hooks.unwrap_or_default(),
|
||||
|
||||
sync: config.sync.unwrap_or_default(),
|
||||
sync_dir: config.sync_dir,
|
||||
sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(),
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
pgp: config.pgp,
|
||||
},
|
||||
)
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let account_config = config.account(&account_name)?;
|
||||
|
||||
Ok((toml_account_config, account_config))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig, maildir::config::MaildirConfig,
|
||||
sendmail::config::SendmailConfig,
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::backend::NotmuchConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::{ImapAuthConfig, ImapConfig};
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::{SmtpAuthConfig, SmtpConfig};
|
||||
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn make_config(config: &str) -> Result<TomlConfig> {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
write!(file, "{}", config).unwrap();
|
||||
TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_config() {
|
||||
let config = make_config("").await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"config file must contain at least one account"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_email_field() {
|
||||
let config = make_config("[account]").await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `email`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `backend`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_invalid_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"bad\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imap_account_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-host`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-port`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-login`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-auth`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_maildir_missing_root_dir_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `maildir-root-dir`"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[tokio::test]
|
||||
async fn account_backend_notmuch_missing_db_path_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `notmuch-db-path`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_missing_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `sender`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_invalid_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"bad\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-host`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-port`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-login`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_missing_auth_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-auth`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_sendmail_sender_missing_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||
cmd: "/usr/sbin/sendmail".into()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[tokio::test]
|
||||
async fn account_smtp_sender_minimum_config() {
|
||||
use email::sender::SenderConfig;
|
||||
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"
|
||||
smtp-auth = \"passwd\"
|
||||
smtp-passwd = { cmd = \"echo password\" }",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Smtp(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 25,
|
||||
login: "login".into(),
|
||||
auth: SmtpAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..SmtpConfig::default()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_sendmail_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"
|
||||
sendmail-cmd = \"echo send\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||
cmd: Cmd::from("echo send")
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_imap_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"
|
||||
imap-auth = \"passwd\"
|
||||
imap-passwd = { cmd = \"echo password\" }",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Imap(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 993,
|
||||
login: "login".into(),
|
||||
auth: ImapAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..ImapConfig::default()
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_backend_maildir_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"
|
||||
maildir-root-dir = \"/tmp/maildir\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Maildir(MaildirConfig {
|
||||
root_dir: "/tmp/maildir".into(),
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[tokio::test]
|
||||
async fn account_backend_notmuch_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"
|
||||
notmuch-db-path = \"/tmp/notmuch.db\"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Notmuch(NotmuchConfig {
|
||||
db_path: "/tmp/notmuch.db".into(),
|
||||
}),
|
||||
..TomlAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::ui::table::{Cell, Row, Table};
|
||||
|
||||
/// Represents the printable account.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Account {
|
||||
/// Represents the account name.
|
||||
pub name: String,
|
||||
/// Represents the backend name of the account.
|
||||
pub backend: String,
|
||||
/// Represents the default state of the account.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
backend: backend.into(),
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Account {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKEND").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let default = if self.default { "yes" } else { "" };
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).shrinkable().green())
|
||||
.cell(Cell::new(&self.backend).blue())
|
||||
.cell(Cell::new(default).white())
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod accounts;
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
pub use account::*;
|
||||
pub use accounts::*;
|
||||
pub use config::*;
|
|
@ -1 +0,0 @@
|
|||
pub mod config;
|
|
@ -1,2 +0,0 @@
|
|||
pub mod config;
|
||||
pub mod flag;
|
|
@ -1 +0,0 @@
|
|||
pub mod config;
|
|
@ -1,4 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod envelope;
|
||||
pub mod handlers;
|
||||
pub mod message;
|
|
@ -1,66 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
ui::{Cell, Row, Table},
|
||||
Flag, Flags,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Mailbox {
|
||||
pub name: Option<String>,
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelope {
|
||||
pub id: String,
|
||||
pub flags: Flags,
|
||||
pub subject: String,
|
||||
pub from: Mailbox,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for Envelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("FROM").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let flags = {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if !unseen { " " } else { "✷" });
|
||||
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
};
|
||||
let subject = &self.subject;
|
||||
let sender = if let Some(name) = &self.from.name {
|
||||
name
|
||||
} else {
|
||||
&self.from.addr
|
||||
};
|
||||
let date = &self.date;
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod envelope;
|
||||
pub mod envelopes;
|
||||
|
||||
pub use envelope::*;
|
||||
pub use envelopes::*;
|
|
@ -1,21 +0,0 @@
|
|||
use serde::Serialize;
|
||||
use std::{collections::HashSet, ops};
|
||||
|
||||
use crate::Flag;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
||||
pub struct Flags(pub HashSet<Flag>);
|
||||
|
||||
impl ops::Deref for Flags {
|
||||
type Target = HashSet<Flag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<email::flag::Flags> for Flags {
|
||||
fn from(flags: email::flag::Flags) -> Self {
|
||||
Flags(flags.iter().map(Flag::from).collect())
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
||||
|
||||
pub mod flag;
|
||||
pub use flag::*;
|
||||
|
||||
pub mod flags;
|
||||
pub use flags::*;
|
|
@ -1,32 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folder {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl From<&email::folder::Folder> for Folder {
|
||||
fn from(folder: &email::folder::Folder) -> Self {
|
||||
Folder {
|
||||
name: folder.name.clone(),
|
||||
desc: folder.desc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
Folder,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folders(Vec<Folder>);
|
||||
|
||||
impl ops::Deref for Folders {
|
||||
type Target = Vec<Folder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<email::folder::Folders> for Folders {
|
||||
fn from(folders: email::folder::Folders) -> Self {
|
||||
Folders(folders.iter().map(Folder::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Folders {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod folder;
|
||||
pub mod folders;
|
||||
pub mod handlers;
|
||||
|
||||
pub use folder::*;
|
||||
pub use folders::*;
|
|
@ -1,13 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod email;
|
||||
pub mod envelope;
|
||||
pub mod flag;
|
||||
pub mod folder;
|
||||
pub mod tpl;
|
||||
|
||||
pub use self::account::{args, handlers, Account, Accounts};
|
||||
pub use self::email::*;
|
||||
pub use self::envelope::*;
|
||||
pub use self::flag::*;
|
||||
pub use self::folder::*;
|
||||
pub use self::tpl::*;
|
90
src/email/envelope/args.rs
Normal file
90
src/email/envelope/args.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
//! Email CLI module.
|
||||
//!
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_PAGE: &str = "page";
|
||||
const ARG_PAGE_SIZE: &str = "page-size";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_ENVELOPE: &str = "envelope";
|
||||
|
||||
pub type Page = usize;
|
||||
pub type PageSize = usize;
|
||||
|
||||
/// Represents the email commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
Some(Cmd::List(max_table_width, page_size, page))
|
||||
} else {
|
||||
Some(Cmd::List(None, None, 0))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the envelope subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ENVELOPE)
|
||||
.about("Manage envelopes")
|
||||
.subcommands([Command::new(CMD_LIST)
|
||||
.alias("lst")
|
||||
.about("List envelopes")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())])
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.long("page-size")
|
||||
.short('s')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE_SIZE)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short('p')
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default()
|
||||
}
|
|
@ -8,7 +8,7 @@ use anyhow::Result;
|
|||
use clap::{Arg, ArgMatches, Command};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::email;
|
||||
use crate::message;
|
||||
|
||||
const ARG_FLAGS: &str = "flag";
|
||||
|
||||
|
@ -21,61 +21,65 @@ pub(crate) const CMD_FLAG: &str = "flags";
|
|||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Add(email::args::Ids<'a>, Flags),
|
||||
Remove(email::args::Ids<'a>, Flags),
|
||||
Set(email::args::Ids<'a>, Flags),
|
||||
Add(message::args::Ids<'a>, Flags),
|
||||
Remove(message::args::Ids<'a>, Flags),
|
||||
Set(message::args::Ids<'a>, Flags),
|
||||
}
|
||||
|
||||
/// Represents the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FLAG) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_ADD) {
|
||||
debug!("add flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let ids = message::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Add(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REMOVE) {
|
||||
info!("remove flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let ids = message::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Remove(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
|
||||
debug!("set flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let ids = message::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Set(ids, flags))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_FLAG)
|
||||
.about("Handles email flags")
|
||||
/// Represents the flag subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_FLAG)
|
||||
.about("Manage flags")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
Command::new(CMD_ADD)
|
||||
.about("Adds flags to an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(message::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_REMOVE)
|
||||
.aliases(["delete", "del", "d"])
|
||||
.about("Removes flags from an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(message::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SET)
|
||||
.aliases(["change", "c"])
|
||||
.about("Sets flags of an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(message::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
)
|
||||
}
|
||||
|
||||
/// Represents the flags argument.
|
|
@ -1,4 +1,9 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
|
||||
use serde::Serialize;
|
||||
use std::{collections::HashSet, ops};
|
||||
|
||||
/// Represents the flag variants.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
|
||||
|
@ -24,3 +29,20 @@ impl From<&email::flag::Flag> for Flag {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
||||
pub struct Flags(pub HashSet<Flag>);
|
||||
|
||||
impl ops::Deref for Flags {
|
||||
type Target = HashSet<Flag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<email::flag::Flags> for Flags {
|
||||
fn from(flags: email::flag::Flags) -> Self {
|
||||
Flags(flags.iter().map(Flag::from).collect())
|
||||
}
|
||||
}
|
32
src/email/envelope/handlers.rs
Normal file
32
src/email/envelope/handlers.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
};
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &Backend,
|
||||
folder: &str,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
|
||||
let envelopes = backend.list_envelopes(&folder, page_size, page).await?;
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,14 +1,80 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod flag;
|
||||
pub mod handlers;
|
||||
|
||||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
cache::IdMapper,
|
||||
flag::{Flag, Flags},
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
Envelope, IdMapper, Mailbox,
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Mailbox {
|
||||
pub name: Option<String>,
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelope {
|
||||
pub id: String,
|
||||
pub flags: Flags,
|
||||
pub subject: String,
|
||||
pub from: Mailbox,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for Envelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("FROM").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let flags = {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if !unseen { " " } else { "✷" });
|
||||
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
};
|
||||
let subject = &self.subject;
|
||||
let sender = if let Some(name) = &self.from.name {
|
||||
name
|
||||
} else {
|
||||
&self.from.addr
|
||||
};
|
||||
let date = &self.date;
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelopes(Vec<Envelope>);
|
||||
|
@ -62,7 +128,9 @@ mod tests {
|
|||
use email::account::config::AccountConfig;
|
||||
use std::env;
|
||||
|
||||
use crate::{Envelopes, IdMapper};
|
||||
use crate::cache::IdMapper;
|
||||
|
||||
use super::Envelopes;
|
||||
|
||||
#[test]
|
||||
fn default_datetime_fmt() {
|
|
@ -6,15 +6,13 @@
|
|||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
|
||||
use crate::{flag, folder, tpl, ui::table};
|
||||
use crate::{folder, template};
|
||||
|
||||
const ARG_CRITERIA: &str = "criterion";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
const ARG_ID: &str = "id";
|
||||
const ARG_IDS: &str = "ids";
|
||||
const ARG_MIME_TYPE: &str = "mime-type";
|
||||
const ARG_PAGE: &str = "page";
|
||||
const ARG_PAGE_SIZE: &str = "page-size";
|
||||
const ARG_QUERY: &str = "query";
|
||||
const ARG_RAW: &str = "raw";
|
||||
const ARG_REPLY_ALL: &str = "reply-all";
|
||||
|
@ -22,14 +20,12 @@ const CMD_ATTACHMENTS: &str = "attachments";
|
|||
const CMD_COPY: &str = "copy";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_MESSAGE: &str = "message";
|
||||
const CMD_MOVE: &str = "move";
|
||||
const CMD_READ: &str = "read";
|
||||
const CMD_REPLY: &str = "reply";
|
||||
const CMD_SAVE: &str = "save";
|
||||
const CMD_SEARCH: &str = "search";
|
||||
const CMD_SEND: &str = "send";
|
||||
const CMD_SORT: &str = "sort";
|
||||
const CMD_WRITE: &str = "write";
|
||||
|
||||
pub type All = bool;
|
||||
|
@ -38,8 +34,6 @@ pub type Folder<'a> = &'a str;
|
|||
pub type Headers<'a> = Vec<&'a str>;
|
||||
pub type Id<'a> = &'a str;
|
||||
pub type Ids<'a> = Vec<&'a str>;
|
||||
pub type Page = usize;
|
||||
pub type PageSize = usize;
|
||||
pub type Query = String;
|
||||
pub type Raw = bool;
|
||||
pub type RawEmail = String;
|
||||
|
@ -51,131 +45,93 @@ pub enum Cmd<'a> {
|
|||
Attachments(Ids<'a>),
|
||||
Copy(Ids<'a>, Folder<'a>),
|
||||
Delete(Ids<'a>),
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Forward(
|
||||
Id<'a>,
|
||||
template::args::Headers<'a>,
|
||||
template::args::Body<'a>,
|
||||
),
|
||||
Move(Ids<'a>, Folder<'a>),
|
||||
Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
Save(RawEmail),
|
||||
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawEmail),
|
||||
Sort(
|
||||
Criteria,
|
||||
Query,
|
||||
table::args::MaxTableWidth,
|
||||
Option<PageSize>,
|
||||
Page,
|
||||
Reply(
|
||||
Id<'a>,
|
||||
All,
|
||||
template::args::Headers<'a>,
|
||||
template::args::Body<'a>,
|
||||
),
|
||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
||||
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
Save(RawEmail),
|
||||
Send(RawEmail),
|
||||
Write(template::args::Headers<'a>, template::args::Body<'a>),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Attachments(ids)
|
||||
Some(Cmd::Attachments(ids))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Copy(ids, folder)
|
||||
Some(Cmd::Copy(ids, folder))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Delete(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
|
||||
Cmd::Flag(flag::args::matches(m)?)
|
||||
Some(Cmd::Delete(ids))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
let id = parse_id_arg(m);
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Forward(id, headers, body)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
Cmd::List(max_table_width, page_size, page)
|
||||
let headers = template::args::parse_headers_arg(m);
|
||||
let body = template::args::parse_body_arg(m);
|
||||
Some(Cmd::Forward(id, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Move(ids, folder)
|
||||
Some(Cmd::Move(ids, folder))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let mime = parse_mime_type_arg(m);
|
||||
let raw = parse_raw_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
Cmd::Read(ids, mime, raw, headers)
|
||||
Some(Cmd::Read(ids, mime, raw, headers))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
let id = parse_id_arg(m);
|
||||
let all = parse_reply_all_flag(m);
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Reply(id, all, headers, body)
|
||||
let headers = template::args::parse_headers_arg(m);
|
||||
let body = template::args::parse_body_arg(m);
|
||||
Some(Cmd::Reply(id, all, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Save(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
let query = parse_query_arg(m);
|
||||
Cmd::Search(query, max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
let criteria = parse_criteria_arg(m);
|
||||
let query = parse_query_arg(m);
|
||||
Cmd::Sort(criteria, query, max_table_width, page_size, page)
|
||||
Some(Cmd::Save(email))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Send(email)
|
||||
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
|
||||
Cmd::Tpl(tpl::args::matches(m)?)
|
||||
Some(Cmd::Send(email))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Write(headers, body)
|
||||
let headers = template::args::parse_headers_arg(m);
|
||||
let body = template::args::parse_body_arg(m);
|
||||
Some(Cmd::Write(headers, body))
|
||||
} else {
|
||||
Cmd::List(None, None, 0)
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Some(cmd))
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the email subcommands.
|
||||
pub fn subcmds() -> Vec<Command> {
|
||||
vec![
|
||||
flag::args::subcmds(),
|
||||
tpl::args::subcmds(),
|
||||
vec![
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_MESSAGE)
|
||||
.about("Manage messages")
|
||||
.aliases(["msg"])
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommands([
|
||||
Command::new(CMD_ATTACHMENTS)
|
||||
.about("Downloads all emails attachments")
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_LIST)
|
||||
.alias("lst")
|
||||
.about("List envelopes")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_SEARCH)
|
||||
.aliases(["query", "q"])
|
||||
.about("Filter envelopes matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(query_arg()),
|
||||
Command::new(CMD_SORT)
|
||||
.about("Sort envelopes by the given criteria and matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(criteria_arg())
|
||||
.arg(query_arg()),
|
||||
Command::new(CMD_WRITE)
|
||||
.about("Write a new email")
|
||||
.aliases(["new", "n"])
|
||||
.args(tpl::args::args()),
|
||||
.args(template::args::args()),
|
||||
Command::new(CMD_SEND)
|
||||
.about("Send a raw email")
|
||||
.arg(raw_arg()),
|
||||
|
@ -191,12 +147,12 @@ pub fn subcmds() -> Vec<Command> {
|
|||
Command::new(CMD_REPLY)
|
||||
.about("Answer to an email")
|
||||
.arg(reply_all_flag())
|
||||
.args(tpl::args::args())
|
||||
.args(template::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_FORWARD)
|
||||
.aliases(["fwd", "f"])
|
||||
.about("Forward an email")
|
||||
.args(tpl::args::args())
|
||||
.args(template::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_COPY)
|
||||
.alias("cp")
|
||||
|
@ -212,9 +168,7 @@ pub fn subcmds() -> Vec<Command> {
|
|||
.aliases(["remove", "rm"])
|
||||
.about("Delete emails")
|
||||
.arg(ids_arg()),
|
||||
],
|
||||
]
|
||||
.concat()
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the email id argument.
|
||||
|
@ -305,43 +259,6 @@ pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
|
|||
matches.get_flag(ARG_REPLY_ALL)
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.long("page-size")
|
||||
.short('s')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE_SIZE)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short('p')
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email headers argument.
|
||||
pub fn headers_arg() -> Arg {
|
||||
Arg::new(ARG_HEADERS)
|
|
@ -4,7 +4,7 @@ use email::{
|
|||
account::config::AccountConfig, envelope::Id, flag::Flag, message::Message,
|
||||
template::FilterParts,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use log::trace;
|
||||
use mail_builder::MessageBuilder;
|
||||
use std::{
|
||||
fs,
|
||||
|
@ -13,11 +13,7 @@ use std::{
|
|||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::editor,
|
||||
};
|
||||
use crate::{backend::Backend, printer::Printer, ui::editor};
|
||||
|
||||
pub async fn attachments<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
|
@ -120,30 +116,6 @@ pub async fn forward<P: Printer>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &Backend,
|
||||
folder: &str,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
|
||||
let envelopes = backend.list_envelopes(&folder, page_size, page).await?;
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
|
@ -284,61 +256,6 @@ pub async fn save<P: Printer>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search<P: Printer>(
|
||||
_config: &AccountConfig,
|
||||
_printer: &mut P,
|
||||
_backend: &Backend,
|
||||
_folder: &str,
|
||||
_query: String,
|
||||
_max_width: Option<usize>,
|
||||
_page_size: Option<usize>,
|
||||
_page: usize,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
// let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
// let envelopes = Envelopes::from_backend(
|
||||
// config,
|
||||
// id_mapper,
|
||||
// backend
|
||||
// .search_envelopes(&folder, &query, "", page_size, page)
|
||||
// .await?,
|
||||
// )?;
|
||||
// let opts = PrintTableOpts {
|
||||
// format: &config.email_reading_format,
|
||||
// max_width,
|
||||
// };
|
||||
|
||||
// printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub async fn sort<P: Printer>(
|
||||
_config: &AccountConfig,
|
||||
_printer: &mut P,
|
||||
_backend: &Backend,
|
||||
_folder: &str,
|
||||
_sort: String,
|
||||
_query: String,
|
||||
_max_width: Option<usize>,
|
||||
_page_size: Option<usize>,
|
||||
_page: usize,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
// let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
// let envelopes = Envelopes::from_backend(
|
||||
// config,
|
||||
// id_mapper,
|
||||
// backend
|
||||
// .search_envelopes(&folder, &query, &sort, page_size, page)
|
||||
// .await?,
|
||||
// )?;
|
||||
// let opts = PrintTableOpts {
|
||||
// format: &config.email_reading_format,
|
||||
// max_width,
|
||||
// };
|
||||
|
||||
// printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub async fn send<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
4
src/email/message/mod.rs
Normal file
4
src/email/message/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
pub mod template;
|
|
@ -7,7 +7,7 @@ use anyhow::Result;
|
|||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use log::warn;
|
||||
|
||||
use crate::email;
|
||||
use crate::message;
|
||||
|
||||
const ARG_BODY: &str = "body";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
|
@ -27,9 +27,14 @@ pub type Body<'a> = Option<&'a str>;
|
|||
/// Represents the template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
|
||||
Forward(message::args::Id<'a>, Headers<'a>, Body<'a>),
|
||||
Write(Headers<'a>, Body<'a>),
|
||||
Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
|
||||
Reply(
|
||||
message::args::Id<'a>,
|
||||
message::args::All,
|
||||
Headers<'a>,
|
||||
Body<'a>,
|
||||
),
|
||||
Save(RawTpl),
|
||||
Send(RawTpl),
|
||||
}
|
||||
|
@ -37,13 +42,13 @@ pub enum Cmd<'a> {
|
|||
/// Represents the template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let id = message::args::parse_id_arg(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Forward(id, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let all = email::args::parse_reply_all_flag(m);
|
||||
let id = message::args::parse_id_arg(m);
|
||||
let all = message::args::parse_reply_all_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Reply(id, all, headers, body))
|
||||
|
@ -65,55 +70,55 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
}
|
||||
|
||||
/// Represents the template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_TPL)
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_TPL)
|
||||
.alias("tpl")
|
||||
.about("Handles email templates")
|
||||
.about("Manage templates")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
Command::new(CMD_FORWARD)
|
||||
.alias("fwd")
|
||||
.about("Generates a template for forwarding an email")
|
||||
.arg(email::args::id_arg())
|
||||
.about("Generate a template for forwarding an email")
|
||||
.arg(message::args::id_arg())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_REPLY)
|
||||
.about("Generates a template for replying to an email")
|
||||
.arg(email::args::id_arg())
|
||||
.arg(email::args::reply_all_flag())
|
||||
.about("Generate a template for replying to an email")
|
||||
.arg(message::args::id_arg())
|
||||
.arg(message::args::reply_all_flag())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SAVE)
|
||||
.about("Compiles the template into a valid email then saves it")
|
||||
.about("Compile the template into a valid email then saves it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SEND)
|
||||
.about("Compiles the template into a valid email then sends it")
|
||||
.about("Compile the template into a valid email then sends it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_WRITE)
|
||||
.aliases(["new", "n"])
|
||||
.about("Generates a template for writing a new email")
|
||||
.about("Generate a template for writing a new email")
|
||||
.args(&args()),
|
||||
)]
|
||||
)
|
||||
}
|
||||
|
||||
/// Represents the template arguments.
|
||||
pub fn args() -> Vec<Arg> {
|
||||
vec![
|
||||
Arg::new(ARG_HEADERS)
|
||||
.help("Overrides a specific header")
|
||||
.help("Override a specific header")
|
||||
.short('H')
|
||||
.long("header")
|
||||
.value_name("KEY:VAL")
|
||||
.action(ArgAction::Append),
|
||||
Arg::new(ARG_BODY)
|
||||
.help("Overrides the body")
|
||||
.help("Override the body")
|
||||
.short('B')
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
5
src/email/mod.rs
Normal file
5
src/email/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod envelope;
|
||||
pub mod message;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::{envelope::flag, message::template};
|
|
@ -19,7 +19,7 @@ const ARG_TARGET: &str = "target";
|
|||
const CMD_CREATE: &str = "create";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_EXPUNGE: &str = "expunge";
|
||||
const CMD_FOLDERS: &str = "folders";
|
||||
const CMD_FOLDER: &str = "folder";
|
||||
const CMD_LIST: &str = "list";
|
||||
|
||||
/// Represents the folder commands.
|
||||
|
@ -33,7 +33,7 @@ pub enum Cmd {
|
|||
|
||||
/// Represents the folder command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) {
|
||||
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
|
||||
info!("expunge folder subcommand matched");
|
||||
Some(Cmd::Expunge)
|
||||
|
@ -60,7 +60,7 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
|
||||
/// Represents the folder subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_FOLDERS)
|
||||
Command::new(CMD_FOLDER)
|
||||
.about("Manage folders")
|
||||
.subcommands([
|
||||
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
|
|
@ -10,9 +10,10 @@ use std::process;
|
|||
use crate::{
|
||||
backend::Backend,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Folders,
|
||||
};
|
||||
|
||||
use super::Folders;
|
||||
|
||||
pub async fn expunge<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
|
||||
backend.expunge_folder(folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))
|
||||
|
@ -58,10 +59,12 @@ pub async fn delete<P: Printer>(printer: &mut P, backend: &Backend, folder: &str
|
|||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
use email::{
|
||||
account::AccountConfig,
|
||||
account::config::AccountConfig,
|
||||
backend::Backend,
|
||||
email::{Envelope, Envelopes, Flags, Messages},
|
||||
envelope::{Envelope, Envelopes},
|
||||
flag::Flags,
|
||||
folder::{Folder, Folders},
|
||||
message::Messages,
|
||||
};
|
||||
use std::{any::Any, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
67
src/folder/mod.rs
Normal file
67
src/folder/mod.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folder {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl From<&email::folder::Folder> for Folder {
|
||||
fn from(folder: &email::folder::Folder) -> Self {
|
||||
Folder {
|
||||
name: folder.name.clone(),
|
||||
desc: folder.desc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folders(Vec<Folder>);
|
||||
|
||||
impl ops::Deref for Folders {
|
||||
type Target = Vec<Folder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<email::folder::Folders> for Folders {
|
||||
fn from(folders: email::folder::Folders) -> Self {
|
||||
Folders(folders.iter().map(Folder::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Folders {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,20 +1,24 @@
|
|||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod cache;
|
||||
pub mod compl;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod domain;
|
||||
pub mod email;
|
||||
pub mod folder;
|
||||
#[cfg(feature = "imap")]
|
||||
pub mod imap;
|
||||
#[cfg(feature = "maildir")]
|
||||
pub mod maildir;
|
||||
pub mod man;
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub mod notmuch;
|
||||
pub mod output;
|
||||
pub mod printer;
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
pub mod ui;
|
||||
|
||||
pub use cache::IdMapper;
|
||||
pub use domain::*;
|
||||
#[doc(inline)]
|
||||
pub use email::{envelope, flag, message, template};
|
||||
|
|
259
src/main.rs
259
src/main.rs
|
@ -8,11 +8,11 @@ use url::Url;
|
|||
use himalaya::{
|
||||
account,
|
||||
backend::{Backend, BackendBuilder},
|
||||
cache, compl,
|
||||
cache, completion,
|
||||
config::{self, TomlConfig},
|
||||
email, flag, folder, man, output,
|
||||
envelope, flag, folder, man, message, output,
|
||||
printer::StdoutPrinter,
|
||||
tpl,
|
||||
template,
|
||||
};
|
||||
|
||||
fn create_app() -> Command {
|
||||
|
@ -27,14 +27,16 @@ fn create_app() -> Command {
|
|||
.arg(cache::args::arg())
|
||||
.args(output::args::args())
|
||||
.arg(folder::args::source_arg())
|
||||
.subcommand(compl::args::subcmd())
|
||||
.subcommand(completion::args::subcmd())
|
||||
.subcommand(man::args::subcmd())
|
||||
.subcommand(account::args::subcmd())
|
||||
.subcommand(folder::args::subcmd())
|
||||
.subcommands(email::args::subcmds())
|
||||
.subcommand(envelope::args::subcmd())
|
||||
.subcommand(flag::args::subcmd())
|
||||
.subcommand(message::args::subcmd())
|
||||
.subcommand(template::args::subcmd())
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
@ -58,22 +60,24 @@ async fn main() -> Result<()> {
|
|||
let backend = backend_builder.build().await?;
|
||||
let mut printer = StdoutPrinter::default();
|
||||
|
||||
return email::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
|
||||
return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
|
||||
}
|
||||
|
||||
let app = create_app();
|
||||
let m = app.get_matches();
|
||||
|
||||
// check completion command before configs
|
||||
// check completionetion command before configs
|
||||
// https://github.com/soywod/himalaya/issues/115
|
||||
match compl::args::matches(&m)? {
|
||||
Some(compl::args::Cmd::Generate(shell)) => {
|
||||
return compl::handlers::generate(create_app(), shell);
|
||||
#[allow(clippy::single_match)]
|
||||
match completion::args::matches(&m)? {
|
||||
Some(completion::args::Cmd::Generate(shell)) => {
|
||||
return completion::handlers::generate(create_app(), shell);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// check also man command before configs
|
||||
#[allow(clippy::single_match)]
|
||||
match man::args::matches(&m)? {
|
||||
Some(man::args::Cmd::GenerateAll(dir)) => {
|
||||
return man::handlers::generate(dir, create_app());
|
||||
|
@ -170,48 +174,11 @@ async fn main() -> Result<()> {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
// checks email commands
|
||||
match email::args::matches(&m)? {
|
||||
Some(email::args::Cmd::Attachments(ids)) => {
|
||||
match envelope::args::matches(&m)? {
|
||||
Some(envelope::args::Cmd::List(max_width, page_size, page)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::attachments(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
ids,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Copy(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
}
|
||||
Some(email::args::Cmd::Delete(ids)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::delete(&mut printer, &backend, &folder, ids).await;
|
||||
}
|
||||
Some(email::args::Cmd::Forward(id, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return email::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::List(max_width, page_size, page)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::list(
|
||||
return envelope::handlers::list(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
|
@ -222,15 +189,74 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Move(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
_ => (),
|
||||
}
|
||||
Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => {
|
||||
|
||||
match flag::args::matches(&m)? {
|
||||
Some(flag::args::Cmd::Set(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::read(
|
||||
return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Add(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Remove(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match message::args::matches(&m)? {
|
||||
Some(message::args::Cmd::Attachments(ids)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::attachments(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
ids,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Copy(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Delete(ids)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::delete(&mut printer, &backend, &folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Forward(id, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Move(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::read(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
|
@ -242,10 +268,10 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
Some(message::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return email::handlers::reply(
|
||||
return message::handlers::reply(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
|
@ -257,73 +283,35 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Save(raw_email)) => {
|
||||
Some(message::args::Cmd::Save(raw_email)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::save(&mut printer, &backend, &folder, raw_email).await;
|
||||
return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
|
||||
}
|
||||
Some(email::args::Cmd::Search(query, max_width, page_size, page)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::search(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return email::handlers::sort(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
criteria,
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Send(raw_email)) => {
|
||||
Some(message::args::Cmd::Send(raw_email)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await;
|
||||
return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
|
||||
.await;
|
||||
}
|
||||
Some(email::args::Cmd::Flag(m)) => match m {
|
||||
Some(flag::args::Cmd::Set(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Add(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Remove(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
|
||||
Some(message::args::Cmd::Write(headers, body)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::write(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Some(email::args::Cmd::Tpl(m)) => match m {
|
||||
Some(tpl::args::Cmd::Forward(id, headers, body)) => {
|
||||
}
|
||||
|
||||
match template::args::matches(&m)? {
|
||||
Some(template::args::Cmd::Forward(id, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return tpl::handlers::forward(
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
|
@ -334,14 +322,13 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
Some(tpl::args::Cmd::Write(headers, body)) => {
|
||||
return tpl::handlers::write(&account_config, &mut printer, headers, body).await;
|
||||
Some(template::args::Cmd::Write(headers, body)) => {
|
||||
return template::handlers::write(&account_config, &mut printer, headers, body).await;
|
||||
}
|
||||
Some(tpl::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
Some(template::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return tpl::handlers::reply(
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::reply(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
|
@ -353,23 +340,21 @@ async fn main() -> Result<()> {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
Some(tpl::args::Cmd::Save(tpl)) => {
|
||||
Some(template::args::Cmd::Save(template)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return tpl::handlers::save(&account_config, &mut printer, &backend, &folder, tpl)
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::save(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
template,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(tpl::args::Cmd::Send(tpl)) => {
|
||||
let backend =
|
||||
Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Some(email::args::Cmd::Write(headers, body)) => {
|
||||
Some(template::args::Cmd::Send(template)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return email::handlers::write(&account_config, &mut printer, &backend, headers, body)
|
||||
return template::handlers::send(&account_config, &mut printer, &backend, template)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
|
|
Loading…
Reference in a new issue