Skip to content

Select on INBOX fails when parsing response from surgemail server #123

@cry-inc

Description

@cry-inc

Hi, I used your awesome library to implement a DMARC report viewer with an embedded IMAP client.

One user reported an error while trying to selecting the inbox. I was able to reproduce the issue by running some minimal example code with a test instance of the mail server software called surgemail.

I am not very familiar with the IMAP protocol, but it seems that the server either returns something invalid or there is a bug with the response parsing?

The exchange looks like this:

> * OK IMAP (C) win11vm (Version 8.0e-1)
< A0001
<
< LOGIN "tester" "password"
<
> A0001 OK login completed
< A0002
<
< SELECT "INBOX"
<
> * 1 EXISTS
> * 0 RECENT
> * OK [UNSEEN 1] first unseen message
> * OK [UIDVALIDITY 1754171061] Uid epoch
> * OK [UIDNEXT 2] Predicted next uid
> * FLAGS (\Answered \Flagged \Deleted \Draft \Seen $Forwarded )
> * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Draft \Seen $Forwarded )] Limited
> A0002 OK [READ-WRITE] SELECT completed

thread 'main' panicked at src\main.rs:23:44:
called `Result::unwrap()` on an `Err` value: Io(Custom { kind: Other, error: "Error(Error { input: [42, 32, 70, 76, 65, 71, 83, 32, 40, 92, 65, 110, 115, 119, 101, 114, 101, 100, 32, 92, 70, 108, 97, 103, 103, 101, 100, 32, 92, 68, 101, 108, 101, 116, 101, 100, 32, 92, 68, 114, 97, 102, 116, 32, 92, 83, 101, 101, 110, 32, 36, 70, 111, 114, 119, 97, 114, 100, 101, 100, 32, 41, 13, 10, 42, 32, 79, 75, 32, 91, 80, 69, 82, 77, 65, 78, 69, 78, 84, 70, 76, 65, 71, 83, 32, 40, 92, 65, 110, 115, 119, 101, 114, 101, 100, 32, 92, 70, 108, 97, 103, 103, 101, 100, 32, 92, 68, 101, 108, 101, 116, 101, 100, 32, 92, 68, 114, 97, 102, 116, 32, 92, 83, 101, 101, 110, 32, 36, 70, 111, 114, 119, 97, 114, 100, 101, 100, 32, 41, 93, 32, 76, 105, 109, 105, 116, 101, 100, 13, 10, 65, 48, 48, 48, 50, 32, 79, 75, 32, 91, 82, 69, 65, 68, 45, 87, 82, 73, 84, 69, 93, 32, 83, 69, 76, 69, 67, 84, 32, 99, 111, 109, 112, 108, 101, 116, 101, 100, 13, 10], code: TakeWhile1 }) during parsing of \"* FLAGS (\\\\Answered \\\\Flagged \\\\Deleted \\\\Draft \\\\Seen $Forwarded )\\r\\n* OK [PERMANENTFLAGS (\\\\Answered \\\\Flagged \\\\Deleted \\\\Draft \\\\Seen $Forwarded )] Limited\\r\\nA0002 OK [READ-WRITE] SELECT completed\\r\\n\"" })

This is the code I used to reproduce the problem:

main.rs

use async_imap::Client;
use async_std::net::TcpStream;
use async_std::pin::Pin;
use async_std::task;
use async_std::task::{Context, Poll};
use futures::io::{AsyncRead, AsyncWrite};
use std::fmt;

fn main() {
    task::block_on(async {
        let host = "127.0.0.1";
        let port = 143;
        let user = "tester";
        let pw = "password";
        let mailbox = "INBOX";

        let stream = TcpStream::connect((host, port)).await.unwrap();
        let proxied = LogProxy::new(stream);

        let mut client = Client::new(proxied);
        client.read_response().await.unwrap().unwrap();
        let mut imap_session = client.login(user, pw).await.unwrap();
        imap_session.select(mailbox).await.unwrap();
        imap_session.logout().await.unwrap();
    });
}

#[derive(Debug)]
struct LogProxy<T: AsyncRead + AsyncWrite + Unpin + fmt::Debug> {
    transport: T,
}

impl<T: AsyncRead + AsyncWrite + Unpin + fmt::Debug> AsyncRead for LogProxy<T> {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context,
        buf: &mut [u8],
    ) -> Poll<std::io::Result<usize>> {
        let ret = AsyncRead::poll_read(Pin::new(&mut self.transport), cx, buf);
        if let Poll::Ready(Ok(num)) = ret {
            println!("> {}", String::from_utf8_lossy(&buf[0..num]));
        }
        ret
    }
}

impl<T: AsyncRead + AsyncWrite + Unpin + fmt::Debug> AsyncWrite for LogProxy<T> {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context,
        buf: &[u8],
    ) -> Poll<std::io::Result<usize>> {
        let ret = AsyncWrite::poll_write(Pin::new(&mut self.transport), cx, buf);
        if let Poll::Ready(Ok(num)) = ret {
            println!("< {}", String::from_utf8_lossy(&buf[0..num]));
        }
        ret
    }

    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::io::Result<()>> {
        AsyncWrite::poll_flush(Pin::new(&mut self.transport), cx)
    }

    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::io::Result<()>> {
        AsyncWrite::poll_close(Pin::new(&mut self.transport), cx)
    }
}

impl<T: AsyncRead + AsyncWrite + Unpin + fmt::Debug> LogProxy<T> {
    pub fn new(transport: T) -> Self {
        Self { transport }
    }
}

Cargo.toml

[package]
name = "repro"
version = "0.1.0"
edition = "2024"

[dependencies]
async-imap = { version = "0.11", features = ["runtime-async-std"] }
async-std = { version = "1", features = ["std", "attributes"] }
futures = "0.3"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions