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

  1. Retry and inspect server-side logs on the remote SMTP host to learn why the handshake connection was dropped
  2. Test the endpoint manually (openssl s_client -starttls smtp -connect host:port) to confirm it negotiates TLS correctly
  3. Check for firewalls, DPI middleboxes, or MTU problems between client and server that reset TLS handshakes
  4. Update tokio-rustls/rustls so handshake errors carry detailed causes instead of bare io::Errors
  5. 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

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


AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06). Data as JSON: /api/errors/a09e9fa65794af9f. Report an issue: GitHub.