use crate::_utility::resolve_os_str;
use crate::{Result, TypedValueMap};
use lettre::message::Mailbox;
use std::path::PathBuf;
#[derive(Deserialize)]
pub struct MaillistConfig {
pub recipients: Vec<String>,
pub signature: Option<String>,
pub audience: Option<String>,
pub domain: Option<String>,
}
impl MaillistConfig {
fn load(path: &PathBuf) -> Result<Self> {
let cb = config::Config::builder()
.set_default("recipients", Vec::<String>::new())?
.set_default("signature", None::<String>)?
.set_default("audience", None::<String>)?
.set_default("domain", None::<String>)?
.add_source(config::File::with_name(&format!("{}", path.display())));
match cb.build() {
Ok(c) => Ok(c.try_deserialize()?),
Err(e) => bail!(
"Unable to build maillist from '{}'. Encountered errors: {}",
path.display(),
e
),
}
}
}
#[derive(Default, Clone, Debug)]
pub struct Maillist {
pub(super) recipients: Vec<String>,
pub(super) file: Option<PathBuf>,
pub(super) audience: Option<String>,
pub(super) signature: Option<String>,
pub(super) domain: Option<String>,
pub name: String,
}
impl Maillist {
pub fn new(
name: String,
recipients: Vec<String>,
signature: Option<String>,
audience: Option<String>,
domain: Option<String>,
) -> Result<Self> {
Ok(Self {
file: None,
recipients,
signature,
audience: match audience.as_ref() {
Some(a) => {
let mapped = Self::map_audience(&a);
let mapped_name = Self::map_audience(&name);
if let Some(mn) = mapped_name {
if let Some(m) = mapped.as_ref() {
if m != &mn {
bail!(
"Maillist '{}' was given audience '{}' (maps to '{}') but conflicts with the named audience '{}'.",
&name,
a,
m,
mn
)
}
} else {
bail!(
"Maillist '{}' was given audience '{}' but conflicts with the named audience '{}'.",
&name,
a,
mn
)
}
}
mapped.or(audience)
}
None => Self::map_audience(&name),
},
domain,
name,
})
}
pub fn from_file(f: &PathBuf) -> Result<Self> {
let ext = resolve_os_str(match f.extension() {
Some(ext) => ext,
None => {
bail!(
"Could not discern extension for maillist at '{}'",
f.display()
)
}
})?;
let mut name = resolve_os_str(match f.file_name() {
Some(n) => n,
None => {
bail!(
"Could not discern file name for maillist at '{}'",
f.display()
)
}
})?;
match ext.as_str() {
"maillist" => {
name = match name.strip_suffix(".maillist") {
Some(n) => n.to_string(),
None => bail!("Expected {} to end with '.maillist'", name),
};
let file;
match std::fs::File::open(f) {
Ok(f) => file = f,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
bail!("Unable to find maillist at: '{}'", f.display())
}
_ => return Err(e.into()),
},
}
let reader = std::io::BufReader::new(file);
let mut recipients: Vec<String> = vec![];
for recipient in std::io::BufRead::lines(reader) {
recipients.push(recipient?);
}
Ok(Self {
recipients: recipients,
file: Some(f.to_path_buf()),
audience: Self::map_audience(&name),
name: name,
..Default::default()
})
}
"toml" => {
name = match name.strip_suffix(".maillist.toml") {
Some(n) => n.to_string(),
None => bail!("Expected {} to end with '.maillist.toml'", name),
};
let c = MaillistConfig::load(f)?;
Ok(Self {
file: Some(f.to_path_buf()),
recipients: c.recipients.clone(),
audience: {
if let Some(aud) = c.audience.as_ref() {
let _a = Self::map_audience(aud);
let a = _a.as_ref().unwrap_or(aud);
if let Some(mapped_a) = Self::map_audience(&name) {
if &mapped_a != a {
bail!(
"Maillist at '{}' was given audience '{}' (maps to '{}') but conflicts with the named audience '{}'.",
f.display(),
aud,
a,
mapped_a
);
} else {
Some(mapped_a)
}
} else {
Some(a.to_string())
}
} else {
Self::map_audience(&name)
}
},
signature: c.signature.clone(),
domain: c.domain.clone(),
name: name,
})
}
_ => bail!("Unsupported file extension for maillist '{}'", f.display()),
}
}
pub(super) fn map_audience(s: &str) -> Option<String> {
match s.to_ascii_lowercase().as_str() {
"dev" | "develop" | "development" => Some("development".to_string()),
"release" | "prod" | "production" => Some("production".to_string()),
_ => None,
}
}
pub fn is_development(&self) -> bool {
self.audience
.as_ref()
.map_or(false, |aud| aud == "development")
}
pub fn is_production(&self) -> bool {
self.audience
.as_ref()
.map_or(false, |aud| aud == "production")
}
pub fn recipients(&self) -> &Vec<String> {
&self.recipients
}
pub fn signature(&self) -> &Option<String> {
&self.signature
}
pub fn audience(&self) -> &Option<String> {
&self.audience
}
pub fn domain(&self) -> &Option<String> {
&self.domain
}
pub fn file(&self) -> &Option<PathBuf> {
&self.file
}
pub fn resolve_recipients(&self, default_domain: &Option<String>) -> Result<Vec<Mailbox>> {
let mut retn = vec![];
for r in self.recipients.iter() {
let email_str;
if r.contains("@") {
email_str = r.to_string();
} else {
if let Some(d) = self.domain.as_ref() {
email_str = format!("{}@{}", r, d);
} else if let Some(d) = default_domain {
email_str = format!("{}@{}", r, d);
} else {
email_str = r.to_string();
}
}
use std::str::FromStr;
let _em = email_address::EmailAddress::from_str(&email_str)?;
retn.push(email_str.parse()?);
}
Ok(retn)
}
pub fn config(&self) -> Result<TypedValueMap> {
let mut retn = TypedValueMap::new();
retn.insert("file", self.file.as_ref());
retn.insert("name", &self.name);
retn.insert("audience", self.audience.as_ref());
retn.insert("recipients", self.recipients.clone());
retn.insert("signature", self.signature.as_ref());
retn.insert("domain", self.domain.as_ref());
Ok(retn)
}
}