feat(mailer-smtp): add SCRAM-SHA-256 auth, Ed25519 DKIM, opportunistic TLS, SNI cert selection, pipelining and delivery/bridge improvements

This commit is contained in:
2026-02-11 10:11:43 +00:00
parent 7908cbaefa
commit b10597fd5e
28 changed files with 1849 additions and 153 deletions

View File

@@ -116,6 +116,7 @@ impl ConnectionPool {
&self.config.host,
self.config.port,
self.config.connection_timeout_secs,
self.config.tls_opportunistic,
)
.await?
} else {
@@ -139,7 +140,7 @@ impl ConnectionPool {
if !self.config.secure && capabilities.starttls {
protocol::send_starttls(&mut stream, self.config.socket_timeout_secs).await?;
stream =
super::connection::upgrade_to_tls(stream, &self.config.host).await?;
super::connection::upgrade_to_tls(stream, &self.config.host, self.config.tls_opportunistic).await?;
// Re-EHLO after STARTTLS — use updated capabilities for auth
capabilities = protocol::send_ehlo(
@@ -244,9 +245,10 @@ impl SmtpClientManager {
protocol::send_rset(&mut conn.stream, config.socket_timeout_secs).await?;
}
// Perform the SMTP transaction
// Perform the SMTP transaction (use pipelining if server supports it)
let pipelining = conn.capabilities.pipelining;
let result =
Self::perform_send(&mut conn.stream, sender, recipients, message, config).await;
Self::perform_send(&mut conn.stream, sender, recipients, message, config, pipelining).await;
// Re-acquire the pool lock and release the connection
let mut pool = pool_arc.lock().await;
@@ -268,30 +270,39 @@ impl SmtpClientManager {
recipients: &[String],
message: &[u8],
config: &SmtpClientConfig,
pipelining: bool,
) -> Result<SmtpSendResult, SmtpClientError> {
let timeout_secs = config.socket_timeout_secs;
// MAIL FROM
protocol::send_mail_from(stream, sender, timeout_secs).await?;
let (accepted, rejected) = if pipelining {
// Use pipelined envelope: MAIL FROM + all RCPT TO in one batch
let (_mail_ok, acc, rej) = protocol::send_pipelined_envelope(
stream, sender, recipients, timeout_secs,
).await?;
(acc, rej)
} else {
// Sequential: MAIL FROM, then each RCPT TO
protocol::send_mail_from(stream, sender, timeout_secs).await?;
// RCPT TO for each recipient
let mut accepted = Vec::new();
let mut rejected = Vec::new();
let mut accepted = Vec::new();
let mut rejected = Vec::new();
for rcpt in recipients {
match protocol::send_rcpt_to(stream, rcpt, timeout_secs).await {
Ok(resp) => {
if resp.is_success() {
accepted.push(rcpt.clone());
} else {
for rcpt in recipients {
match protocol::send_rcpt_to(stream, rcpt, timeout_secs).await {
Ok(resp) => {
if resp.is_success() {
accepted.push(rcpt.clone());
} else {
rejected.push(rcpt.clone());
}
}
Err(_) => {
rejected.push(rcpt.clone());
}
}
Err(_) => {
rejected.push(rcpt.clone());
}
}
}
(accepted, rejected)
};
// If no recipients were accepted, fail
if accepted.is_empty() {
@@ -339,6 +350,7 @@ impl SmtpClientManager {
&config.host,
config.port,
config.connection_timeout_secs,
config.tls_opportunistic,
)
.await?
} else {