IMAP: Fix fetch responses (closes #2940) (credits to @markstos)
This commit is contained in:
@@ -138,9 +138,12 @@ pub fn test() {
|
||||
sections: sections.clone(),
|
||||
offset: None,
|
||||
contents: match contents {
|
||||
BodyContents::Bytes(_) => {
|
||||
BodyContents::Text("[binary content]".into())
|
||||
}
|
||||
BodyContents::Bytes(bytes) => BodyContents::Text(
|
||||
std::str::from_utf8(bytes.as_ref())
|
||||
.unwrap_or("[binary content]")
|
||||
.to_string()
|
||||
.into(),
|
||||
),
|
||||
text => text,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,18 +71,22 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("BINARY[1] {175}")
|
||||
.assert_contains("BINARY[1] ~{175}")
|
||||
.assert_contains("BINARY.SIZE[1] 175")
|
||||
.assert_contains("BODY[1.TEXT] {239}")
|
||||
.assert_contains("BODY[2.1.HEADER] {88}")
|
||||
.assert_contains("BINARY[2.1] {101}")
|
||||
.assert_contains("BINARY[2.1] ~{108}")
|
||||
.assert_contains("BODY[MIME] {54}")
|
||||
.assert_contains("BODY[HEADER.FIELDS (FROM)]<10> {8}")
|
||||
.assert_contains("“exporting”")
|
||||
.assert_contains("PGh0bWw+PHA+")
|
||||
.assert_contains("Content-Transfer-Encoding: quoted-printable")
|
||||
.assert_contains("ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨")
|
||||
.assert_contains("Vandelay");
|
||||
let fraktur_utf16_le: Vec<u8> = "ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨"
|
||||
.encode_utf16()
|
||||
.flat_map(|c| c.to_le_bytes())
|
||||
.collect();
|
||||
imap.assert_last_contains_bytes(&fraktur_utf16_le);
|
||||
|
||||
// We are in EXAMINE mode, fetching body should not set \Seen
|
||||
imap.send("UID FETCH 10 (FLAGS)").await;
|
||||
@@ -99,7 +103,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("BINARY[1] {175}")
|
||||
.assert_contains("BINARY[1] ~{175}")
|
||||
.assert_contains("BINARY.SIZE[1] 175")
|
||||
.assert_contains("BODY[1.TEXT] {239}");
|
||||
|
||||
|
||||
@@ -61,6 +61,17 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, te
|
||||
.await
|
||||
.assert_equals("* SEARCH 10");
|
||||
|
||||
imap_check
|
||||
.send(concat!(
|
||||
"UID SEARCH CHARSET UTF-8 TEXT {75+}\r\n",
|
||||
"ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨"
|
||||
))
|
||||
.await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* SEARCH 10");
|
||||
|
||||
imap_check
|
||||
.send("UID SEARCH NOT (FROM nathaniel ANSWERED)")
|
||||
.await;
|
||||
|
||||
@@ -8,14 +8,33 @@ use base64::{Engine, engine::general_purpose};
|
||||
use imap_proto::ResponseType;
|
||||
use std::time::Duration;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf},
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
pub struct ImapConnection {
|
||||
tag: &'static [u8],
|
||||
reader: Lines<BufReader<ReadHalf<TcpStream>>>,
|
||||
reader: BufReader<ReadHalf<TcpStream>>,
|
||||
writer: WriteHalf<TcpStream>,
|
||||
last_raw: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn read_lossy_line(
|
||||
reader: &mut BufReader<ReadHalf<TcpStream>>,
|
||||
) -> std::io::Result<Option<(String, Vec<u8>)>> {
|
||||
let mut buf = Vec::new();
|
||||
let n = reader.read_until(b'\n', &mut buf).await?;
|
||||
if n == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut trimmed = buf.as_slice();
|
||||
if trimmed.last() == Some(&b'\n') {
|
||||
trimmed = &trimmed[..trimmed.len() - 1];
|
||||
}
|
||||
if trimmed.last() == Some(&b'\r') {
|
||||
trimmed = &trimmed[..trimmed.len() - 1];
|
||||
}
|
||||
Ok(Some((String::from_utf8_lossy(trimmed).into_owned(), buf)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -35,11 +54,23 @@ impl ImapConnection {
|
||||
let (reader, writer) = tokio::io::split(TcpStream::connect(addr.as_ref()).await.unwrap());
|
||||
ImapConnection {
|
||||
tag,
|
||||
reader: BufReader::new(reader).lines(),
|
||||
reader: BufReader::new(reader),
|
||||
writer,
|
||||
last_raw: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_last_contains_bytes(&self, pattern: &[u8]) -> &Self {
|
||||
if !self.last_raw.windows(pattern.len()).any(|w| w == pattern) {
|
||||
panic!(
|
||||
"Expected byte sequence {:02x?} not found in last response ({} bytes).",
|
||||
pattern,
|
||||
self.last_raw.len()
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn assert_read(&mut self, t: Type, rt: ResponseType) -> Vec<String> {
|
||||
let lines = self.read(t).await;
|
||||
let mut buf = Vec::with_capacity(10);
|
||||
@@ -63,9 +94,14 @@ impl ImapConnection {
|
||||
}
|
||||
|
||||
pub async fn assert_disconnect(&mut self) {
|
||||
match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await {
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(1500),
|
||||
read_lossy_line(&mut self.reader),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(None)) => {}
|
||||
Ok(Ok(Some(line))) => {
|
||||
Ok(Ok(Some((line, _)))) => {
|
||||
panic!("Expected connection to be closed, but got {:?}", line);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
@@ -77,9 +113,16 @@ impl ImapConnection {
|
||||
|
||||
pub async fn read(&mut self, t: Type) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
self.last_raw.clear();
|
||||
loop {
|
||||
match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await {
|
||||
Ok(Ok(Some(line))) => {
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(1500),
|
||||
read_lossy_line(&mut self.reader),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(Some((line, raw)))) => {
|
||||
self.last_raw.extend_from_slice(&raw);
|
||||
let is_done = line.starts_with(match t {
|
||||
Type::Tagged => std::str::from_utf8(self.tag).unwrap(),
|
||||
Type::Untagged | Type::Status => "* ",
|
||||
|
||||
Reference in New Issue
Block a user