Files
mailks/config.rs
T
zhenyi c4824ef261 feat(k8s): add Kubernetes Helm chart for emailks service
- Create Helm chart structure with Chart.yaml and values.yaml
- Add deployment template with container configuration and environment variables
- Implement service template for gRPC port exposure
- Add service account template with security configuration
- Include horizontal pod autoscaler template for scaling capabilities
- Add helper templates for naming and label management
- Configure SMTP settings as configurable parameters in values.yaml
- Set up resource limits and requests for container performance
- Implement liveness and readiness probes for health checks
- Add support for existing secrets and custom configurations
2026-06-07 22:59:06 +08:00

214 lines
6.2 KiB
Rust

use std::{env, fmt, net::SocketAddr, time::Duration};
pub use crate::error::ConfigError;
use crate::ENV_PREFIX;
use tracing;
const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:50051";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppConfig {
pub smtp: SmtpConfig,
pub queue_capacity: Option<usize>,
pub listen_addr: SocketAddr,
}
#[derive(Clone, PartialEq, Eq)]
pub struct SmtpConfig {
pub host: String,
pub port: u16,
pub username: Option<String>,
pub password: Option<String>,
pub from_email: Option<String>,
pub from_name: Option<String>,
pub reply_to: Option<String>,
pub tls: SmtpTls,
pub timeout: Duration,
pub helo_name: Option<String>,
pub allow_request_from: bool,
}
impl fmt::Debug for SmtpConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SmtpConfig")
.field("host", &self.host)
.field("port", &self.port)
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "***"))
.field("from_email", &self.from_email)
.field("from_name", &self.from_name)
.field("reply_to", &self.reply_to)
.field("tls", &self.tls)
.field("timeout", &self.timeout)
.field("helo_name", &self.helo_name)
.field("allow_request_from", &self.allow_request_from)
.finish()
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum SmtpTls {
None,
#[default]
StartTls,
Tls,
}
impl AppConfig {
pub fn from_env() -> Result<Self, ConfigError> {
let _ = dotenvy::dotenv();
let queue_capacity: Option<usize> = optional("QUEUE_CAPACITY")?
.map(|v| {
v.parse::<usize>()
.map_err(|e| invalid("QUEUE_CAPACITY", e.to_string()))
})
.transpose()?;
let listen_addr = parse_or(
"LISTEN_ADDR",
DEFAULT_LISTEN_ADDR.parse().expect("valid default address"),
)?;
Ok(Self {
smtp: SmtpConfig::from_env()?,
queue_capacity,
listen_addr,
})
}
}
impl SmtpConfig {
pub fn from_env() -> Result<Self, ConfigError> {
let _ = dotenvy::dotenv();
let host = required("HOST")?;
let port = parse_or("PORT", 587)?;
let from_email = optional("FROM_EMAIL")?;
let reply_to = optional("REPLY_TO")?;
let timeout_secs = parse_or("TIMEOUT_SECS", 30)?;
let allow_request_from = parse_bool("ALLOW_REQUEST_FROM", false)?;
validate_port("PORT", port)?;
validate_email_if_present("FROM_EMAIL", from_email.as_deref())?;
validate_email_if_present("REPLY_TO", reply_to.as_deref())?;
let tls = parse_tls("TLS")?;
if matches!(tls, SmtpTls::None) {
tracing::warn!(
"SMTP TLS is disabled — credentials and email content will be sent in plaintext"
);
}
Ok(Self {
host,
port,
username: optional("USERNAME")?,
password: optional("PASSWORD")?,
from_email,
from_name: optional("FROM_NAME")?,
reply_to,
tls,
timeout: Duration::from_secs(timeout_secs),
helo_name: optional("HELO_NAME")?,
allow_request_from,
})
}
pub fn has_credentials(&self) -> bool {
self.username.as_deref().is_some_and(|v| !v.is_empty())
&& self.password.as_deref().is_some_and(|v| !v.is_empty())
}
}
impl fmt::Display for SmtpTls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => f.write_str("none"),
Self::StartTls => f.write_str("starttls"),
Self::Tls => f.write_str("tls"),
}
}
}
fn required(name: &'static str) -> Result<String, ConfigError> {
match optional(name)? {
Some(value) => Ok(value),
None => Err(ConfigError::MissingEnv { name }),
}
}
fn optional(name: &'static str) -> Result<Option<String>, ConfigError> {
match env::var(env_name(name)) {
Ok(value) if value.trim().is_empty() => Ok(None),
Ok(value) => Ok(Some(value)),
Err(env::VarError::NotPresent) => Ok(None),
Err(env::VarError::NotUnicode(_)) => Err(invalid(name, "value is not valid UTF-8")),
}
}
fn parse_or<T>(name: &'static str, default: T) -> Result<T, ConfigError>
where
T: std::str::FromStr,
T::Err: fmt::Display,
{
optional(name)?
.map(|value| {
value
.parse::<T>()
.map_err(|err| invalid(name, err.to_string()))
})
.unwrap_or(Ok(default))
}
fn parse_tls(name: &'static str) -> Result<SmtpTls, ConfigError> {
let Some(value) = optional(name)? else {
return Ok(SmtpTls::default());
};
match value.trim().to_ascii_lowercase().as_str() {
"none" | "false" | "0" => Ok(SmtpTls::None),
"starttls" | "start_tls" | "start-tls" => Ok(SmtpTls::StartTls),
"tls" | "ssl" | "smtps" => Ok(SmtpTls::Tls),
_ => Err(invalid(name, "expected one of: none, starttls, tls")),
}
}
fn parse_bool(name: &'static str, default: bool) -> Result<bool, ConfigError> {
let Some(value) = optional(name)? else {
return Ok(default);
};
match value.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "y" | "on" => Ok(true),
"false" | "0" | "no" | "n" | "off" => Ok(false),
_ => Err(invalid(name, "expected a boolean value")),
}
}
fn validate_port(name: &'static str, port: u16) -> Result<(), ConfigError> {
if port == 0 {
return Err(invalid(name, "must be between 1 and 65535"));
}
Ok(())
}
fn validate_email_if_present(name: &'static str, value: Option<&str>) -> Result<(), ConfigError> {
if let Some(value) = value
&& !(value.contains('@') && value.split('@').all(|part| !part.is_empty()))
{
return Err(invalid(name, "must be a valid email address"));
}
Ok(())
}
fn env_name(name: &str) -> String {
format!("{ENV_PREFIX}{name}")
}
fn invalid(name: &'static str, reason: impl Into<String>) -> ConfigError {
ConfigError::InvalidEnv {
name,
reason: reason.into(),
}
}