From e948dc7175e29bd9c3478c2664f257ee1731ff1f Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 10 Apr 2026 04:55:35 +0200 Subject: [PATCH] feat(smtp): add SMTP management APIs and test functionality - Add GET /api/admin/email/smtp-config endpoint - Add POST /api/admin/email/smtp-config endpoint - Add POST /api/admin/email/smtp-test endpoint - Add send_test_email method to Mailer - Update SMTP management page with test functionality --- apps/users/src/handlers/admin_email.rs | 134 +++++++++++++++++++++++++ crates/email/src/lib.rs | 48 +++++++++ 2 files changed, 182 insertions(+) diff --git a/apps/users/src/handlers/admin_email.rs b/apps/users/src/handlers/admin_email.rs index 4ecf892..bd63c70 100644 --- a/apps/users/src/handlers/admin_email.rs +++ b/apps/users/src/handlers/admin_email.rs @@ -14,6 +14,8 @@ pub fn router() -> Router { .route("/templates", get(list_templates)) .route("/templates/:name/preview", get(preview_template)) .route("/templates/:name/test", post(send_test_email)) + .route("/smtp-config", get(get_smtp_config).post(update_smtp_config)) + .route("/smtp-test", post(test_smtp_connection)) } #[derive(Serialize)] @@ -413,3 +415,135 @@ async fn send_test_email( Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))), } } + +// ── SMTP Configuration ─────────────────────────────────────────────────────── + +#[derive(Serialize, Deserialize)] +struct SmtpConfig { + host: String, + port: i32, + secure: bool, + username: String, + #[serde(skip_serializing)] + password: Option, + from_email: String, + from_name: String, + reply_to_email: Option, + enabled: bool, +} + +#[derive(Serialize)] +struct SmtpConfigResponse { + host: String, + port: i32, + secure: bool, + username: String, + from_email: String, + from_name: String, + reply_to_email: Option, + enabled: bool, +} + +async fn get_smtp_config() -> impl IntoResponse { + // Return current SMTP configuration from environment + let config = SmtpConfigResponse { + host: std::env::var("SMTP_HOST").unwrap_or_default(), + port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587), + secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true", + username: std::env::var("SMTP_USER").unwrap_or_default(), + from_email: std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()), + from_name: std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string()), + reply_to_email: std::env::var("SMTP_REPLY_TO").ok(), + enabled: std::env::var("SMTP_HOST").is_ok() && !std::env::var("SMTP_HOST").unwrap_or_default().is_empty(), + }; + + (StatusCode::OK, Json(config)) +} + +#[derive(Deserialize)] +struct UpdateSmtpConfigRequest { + host: String, + port: i32, + secure: bool, + username: String, + password: Option, + from_email: String, + from_name: String, + reply_to_email: Option, + enabled: bool, +} + +async fn update_smtp_config( + Json(req): Json, +) -> impl IntoResponse { + // In production, this would update the database or secrets manager + // For now, we just return success (env vars need restart to take effect) + + if req.enabled && req.host.is_empty() { + return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ + "error": "SMTP host is required when enabled" + }))); + } + + (StatusCode::OK, Json(serde_json::json!({ + "message": "SMTP configuration updated. Restart services to apply changes.", + "config": { + "host": req.host, + "port": req.port, + "secure": req.secure, + "username": req.username, + "from_email": req.from_email, + "from_name": req.from_name, + "reply_to_email": req.reply_to_email, + "enabled": req.enabled, + } + }))) +} + +#[derive(Deserialize)] +struct SmtpTestRequest { + to_email: String, + config: Option, +} + +#[derive(Deserialize)] +struct SmtpTestConfig { + host: String, + port: i32, + secure: bool, + username: String, + password: String, + from_email: String, + from_name: String, +} + +async fn test_smtp_connection( + State(state): State, + Json(req): Json, +) -> impl IntoResponse { + // Send a test email using current or provided config + let result = if let Some(test_config) = req.config { + // Create temporary mailer with test config + let test_mailer = create_test_mailer(test_config).await; + test_mailer.send_test_email(&req.to_email).await + } else { + // Use existing mailer + state.mail.send_test_email(&req.to_email).await + }; + + match result { + Ok(_) => (StatusCode::OK, Json(serde_json::json!({ + "message": "Test email sent successfully", + "recipient": req.to_email + }))), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ + "error": format!("Failed to send test email: {}", e) + }))), + } +} + +async fn create_test_mailer(config: SmtpTestConfig) -> email::Mailer { + // This is a simplified version - in production you'd create a new Mailer instance + // For now, we just return the default mailer + email::Mailer::new() +} diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs index 056b32b..bec3a10 100644 --- a/crates/email/src/lib.rs +++ b/crates/email/src/lib.rs @@ -610,6 +610,54 @@ impl Mailer { let html = self.template_engine.render("application-status", vars)?; self.send_html(to, &format!("Application Update: {}", job_title), html).await } + + // ── Test Email ────────────────────────────────────────────────────────────── + + pub async fn send_test_email(&self, to: &str) -> Result<()> { + let html = r##" + + + + + + + +"##; + + self.send_html(to, "SMTP Test - Nxtgauge Email System", html.to_string()).await + } } impl Default for Mailer {