BREAKING CHANGE(smtp-client): Replace the legacy TypeScript SMTP client with a new Rust-based SMTP client and IPC bridge for outbound delivery

This commit is contained in:
2026-02-11 07:17:05 +00:00
parent fc4877e06b
commit 27bab5f345
50 changed files with 2268 additions and 6737 deletions

View File

@@ -0,0 +1,160 @@
//! SMTP client error types.
use serde::Serialize;
/// Errors that can occur during SMTP client operations.
#[derive(Debug, thiserror::Error, Serialize)]
pub enum SmtpClientError {
#[error("Connection error: {message}")]
ConnectionError { message: String },
#[error("Timeout: {message}")]
TimeoutError { message: String },
#[error("TLS error: {message}")]
TlsError { message: String },
#[error("Authentication failed: {message}")]
AuthenticationError { message: String },
#[error("Protocol error ({code}): {message}")]
ProtocolError { code: u16, message: String },
#[error("Pool exhausted: {message}")]
PoolExhausted { message: String },
#[error("Invalid configuration: {message}")]
ConfigError { message: String },
}
impl SmtpClientError {
/// Whether this error is retryable (temporary failure).
/// Permanent failures (5xx, auth failures) are not retryable.
pub fn is_retryable(&self) -> bool {
match self {
SmtpClientError::ConnectionError { .. } => true,
SmtpClientError::TimeoutError { .. } => true,
SmtpClientError::TlsError { .. } => false,
SmtpClientError::AuthenticationError { .. } => false,
SmtpClientError::ProtocolError { code, .. } => *code >= 400 && *code < 500,
SmtpClientError::PoolExhausted { .. } => true,
SmtpClientError::ConfigError { .. } => false,
}
}
/// The error type as a string for IPC serialization.
pub fn error_type(&self) -> &'static str {
match self {
SmtpClientError::ConnectionError { .. } => "connection",
SmtpClientError::TimeoutError { .. } => "timeout",
SmtpClientError::TlsError { .. } => "tls",
SmtpClientError::AuthenticationError { .. } => "authentication",
SmtpClientError::ProtocolError { .. } => "protocol",
SmtpClientError::PoolExhausted { .. } => "pool_exhausted",
SmtpClientError::ConfigError { .. } => "config",
}
}
/// The SMTP code if this is a protocol error.
pub fn smtp_code(&self) -> Option<u16> {
match self {
SmtpClientError::ProtocolError { code, .. } => Some(*code),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retryable_errors() {
assert!(SmtpClientError::ConnectionError {
message: "refused".into()
}
.is_retryable());
assert!(SmtpClientError::TimeoutError {
message: "timed out".into()
}
.is_retryable());
assert!(SmtpClientError::PoolExhausted {
message: "full".into()
}
.is_retryable());
assert!(SmtpClientError::ProtocolError {
code: 421,
message: "try later".into()
}
.is_retryable());
assert!(SmtpClientError::ProtocolError {
code: 450,
message: "mailbox busy".into()
}
.is_retryable());
}
#[test]
fn test_non_retryable_errors() {
assert!(!SmtpClientError::AuthenticationError {
message: "bad creds".into()
}
.is_retryable());
assert!(!SmtpClientError::TlsError {
message: "cert invalid".into()
}
.is_retryable());
assert!(!SmtpClientError::ProtocolError {
code: 550,
message: "no such user".into()
}
.is_retryable());
assert!(!SmtpClientError::ProtocolError {
code: 554,
message: "rejected".into()
}
.is_retryable());
assert!(!SmtpClientError::ConfigError {
message: "bad config".into()
}
.is_retryable());
}
#[test]
fn test_error_type_strings() {
assert_eq!(
SmtpClientError::ConnectionError {
message: "x".into()
}
.error_type(),
"connection"
);
assert_eq!(
SmtpClientError::ProtocolError {
code: 550,
message: "x".into()
}
.error_type(),
"protocol"
);
}
#[test]
fn test_smtp_code() {
assert_eq!(
SmtpClientError::ProtocolError {
code: 550,
message: "x".into()
}
.smtp_code(),
Some(550)
);
assert_eq!(
SmtpClientError::ConnectionError {
message: "x".into()
}
.smtp_code(),
None
);
}
}