use std::{fs, path::Path, process::Command};
use shellexpand::tilde;
use crate::{ConfigError, DEFAULT_MAILDIR};
pub(crate) struct TomlAccount<'a> {
name: String,
settings: &'a toml::value::Table,
local: String,
}
pub(crate) struct TomlConfig {
cfg: toml::value::Table,
}
impl TomlConfig {
pub(crate) fn for_account(&self, name: Option<&str>) -> Option<TomlAccount<'_>> {
let name = match name {
Some(n) => n,
None => self.cfg.keys().next().expect("no account found"),
};
if let Some(a) = self.cfg.get(name) {
if let Some(t) = a.as_table() {
let mut path = DEFAULT_MAILDIR;
if let Some(l) = t.get("local") {
if let Some(p) = l.as_str() {
path = p
}
};
return Some(TomlAccount {
name: String::from(name),
settings: t,
local: tilde(path).into_owned(),
});
}
}
None
}
}
impl TomlAccount<'_> {
pub(crate) fn name(&self) -> &String {
&self.name
}
pub(crate) fn local(&self) -> &str {
&self.local
}
pub(crate) fn remote(&self) -> Option<&str> {
self.settings.get("remote").and_then(|v| v.as_str())
}
pub(crate) fn user(&self) -> Option<&str> {
self.settings.get("user").and_then(|v| v.as_str())
}
pub(crate) fn send_from(&self) -> Option<&str> {
let v = self
.settings
.get("send")
.and_then(|v| v.as_table())
.and_then(|v| v.get("from"))
.and_then(|v| v.as_str());
v.or_else(|| self.user())
}
pub(crate) fn send_user(&self) -> Option<&str> {
let v = self
.settings
.get("send")
.and_then(|v| v.as_table())
.and_then(|v| v.get("user"))
.and_then(|v| v.as_str());
v.or_else(|| self.user())
}
pub(crate) fn send_remote(&self) -> Option<&str> {
let v = self
.settings
.get("send")
.and_then(|v| v.as_table())
.and_then(|v| v.get("remote"))
.and_then(|v| v.as_str());
v.or_else(|| self.remote())
}
fn pass_cmd(cmd: &str) -> Result<Option<String>, ConfigError> {
let out = Command::new("sh").arg("-c").arg(cmd).output();
match out {
Ok(mut output) => {
let newline: u8 = 10;
if Some(&newline) == output.stdout.last() {
_ = output.stdout.pop(); }
Ok(Some(String::from_utf8(output.stdout)?))
}
Err(e) => Err(ConfigError::PassExecError(e.to_string())),
}
}
pub(crate) fn password(&self) -> Result<Option<String>, ConfigError> {
if let Some(p) = self.settings.get("password").and_then(|v| v.as_str()) {
return Ok(Some(String::from(p)));
} else if let Some(cmd) = self.settings.get("pass-cmd").and_then(|v| v.as_str()) {
return TomlAccount::pass_cmd(cmd);
}
Ok(None)
}
pub(crate) fn send_password(&self) -> Result<Option<String>, ConfigError> {
let pass = self
.settings
.get("send")
.and_then(|v| v.as_table())
.and_then(|v| v.get("password"))
.and_then(|v| v.as_str());
if let Some(p) = pass {
return Ok(Some(String::from(p)));
}
let pcmd = self
.settings
.get("send")
.and_then(|v| v.as_table())
.and_then(|v| v.get("pass-cmd"))
.and_then(|v| v.as_str());
if let Some(cmd) = pcmd {
return TomlAccount::pass_cmd(cmd);
}
self.password()
}
}
pub(crate) fn load_toml<P: AsRef<Path>>(path: P) -> Result<TomlConfig, ConfigError> {
let contents = fs::read_to_string(path)?;
let table = contents.parse::<toml::Table>()?;
if table.is_empty() {
Err(ConfigError::Error("no accounts found"))
} else {
Ok(TomlConfig { cfg: table })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple() {
let table: toml::value::Table = toml::from_str(
r#"
[example]
local = '/home/test/.maildir'
remote = 'mx.example.com'
user = 'johndoe'
password = 'hunter1'
"#,
)
.unwrap();
let config = TomlConfig { cfg: table };
let acc = config.for_account(None).expect("no account found");
assert_eq!(acc.user(), Some("johndoe"));
assert_eq!(acc.remote(), Some("mx.example.com"));
assert_eq!(
acc.password().expect("failed to get password"),
Some(String::from("hunter1"))
);
assert_eq!(acc.send_user(), Some("johndoe"));
assert_eq!(acc.send_remote(), Some("mx.example.com"));
assert_eq!(
acc.send_password().expect("failed to get password"),
Some(String::from("hunter1"))
);
}
#[test]
fn test_minimal() {
let table: toml::value::Table = toml::from_str(
r#"
[example]
"#,
)
.unwrap();
let config = TomlConfig { cfg: table };
let _ = config.for_account(None).expect("no account found");
}
#[test]
fn test_send() {
let table: toml::value::Table = toml::from_str(
r#"
[example]
local = '/home/test/.maildir'
remote = 'imap.example.com'
user = 'johndoe'
password = 'hunter1'
send.remote = 'smtp.example.com'
send.user = 'johndoe@example.com'
send.password = 's3cr3t'
"#,
)
.unwrap();
let config = TomlConfig { cfg: table };
let acc = config.for_account(None).expect("no account found");
assert_eq!(acc.user(), Some("johndoe"));
assert_eq!(acc.remote(), Some("imap.example.com"));
assert_eq!(
acc.password().expect("failed to get password"),
Some(String::from("hunter1"))
);
assert_eq!(acc.send_user(), Some("johndoe@example.com"));
assert_eq!(acc.send_remote(), Some("smtp.example.com"));
assert_eq!(
acc.send_password().expect("failed to get password"),
Some(String::from("s3cr3t"))
);
}
#[test]
fn test_simple_cmd() {
let table: toml::value::Table = toml::from_str(
r#"
[example]
local = '/home/test/.maildir'
remote = 'mx.example.com'
user = 'johndoe'
pass-cmd = 'echo hunter1'
"#,
)
.unwrap();
let config = TomlConfig { cfg: table };
let acc = config.for_account(None).expect("no account found");
assert_eq!(acc.user(), Some("johndoe"));
assert_eq!(acc.remote(), Some("mx.example.com"));
assert_eq!(
acc.password().expect("failed to get password"),
Some(String::from("hunter1"))
);
assert_eq!(acc.send_user(), Some("johndoe"));
assert_eq!(acc.send_remote(), Some("mx.example.com"));
assert_eq!(
acc.send_password().expect("failed to get password"),
Some(String::from("hunter1"))
);
}
#[test]
fn test_send_cmd() {
let table: toml::value::Table = toml::from_str(
r#"
[example]
local = '/home/test/.maildir'
remote = 'imap.example.com'
user = 'johndoe'
pass-cmd = 'echo hunter1'
send.remote = 'smtp.example.com'
send.user = 'johndoe@example.com'
send.pass-cmd = 'echo s3cr3t'
"#,
)
.unwrap();
let config = TomlConfig { cfg: table };
let acc = config.for_account(None).expect("no account found");
assert_eq!(acc.user(), Some("johndoe"));
assert_eq!(acc.remote(), Some("imap.example.com"));
assert_eq!(
acc.password().expect("failed to get password"),
Some(String::from("hunter1"))
);
assert_eq!(acc.send_user(), Some("johndoe@example.com"));
assert_eq!(acc.send_remote(), Some("smtp.example.com"));
assert_eq!(
acc.send_password().expect("failed to get password"),
Some(String::from("s3cr3t"))
);
}
}