1use axum::{
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7 Json,
8};
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12use std::sync::Arc;
13
14use cloudillo_core::extract::Auth;
15use cloudillo_email::{EmailMessage, EmailModule};
16use cloudillo_types::types::{ApiResponse, ErrorResponse};
17
18use crate::prelude::*;
19
20#[derive(Debug, Clone, Deserialize)]
22pub struct TestEmailRequest {
23 pub to: String,
25}
26
27#[skip_serializing_none]
29#[derive(Debug, Clone, Serialize)]
30pub struct TestEmailResponse {
31 pub success: bool,
33 pub message: String,
35 pub error: Option<String>,
37}
38
39#[axum::debug_handler]
44pub async fn send_test_email(
45 State(app): State<App>,
46 Auth(auth_ctx): Auth,
47 Json(request): Json<TestEmailRequest>,
48) -> ClResult<Response> {
49 let tn_id = auth_ctx.tn_id;
50
51 info!(
52 tn_id = ?tn_id,
53 to = %request.to,
54 "POST /api/admin/email/test - Sending test email"
55 );
56
57 if !request.to.contains('@') || !request.to.contains('.') {
59 return Ok((
60 StatusCode::BAD_REQUEST,
61 Json(ErrorResponse::new(
62 "INVALID_EMAIL_FORMAT".to_string(),
63 "Invalid email address format. Email address must contain @ and .".to_string(),
64 )),
65 )
66 .into_response());
67 }
68
69 let email_enabled = app.settings.get_bool(tn_id, "email.enabled").await.unwrap_or(false);
71 if !email_enabled {
72 return Ok((
73 StatusCode::PRECONDITION_FAILED,
74 Json(ErrorResponse::new(
75 "EMAIL_DISABLED".to_string(),
76 "Email sending is disabled. Enable email.enabled setting to send emails."
77 .to_string(),
78 )),
79 )
80 .into_response());
81 }
82
83 let smtp_host = app.settings.get_string_opt(tn_id, "email.smtp.host").await.unwrap_or(None);
85 if smtp_host.is_none() || smtp_host.as_ref().is_some_and(|h| h.is_empty()) {
86 return Ok((
87 StatusCode::PRECONDITION_FAILED,
88 Json(ErrorResponse::new(
89 "SMTP_NOT_CONFIGURED".to_string(),
90 "SMTP host not configured. Configure email.smtp.host setting.".to_string(),
91 )),
92 )
93 .into_response());
94 }
95
96 let base_id_tag = app.opts.base_id_tag.as_ref().map(|s| s.as_ref()).unwrap_or("cloudillo");
98
99 let message = EmailMessage {
101 to: request.to.clone(),
102 subject: format!("Test Email from Cloudillo ({})", base_id_tag),
103 text_body: format!(
104 "This is a test email from your Cloudillo instance ({}).\n\n\
105 If you received this email, your email configuration is working correctly.\n\n\
106 -- Cloudillo",
107 base_id_tag
108 ),
109 html_body: Some(format!(
110 "<html><body>\
111 <h2>Test Email from Cloudillo</h2>\
112 <p>This is a test email from your Cloudillo instance (<strong>{}</strong>).</p>\
113 <p>If you received this email, your email configuration is working correctly.</p>\
114 <hr>\
115 <p style=\"color: #666;\">-- Cloudillo</p>\
116 </body></html>",
117 base_id_tag
118 )),
119 from_name_override: Some(format!("Cloudillo | {}", base_id_tag.to_uppercase())),
120 };
121
122 match app.ext::<Arc<EmailModule>>()?.send_now(tn_id, message).await {
124 Ok(()) => {
125 info!(
126 tn_id = ?tn_id,
127 to = %request.to,
128 "Test email sent successfully"
129 );
130 Ok((
131 StatusCode::OK,
132 Json(ApiResponse::new(TestEmailResponse {
133 success: true,
134 message: format!("Test email sent to {}", request.to),
135 error: None,
136 })),
137 )
138 .into_response())
139 }
140 Err(e) => {
141 warn!(
142 tn_id = ?tn_id,
143 to = %request.to,
144 error = %e,
145 "Failed to send test email"
146 );
147 Ok((
148 StatusCode::SERVICE_UNAVAILABLE,
149 Json(ErrorResponse::new(
150 "SMTP_SEND_FAILED".to_string(),
151 format!("Failed to send test email: {}", e),
152 )),
153 )
154 .into_response())
155 }
156 }
157}
158
159