stalwartlabs/stalwart · error · io::Error
Unspecified
Error message
Unspecified
What it means
This error occurs when the SMTP client fails to upgrade a plain TCP connection to TLS via STARTTLS, but the underlying io::Error carries no inner error payload. The library falls back to wrapping the io::ErrorKind with the literal message 'Unspecified', so the actual TLS handshake failure cause is unknown. It is produced by SmtpClient::into_tls when tls_connector.connect() fails without a nested error.
Source
Thrown at crates/smtp/src/outbound/client.rs:535
tokio::time::timeout(self.timeout, async {
Ok(SmtpClient {
stream: tls_connector
.connect(
ServerName::try_from(hostname)
.map_err(|_| ClientError::InvalidTLSName)?
.to_owned(),
self.stream,
)
.await
.map_err(|err| {
let kind = err.kind();
if let Some(inner) = err.into_inner() {
match inner.downcast::<rustls::Error>() {
Ok(error) => ClientError::Tls(error),
Err(error) => ClientError::Io(std::io::Error::new(kind, error)),
}
} else {
ClientError::Io(std::io::Error::new(kind, "Unspecified"))
}
})?,
timeout: self.timeout,
session_id: self.session_id,
})
})
.await
.map_err(|_| ClientError::Timeout)?
}
}
impl SmtpClient<TcpStream> {
/// Connects to a remote host address
pub async fn connect(
remote_addr: SocketAddr,
timeout: Duration,
session_id: u64,
) -> ClientResult<Self> {View on GitHub (pinned to e962003857)
Solutions
- Retry and inspect server-side logs on the remote SMTP host to learn why the handshake connection was dropped
- Test the endpoint manually (openssl s_client -starttls smtp -connect host:port) to confirm it negotiates TLS correctly
- Check for firewalls, DPI middleboxes, or MTU problems between client and server that reset TLS handshakes
- Update tokio-rustls/rustls so handshake errors carry detailed causes instead of bare io::Errors
- Handle ClientError::Io with ConnectionAborted/UnexpectedEof kinds by skipping to the next MX host or falling back to plaintext delivery
Example fix
// before
match client.starttls(&tls_connector, hostname).await {
Ok(c) => ...,
Err(e) => panic!("{:?}", e),
}
// after
match client.starttls(&tls_connector, hostname).await {
Ok(c) => ...,
Err(ClientError::Io(e))
if matches!(e.kind(), std::io::ErrorKind::ConnectionAborted | std::io::ErrorKind::UnexpectedEof) =>
{
tracing::warn!("TLS handshake dropped by peer; trying next MX host");
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
// before dialing, confirm the host supports STARTTLS
let caps = smtp_client.capabilities().await?;
if !caps.supports_starttls() {
// skip TLS upgrade or choose another MX host
} Type guard
fn is_tls_handshake_dropped(err: &ClientError) -> bool {
matches!(err, ClientError::Io(e) if matches!(e.kind(),
std::io::ErrorKind::ConnectionAborted | std::io::ErrorKind::UnexpectedEof))
} Try / catch
match client.starttls(&tls_connector, hostname).await {
Ok(c) => c,
Err(e) if is_tls_handshake_dropped(&e) => {
tracing::warn!("peer dropped TLS handshake; failing over");
try_next_mx_host()? // or fall back to plaintext relay
}
Err(e) => return Err(e),
} Prevention
- Verify destination servers support STARTTLS before requiring TLS (test with openssl s_client)
- Check network path for firewalls/DPI devices that reset TLS handshakes
- Keep tokio-rustls/rustls updated so errors carry detailed causes
- Implement per-host failover so one broken relay does not stop delivery
- Monitor logs for repeated 'Unspecified' errors from the same peer and investigate server-side
When it happens
Trigger: Calling SmtpClient::starttls or SmtpClient::into_tls where tokio-rustls' TlsConnector::connect() returns an io::Error whose into_inner() is None — typically the peer drops/resets the TCP connection mid-handshake, or the OS reports a socket error without a nested cause.
Common situations: Remote SMTP server crashes or closes the connection right after accepting STARTTLS; a firewall or DPI middlebox RSTs the TLS handshake; connecting to a port whose TLS stack is broken; MTU/fragmentation issues killing large handshake records.
Related errors
- unwrap_tls called on non-TLS acceptor
- StoreEvent::HttpStoreError
- OpenAPI request failed
- Cluster::PublisherError
- Cluster::PublisherError
AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06).
Data as JSON: /api/errors/a09e9fa65794af9f.
Report an issue: GitHub.