#![allow(deprecated)]
use std::{num::NonZeroU16, str::FromStr};
use lettre::message::Mailbox;
use schemars::JsonSchema;
use serde::{de::Error, Deserialize, Serialize};
use super::ConfigurationSection;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Credentials {
pub username: String,
pub password: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EmailSmtpMode {
Plain,
StartTls,
Tls,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum EmailTransportKind {
#[default]
Blackhole,
Smtp,
Sendmail,
}
fn default_email() -> String {
r#""Authentication Service" <root@localhost>"#.to_owned()
}
#[allow(clippy::unnecessary_wraps)]
fn default_sendmail_command() -> Option<String> {
Some("sendmail".to_owned())
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EmailConfig {
#[serde(default = "default_email")]
#[schemars(email)]
pub from: String,
#[serde(default = "default_email")]
#[schemars(email)]
pub reply_to: String,
transport: EmailTransportKind,
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<EmailSmtpMode>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::Hostname>")]
hostname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 65535))]
port: Option<NonZeroU16>,
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(default = "default_sendmail_command")]
command: Option<String>,
}
impl EmailConfig {
#[must_use]
pub fn transport(&self) -> EmailTransportKind {
self.transport
}
#[must_use]
pub fn mode(&self) -> Option<EmailSmtpMode> {
self.mode
}
#[must_use]
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
#[must_use]
pub fn port(&self) -> Option<NonZeroU16> {
self.port
}
#[must_use]
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
#[must_use]
pub fn command(&self) -> Option<&str> {
self.command.as_deref()
}
}
impl Default for EmailConfig {
fn default() -> Self {
Self {
from: default_email(),
reply_to: default_email(),
transport: EmailTransportKind::Blackhole,
mode: None,
hostname: None,
port: None,
username: None,
password: None,
command: None,
}
}
}
impl ConfigurationSection for EmailConfig {
const PATH: Option<&'static str> = Some("email");
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
let metadata = figment.find_metadata(Self::PATH.unwrap());
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
error.metadata = metadata.cloned();
error.profile = Some(figment::Profile::Default);
error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
error
};
let missing_field = |field: &'static str| {
error_on_field(figment::error::Error::missing_field(field), field)
};
let unexpected_field = |field: &'static str, expected_fields: &'static [&'static str]| {
error_on_field(
figment::error::Error::unknown_field(field, expected_fields),
field,
)
};
match self.transport {
EmailTransportKind::Blackhole => {}
EmailTransportKind::Smtp => {
if let Err(e) = Mailbox::from_str(&self.from) {
return Err(error_on_field(figment::error::Error::custom(e), "from"));
}
if let Err(e) = Mailbox::from_str(&self.reply_to) {
return Err(error_on_field(figment::error::Error::custom(e), "reply_to"));
}
match (self.username.is_some(), self.password.is_some()) {
(true, true) | (false, false) => {}
(true, false) => {
return Err(missing_field("password"));
}
(false, true) => {
return Err(missing_field("username"));
}
}
if self.mode.is_none() {
return Err(missing_field("mode"));
}
if self.hostname.is_none() {
return Err(missing_field("hostname"));
}
if self.command.is_some() {
return Err(unexpected_field(
"command",
&[
"from",
"reply_to",
"transport",
"mode",
"hostname",
"port",
"username",
"password",
],
));
}
}
EmailTransportKind::Sendmail => {
let expected_fields = &["from", "reply_to", "transport", "command"];
if let Err(e) = Mailbox::from_str(&self.from) {
return Err(error_on_field(figment::error::Error::custom(e), "from"));
}
if let Err(e) = Mailbox::from_str(&self.reply_to) {
return Err(error_on_field(figment::error::Error::custom(e), "reply_to"));
}
if self.command.is_none() {
return Err(missing_field("command"));
}
if self.mode.is_some() {
return Err(unexpected_field("mode", expected_fields));
}
if self.hostname.is_some() {
return Err(unexpected_field("hostname", expected_fields));
}
if self.port.is_some() {
return Err(unexpected_field("port", expected_fields));
}
if self.username.is_some() {
return Err(unexpected_field("username", expected_fields));
}
if self.password.is_some() {
return Err(unexpected_field("password", expected_fields));
}
}
}
Ok(())
}
}