async-email / async-smtp

Apache License 2.0
60 stars 13 forks source link

Client::is_connected() doesn't report about connections closed by foreign host #37

Closed tyranron closed 1 year ago

tyranron commented 3 years ago

Revelead from https://github.com/amaurymartiny/check-if-email-exists/issues/794

Preamble

Some SMTP servers (like gmx.com and ukr.net, for example) automatically close connection if some SMTP error appears.

We can check this easily via telnet:

❯ telnet mxs.ukr.net 25
Trying 212.42.75.251...
Connected to mxs.ukr.net.
Escape character is '^]'.
220 UKR.NET ESMTP Thu, 07 Jan 2021 18:48:16 +0200
HELO localhost
250 frv71.fwdcdn.com Hello localhost [87.120.35.246]
MAIL FROM: <user@example.org>   
250 OK
RCPT TO: <some@ukr.net>
550 SPF match mandatory for 87.120.35.246 user@example.org
Connection closed by foreign host.

Bug description

In such situations SmtpClient doesn't report about a connection being closed neither with Error::Client("Connection closed"), nor with SmtpClient::is_connected().

But, instead, any next command fails with the io: incomplete error.

Expected behaviour

If SMTP connection is closed automatically by the foreign server, the SmtpClient::is_connected() method should reflect that and subsequent commands should fail witn an Error::Client("Connection closed"), but not io: incomplete.

Steps to reproduce

Paste this program to examples/ directory:

use std::time::Duration;

use async_smtp::{
    smtp::{
        commands::{MailCommand, RcptCommand},
        extension::ClientId,
    },
    ClientSecurity, EmailAddress, SmtpClient,
};

fn main() {
    let result = async_std::task::block_on(async move {
        let mut mailer = SmtpClient::with_security(("mxs.ukr.net", 25), ClientSecurity::None)
            .await
            .map_err(|e| println!("Creation failed: {}", e))?
            .hello_name(ClientId::Domain("mail.example.com".to_owned()))
            .timeout(Some(Duration::new(30, 0))) // Set timeout to 30s
            .into_transport();
        if let Err(e) = mailer.connect().await {
            println!("Connection failed: {}", e);
            mailer
                .close()
                .await
                .map_err(|e| println!("Closing failed: {}", e))?;
        }

        let _ = mailer
            .command(MailCommand::new(
                Some(EmailAddress::new("test@example.com".to_owned()).unwrap()),
                vec![],
            ))
            .await
            .map_err(|e| println!("MAIL FROM failed: {}", e))?;

        let _ = mailer
            .command(RcptCommand::new(
                EmailAddress::new("some@ukr.net".to_owned()).unwrap(),
                vec![],
            ))
            .await
            .map_err(|e| println!("RCPT TO 1 failed: {}", e));

        dbg!(mailer.is_connected());

        mailer
            .command(RcptCommand::new(
                EmailAddress::new("some@ukr.net".to_owned()).unwrap(),
                vec![],
            ))
            .await
            .map_err(|e| println!("RCPT TO 2 failed: {}", e))
    });
    assert!(result.is_ok());
}

And run it:

❯ cargo run --example rcpt
    Finished dev [unoptimized + debuginfo] target(s) in 5.94s
     Running `target/debug/examples/rcpt`
RCPT TO 1 failed: permanent: SPF match mandatory for 87.120.35.246 test@example.com
[examples/rcpt.rs:43] mailer.is_connected() = true
RCPT TO 2 failed: io: incomplete
thread 'main' panicked at 'assertion failed: result.is_ok()', examples/rcpt.rs:53:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
link2xt commented 1 year ago

Since merge of #57 connection setup is not part of the library. Connection failures will result in read/write errors.

It's generally not possible to determine if TCP connection is still established without trying to use it.