From 10119033a33d5865e471744b6c3a1a6b1f636e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jagoda=20Estera=20=C5=9Al=C4=85zak?= <128227338+j-g00da@users.noreply.github.com> Date: Thu, 7 May 2026 14:20:49 +0200 Subject: [PATCH] fix(smtp-client): Handle 421 on reused connection (#145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a reused connection was closed in the meantime, open a new connection, instead of failing immediately. Fixes #143 Signed-off-by: Jagoda Ślązak --- filtermail/src/smtp_client.rs | 36 +++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/filtermail/src/smtp_client.rs b/filtermail/src/smtp_client.rs index b35c1b4b..be012554 100644 --- a/filtermail/src/smtp_client.rs +++ b/filtermail/src/smtp_client.rs @@ -216,7 +216,7 @@ pub async fn send( } (stream, true) } else { - let stream = establish_tcp_connection(address, port, dns_resolver).await?; + let stream = establish_tcp_connection(address, port, dns_resolver.clone()).await?; log::debug!("Successfully connected to {}", stream.peer_addr()?); (BufStream::new(SmtpStream::plain(stream)), false) }; @@ -232,7 +232,7 @@ pub async fn send( } macro_rules! smtp_read { - ($context:expr, $expected_code:expr) => { + ($context:expr) => { response.clear(); let mut next_line = String::new(); buf_stream.read_line(&mut next_line).await?; @@ -245,6 +245,15 @@ pub async fn send( response.push_str(&next_line); } log::trace!("SMTP response for {}:\n{}", $context, response); + }; + ($context:expr, $expected_code:expr) => { + smtp_read!($context); + smtp_expect!($context, $expected_code); + }; + } + + macro_rules! smtp_expect { + ($context:expr, $expected_code:expr) => { if !response.starts_with($expected_code) { return Err(crate::error::Error::MailSend { context: $context.to_string(), @@ -261,9 +270,28 @@ pub async fn send( }; } - if reused { - smtp_cmd!(b"RSET\r\n", "RSET on reused connection", "250"); + // RSET reused connection or fallback to a new connection + let reused = if reused { + smtp_write!(b"RSET\r\n"); + smtp_read!("RSET"); + // We don't want to defer if the connection was closed already by the server. + // This is a special case where we end up reading message sent before we sent RSET. + // e.g.: 421 example.org Service closing transmission channel - command timeout + if response.starts_with("421") { + log::debug!("Reused connection is dead; establishing new connection..."); + let stream = establish_tcp_connection(address, port, dns_resolver).await?; + log::debug!("Successfully connected to {}", stream.peer_addr()?); + buf_stream = BufStream::new(SmtpStream::plain(stream)); + false + } else { + smtp_expect!("RSET", "250"); + true + } } else { + false + }; + + if !reused { // Read initial greeting smtp_read!("initial greeting", "220");