feat(smtp): add a precious SMTP test for admin only
This commit is contained in:
@@ -229,3 +229,55 @@ pub struct VerifyMigrationDto {
|
||||
/// Number of random blobs to sample-check (default: 100).
|
||||
pub sample_size: Option<usize>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SMTP Settings DTOs (Admin Panel)
|
||||
// ============================================================================
|
||||
|
||||
/// Read-only SMTP info shown on the admin SMTP page. SMTP configuration
|
||||
/// is sourced exclusively from environment variables — these fields are
|
||||
/// for display only and any change has to happen by updating the env
|
||||
/// and restarting the server.
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SmtpInfoDto {
|
||||
/// Whether `OXICLOUD_SMTP_HOST` is set and SMTP construction succeeded.
|
||||
pub enabled: bool,
|
||||
/// `OXICLOUD_SMTP_HOST`. Empty string when unset.
|
||||
pub host: String,
|
||||
/// `OXICLOUD_SMTP_PORT`. Default 587.
|
||||
pub port: u16,
|
||||
/// Transport encryption mode: `"starttls"`, `"tls"`, or `"none"`.
|
||||
pub tls: String,
|
||||
/// `OXICLOUD_SMTP_FROM` mailbox. Empty when unset.
|
||||
pub from: String,
|
||||
/// `<set>` if a SASL user is configured, `<anon>` otherwise.
|
||||
/// Never echoes the username — admins compare against the
|
||||
/// runtime config without having to look in `.env`.
|
||||
pub user_state: &'static str,
|
||||
}
|
||||
|
||||
/// Request body for `POST /api/admin/smtp/test`: send a hardcoded
|
||||
/// diagnostic email to the given recipient.
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SendSmtpTestDto {
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
/// Result of a `POST /api/admin/smtp/test` invocation. `success=true`
|
||||
/// carries the SMTP server's response code + first reply line; on
|
||||
/// failure the relevant error message goes in `error`. Always 200 OK
|
||||
/// so the frontend can render both outcomes in one place — the SMTP
|
||||
/// failure is a normal operational state, not an HTTP error.
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SmtpTestResultDto {
|
||||
pub success: bool,
|
||||
/// SMTP status code (e.g. 250). Only set on success.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<u16>,
|
||||
/// First line of the SMTP server's reply. Only set on success.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
/// Human-readable error message. Only set on failure.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -39,6 +39,22 @@ pub struct EmailMessage {
|
||||
pub html_body: Option<String>,
|
||||
}
|
||||
|
||||
/// What the SMTP server said when it accepted the message. Surfaced
|
||||
/// through the trait so the admin "test email" endpoint can show the
|
||||
/// response to operators; the invitation flow generally ignores it but
|
||||
/// logs it via `tracing`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmailSendOutcome {
|
||||
/// SMTP status code from the final response (e.g. `250` for "OK").
|
||||
/// Encoded as a `u16` because that's the natural range; lettre
|
||||
/// returns it as a structured enum and we collapse it here.
|
||||
pub code: u16,
|
||||
/// First line of the server's reply (e.g. `"2.0.0 OK"`, or the
|
||||
/// upstream provider's queue-id banner). Best-effort; if the
|
||||
/// response was empty (unusual) this is the empty string.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Port for sending transactional email.
|
||||
///
|
||||
/// Implementations must:
|
||||
@@ -54,10 +70,12 @@ pub struct EmailMessage {
|
||||
/// `dyn` patterns at the service boundary).
|
||||
#[async_trait]
|
||||
pub trait EmailSender: Send + Sync + 'static {
|
||||
/// Send one message. Returns `Ok(())` only after the SMTP server has
|
||||
/// accepted the message (i.e. after the final `.` or LMTP DATA close).
|
||||
/// Send one message. Returns `Ok(outcome)` only after the SMTP server
|
||||
/// has accepted the message (i.e. after the final `.` or LMTP DATA
|
||||
/// close). The outcome carries the SMTP response code + first line
|
||||
/// so diagnostic surfaces (admin "test email" page) can show it.
|
||||
/// Caller may run this fire-and-forget via `tokio::spawn` if response
|
||||
/// timing matters (e.g. magic-link invite path defending against
|
||||
/// enumeration via latency).
|
||||
async fn send(&self, message: EmailMessage) -> Result<(), DomainError>;
|
||||
/// enumeration via latency); the outcome is then logged-only.
|
||||
async fn send(&self, message: EmailMessage) -> Result<EmailSendOutcome, DomainError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user