jonhoo / rust-imap

IMAP client library for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

imap::connect timeout

wookietreiber opened this issue · comments

I have a specific server with a long connection time and would like to configure a timeout for that, specifically for the imap::connect function.

From looking at the documentation and source code, my current understanding is that imap::connect uses TcpStream::connect. There is also an alternative TcpStream::connect_timeout. Would it be useful to either add a timeout function argument to imap::connect or to mimic TcpStream and an imap::connect_timeout alternative?

Hmm, it's a good question. My first instinct here is that if you want to set particular settings for the TCP connection (whether that be connection timeout or something else), you should prefer using Client::new, which lets you establish the TcpStream yourself, and then just give the established stream to imap. Would that work for your use-case?

Thanks for the tip about Client::new! This has been quite an interesting journey.

At first I was a bit annoyed that I had to handle the socket addresses myself, because TcpStream::connect_timeout doesn't take ToSocketAddrs like connect does but a single SocketAddr. Then I found out that it's the IPv6 address that keeps blocking, the IPv4 address works just fine. That's probably because my network isn't set up correctly for IPv6. Having to actually handle the addresses myself helped me to figure that out (see also note below).

What I have now is this for the single-address timeout function:

fn connect_timeout<S: AsRef<str>>(
    addr: &SocketAddr,
    domain: S,
    ssl_connector: &TlsConnector,
    timeout: Duration,
) -> Result<Client<TlsStream<TcpStream>>> {
    let tcp_stream = TcpStream::connect_timeout(addr, timeout)
        .with_context(|| format!("connecting tcp stream to {}", addr))?;

    let tls_stream =
        TlsConnector::connect(ssl_connector, domain.as_ref(), tcp_stream)
            .with_context(|| format!("tls handshake with {}", addr))?;

    let mut client = Client::new(tls_stream);
    client
        .read_greeting()
        .with_context(|| format!("tls greeting from {}", addr))?;

    Ok(client)
}

I'm using this to have a convenience function to try all addresses from the DNS resolution:

fn connect_all_timeout<A: ToSocketAddrs + fmt::Debug, S: AsRef<str>>(
    addr: A,
    domain: S,
    ssl_connector: &TlsConnector,
    timeout: Duration,
) -> Result<Client<TlsStream<TcpStream>>> {
    let addrs = addr
        .to_socket_addrs()
        .with_context(|| format!("unable to resolve {:?}", addr))?;

    let mut errors = vec![];

    for addr in addrs {
        match connect_timeout(&addr, &domain, ssl_connector, timeout) {
            Ok(client) => return Ok(client),
            Err(error) => errors.push(error),
        }
    }

    Err(anyhow!("all addresses failed to connect: {:#?}", errors))
}

Note: I'm using anyhow for the detailed error messages. This was how I was able to figure out that it's the IPv6 address that doesn't work.

@jonhoo: Would you like me to PR any of these?

Haha, yeah, I can imagine that was quite a journey! I'm glad you figured it out.

I think I'd like to keep both of those outside of imap — they are somewhat specialized, pretty easy to model with Client::new, and once we add that, there are also other configurations we may have to add, like more advanced TLS connection options, TCP read/write timeouts, reuse port/addr options, TCP nodelay, etc. I'd prefer we keep it to Client::new, and leave such configuration to the user.

I do think that mentioning Client::read_greeting in the documentation for Client::new would be a good thing to add though! Arguably, Client::new should just do it for you — I'm not sure why it doesn't at the moment. But that'd be a backwards incompatible change. I think for now, if you could submit a PR that adds a mention of read_greeting to Client::new, that'd be great!

I am using Client::new in my fn connect_timeout. I adapted the code from imap::connect to use TcpStream::connect_timeout instead of TcpStream::connect. The remaining diff is from using anyhow.

I'm wondering how to reference read_greeting. It is defined on Connection which is not a completely public struct, i.e. I can find neither Connection nor read_greeting in the docs. I'm wondering how I'm even able to use it.

@wookietreiber The methods from Connection are usable on both Client and Session due to the Deref impl, and the implicit Deref coercions; basically the compiler inserts .deref() or .deref_mut() calls where necessary.

@dario23 Thanks, that explains half of my questions :) I'm still wondering why I'm not seeing it in the docs.

@wookietreiber Ah, that's because the Connection type isn't public — the Deref is there primarily so that we can conveniently call methods on Connection through Client internally, but just so happens to enable this particular workaround. This is the other reason why I'd ideally like for read_greeting to never be something the user has to call..