From 3618bdcffba4911a5eb0e4a7a4c846e5445cdaf5 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 16 Sep 2020 13:07:26 +0300 Subject: [PATCH] melib/imap: treat server input as bytes Server input was assumed valid ascii and converted haphazardly to &str. Don't do that, since it might not be valid UTF8. --- melib/src/backends/imap.rs | 78 ++-- melib/src/backends/imap/cache/sync.rs | 16 +- melib/src/backends/imap/connection.rs | 118 +++--- melib/src/backends/imap/operations.rs | 10 +- melib/src/backends/imap/protocol_parser.rs | 404 +++++++++++---------- melib/src/backends/imap/untagged.rs | 14 +- melib/src/backends/imap/watch.rs | 37 +- melib/src/email/parser.rs | 21 +- 8 files changed, 366 insertions(+), 332 deletions(-) diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index cb4d994b9..60e3c3815 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -44,7 +44,7 @@ use crate::backends::{ use crate::conf::AccountSettings; use crate::connections::timeout; -use crate::email::*; +use crate::email::{parser::BytesExt, *}; use crate::error::{MeliError, Result, ResultIntoMeliError}; use futures::lock::Mutex as FutureMutex; use futures::stream::Stream; @@ -534,7 +534,7 @@ impl MailBackend for ImapType { let uid_store = self.uid_store.clone(); let connection = self.connection.clone(); Ok(Box::pin(async move { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; conn.select_mailbox(mailbox_hash, &mut response, true) .await?; @@ -645,7 +645,7 @@ impl MailBackend for ImapType { mailbox.imap_path().to_string() }; - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; conn.select_mailbox(source_mailbox_hash, &mut response, false) .await?; @@ -711,7 +711,7 @@ impl MailBackend for ImapType { return Ok(()); } - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; conn.select_mailbox(mailbox_hash, &mut response, false) .await?; @@ -883,7 +883,7 @@ impl MailBackend for ImapType { * flag set. */ } - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); { let mut conn_lck = connection.lock().await; conn_lck.unselect().await?; @@ -901,11 +901,11 @@ impl MailBackend for ImapType { .read_response(&mut response, RequiredResponses::empty()) .await?; } - let ret: Result<()> = ImapResponse::try_from(response.as_str())?.into(); + let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into(); ret?; let new_hash = get_path_hash!(path.as_str()); uid_store.mailboxes.lock().await.clear(); - Ok((new_hash, new_mailbox_fut?.await.map_err(|err| MeliError::new(format!("Mailbox create was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err)))?)) + Ok((new_hash, new_mailbox_fut?.await.map_err(|err| MeliError::new(format!("Mailbox create was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", String::from_utf8_lossy(&response), err)))?)) })) } @@ -928,7 +928,7 @@ impl MailBackend for ImapType { return Err(MeliError::new(format!("You do not have permission to delete `{}`. Set permissions for this mailbox are {}", mailboxes[&mailbox_hash].name(), permissions))); } } - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); { let mut conn_lck = connection.lock().await; /* make sure mailbox is not selected before it gets deleted, otherwise @@ -950,10 +950,10 @@ impl MailBackend for ImapType { .read_response(&mut response, RequiredResponses::empty()) .await?; } - let ret: Result<()> = ImapResponse::try_from(response.as_str())?.into(); + let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into(); ret?; uid_store.mailboxes.lock().await.clear(); - new_mailbox_fut?.await.map_err(|err| format!("Mailbox delete was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err).into()) + new_mailbox_fut?.await.map_err(|err| format!("Mailbox delete was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", String::from_utf8_lossy(&response), err).into()) })) } @@ -974,7 +974,7 @@ impl MailBackend for ImapType { command = format!("SUBSCRIBE \"{}\"", mailboxes[&mailbox_hash].imap_path()); } - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); { let mut conn_lck = connection.lock().await; if new_val { @@ -989,7 +989,7 @@ impl MailBackend for ImapType { .await?; } - let ret: Result<()> = ImapResponse::try_from(response.as_str())?.into(); + let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into(); if ret.is_ok() { uid_store .mailboxes @@ -1014,7 +1014,7 @@ impl MailBackend for ImapType { let new_mailbox_fut = self.mailboxes(); Ok(Box::pin(async move { let command: String; - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); { let mailboxes = uid_store.mailboxes.lock().await; let permissions = mailboxes[&mailbox_hash].permissions(); @@ -1041,10 +1041,10 @@ impl MailBackend for ImapType { .await?; } let new_hash = get_path_hash!(new_path.as_str()); - let ret: Result<()> = ImapResponse::try_from(response.as_str())?.into(); + let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into(); ret?; uid_store.mailboxes.lock().await.clear(); - new_mailbox_fut?.await.map_err(|err| format!("Mailbox rename was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?; + new_mailbox_fut?.await.map_err(|err| format!("Mailbox rename was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", String::from_utf8_lossy(&response), err))?; Ok(BackendMailbox::clone( &uid_store.mailboxes.lock().await[&new_hash], )) @@ -1172,7 +1172,7 @@ impl MailBackend for ImapType { let uid_store = self.uid_store.clone(); Ok(Box::pin(async move { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; conn.examine_mailbox(mailbox_hash, &mut response, false) .await?; @@ -1180,16 +1180,18 @@ impl MailBackend for ImapType { .await?; conn.read_response(&mut response, RequiredResponses::SEARCH) .await?; - debug!(&response); + debug!( + "searching for {} returned: {}", + query_str, + String::from_utf8_lossy(&response) + ); - let mut lines = response.lines(); - for l in lines.by_ref() { - if l.starts_with("* SEARCH") { + for l in response.split_rn() { + if l.starts_with(b"* SEARCH") { use std::iter::FromIterator; let uid_index = uid_store.uid_index.lock()?; return Ok(SmallVec::from_iter( - l["* SEARCH".len()..] - .trim() + String::from_utf8_lossy(l[b"* SEARCH".len()..].trim()) .split_whitespace() .map(UID::from_str) .filter_map(std::result::Result::ok) @@ -1198,7 +1200,9 @@ impl MailBackend for ImapType { )); } } - Err(MeliError::new(response)) + Err(MeliError::new( + String::from_utf8_lossy(&response).to_string(), + )) })) } } @@ -1303,7 +1307,7 @@ impl ImapType { futures::executor::block_on(timeout(self.server_conf.timeout, conn.connect())) .unwrap() .unwrap(); - let mut res = String::with_capacity(8 * 1024); + let mut res = Vec::with_capacity(8 * 1024); futures::executor::block_on(timeout( self.server_conf.timeout, conn.send_command(b"NOOP"), @@ -1332,7 +1336,7 @@ impl ImapType { .unwrap(); futures::executor::block_on(timeout( self.server_conf.timeout, - conn.read_lines(&mut res, String::new()), + conn.read_lines(&mut res, Vec::new()), )) .unwrap() .unwrap(); @@ -1348,7 +1352,7 @@ impl ImapType { conn = iter.into_conn(); } */ - println!("S: {}", &res); + println!("S: {}", String::from_utf8_lossy(&res)); } Err(error) => println!("error: {}", error), } @@ -1359,7 +1363,7 @@ impl ImapType { connection: &Arc>, ) -> Result> { let mut mailboxes: HashMap = Default::default(); - let mut res = String::with_capacity(8 * 1024); + let mut res = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; let has_list_status: bool = conn .uid_store @@ -1381,14 +1385,12 @@ impl ImapType { conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED) .await?; } - debug!("out: {}", &res); + debug!("out: {}", String::from_utf8_lossy(&res)); let mut lines = res.split_rn(); /* Remove "M__ OK .." line */ lines.next_back(); for l in lines { - if let Ok(mut mailbox) = - protocol_parser::list_mailbox_result(l.as_bytes()).map(|(_, v)| v) - { + if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) { if let Some(parent) = mailbox.parent { if mailboxes.contains_key(&parent) { mailboxes @@ -1414,9 +1416,7 @@ impl ImapType { } else { mailboxes.insert(mailbox.hash, mailbox); } - } else if let Ok(status) = - protocol_parser::status_response(l.as_bytes()).map(|(_, v)| v) - { + } else if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) { if let Some(mailbox_hash) = status.mailbox { if mailboxes.contains_key(&mailbox_hash) { let entry = mailboxes.entry(mailbox_hash).or_default(); @@ -1437,13 +1437,11 @@ impl ImapType { conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED) .await?; let mut lines = res.split_rn(); - debug!("out: {}", &res); + debug!("out: {}", String::from_utf8_lossy(&res)); /* Remove "M__ OK .." line */ lines.next_back(); for l in lines { - if let Ok(subscription) = - protocol_parser::list_mailbox_result(l.as_bytes()).map(|(_, v)| v) - { + if let Ok(subscription) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) { if let Some(f) = mailboxes.get_mut(&subscription.hash()) { f.is_subscribed = true; } @@ -1651,7 +1649,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result> { } let mut conn = connection.lock().await; debug!("locked for fetch {}", mailbox_path); - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let max_uid_left = max_uid; let chunk_size = 250; @@ -1686,7 +1684,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result> { debug!( "fetch response is {} bytes and {} lines", response.len(), - response.lines().count() + String::from_utf8_lossy(&response).lines().count() ); let (_, mut v, _) = protocol_parser::fetch_responses(&response)?; debug!("responses len is {}", v.len()); diff --git a/melib/src/backends/imap/cache/sync.rs b/melib/src/backends/imap/cache/sync.rs index 1ff2e968f..861f3cb89 100644 --- a/melib/src/backends/imap/cache/sync.rs +++ b/melib/src/backends/imap/cache/sync.rs @@ -83,7 +83,7 @@ impl ImapConnection { mailbox_hash: MailboxHash, ) -> Result<()> { debug!("build_cache {}", mailbox_hash); - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); // 1 get uidvalidity, highestmodseq let select_response = self .select_mailbox(mailbox_hash, &mut response, true) @@ -119,7 +119,7 @@ impl ImapConnection { ) -> Result>> { let mut payload = vec![]; debug!("resync_basic"); - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let cached_uidvalidity = self .uid_store .uidvalidity @@ -181,7 +181,7 @@ impl ImapConnection { debug!( "fetch response is {} bytes and {} lines", response.len(), - response.lines().count() + String::from_utf8_lossy(&response).lines().count() ); let (_, mut v, _) = protocol_parser::fetch_responses(&response)?; debug!("responses len is {}", v.len()); @@ -344,7 +344,7 @@ impl ImapConnection { ) -> Result>> { let mut payload = vec![]; debug!("resync_condstore"); - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let cached_uidvalidity = self .uid_store .uidvalidity @@ -465,7 +465,7 @@ impl ImapConnection { debug!( "fetch response is {} bytes and {} lines", response.len(), - response.lines().count() + String::from_utf8_lossy(&response).lines().count() ); let (_, mut v, _) = protocol_parser::fetch_responses(&response)?; debug!("responses len is {}", v.len()); @@ -597,7 +597,7 @@ impl ImapConnection { self.read_response(&mut response, RequiredResponses::SEARCH) .await?; //1) update cached flags for old messages; - let (_, v) = protocol_parser::search_results(response.as_bytes())?; + let (_, v) = protocol_parser::search_results(response.as_slice())?; for uid in v { valid_envs.insert(generate_envelope_hash(&mailbox_path, &uid)); } @@ -644,7 +644,7 @@ impl ImapConnection { } pub async fn init_mailbox(&mut self, mailbox_hash: MailboxHash) -> Result { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let (mailbox_path, mailbox_exists, unseen, permissions) = { let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash]; ( @@ -708,7 +708,7 @@ impl ImapConnection { .await?; self.read_response(&mut response, RequiredResponses::STATUS) .await?; - let (_, status) = protocol_parser::status_response(response.as_bytes())?; + let (_, status) = protocol_parser::status_response(response.as_slice())?; if let Some(uidnext) = status.uidnext { if uidnext == 0 { return Err(MeliError::new( diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index a2e46d5cc..d13a5b7d3 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -165,7 +165,7 @@ impl ImapStream { .flush() .await .chain_err_kind(crate::error::ErrorKind::Network)?; - let mut response = String::with_capacity(1024); + let mut response = Vec::with_capacity(1024); let mut broken = false; let now = Instant::now(); @@ -174,24 +174,24 @@ impl ImapStream { .read(&mut buf) .await .chain_err_kind(crate::error::ErrorKind::Network)?; - response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) }); + response.extend_from_slice(&buf[0..len]); match server_conf.protocol { ImapProtocol::IMAP { .. } => { - if response.starts_with("* OK ") && response.find("\r\n").is_some() { - if let Some(pos) = response.as_bytes().find(b"\r\n") { + if response.starts_with(b"* OK ") && response.find(b"\r\n").is_some() { + if let Some(pos) = response.find(b"\r\n") { response.drain(0..pos + 2); } } } ImapProtocol::ManageSieve => { - if response.starts_with("OK ") && response.find("\r\n").is_some() { + if response.starts_with(b"OK ") && response.find(b"\r\n").is_some() { response.clear(); broken = true; break; } } } - if response.starts_with("M1 OK") { + if response.starts_with(b"M1 OK") { broken = true; break; } @@ -262,7 +262,7 @@ impl ImapStream { crate::LoggingLevel::WARN, ); } - let mut res = String::with_capacity(8 * 1024); + let mut res = Vec::with_capacity(8 * 1024); let mut ret = ImapStream { cmd_id, stream, @@ -295,10 +295,10 @@ impl ImapStream { ret.read_response(&mut res).await?; let capabilities: std::result::Result, _> = res .split_rn() - .find(|l| l.starts_with("* CAPABILITY")) + .find(|l| l.starts_with(b"* CAPABILITY")) .ok_or_else(|| MeliError::new("")) .and_then(|res| { - protocol_parser::capabilities(res.as_bytes()) + protocol_parser::capabilities(&res) .map_err(|_| MeliError::new("")) .map(|(_, v)| v) }); @@ -306,8 +306,10 @@ impl ImapStream { if capabilities.is_err() { return Err(MeliError::new(format!( "Could not connect to {}: expected CAPABILITY response but got:{}", - &server_conf.server_hostname, res - ))); + &server_conf.server_hostname, + String::from_utf8_lossy(&res) + )) + .set_kind(ErrorKind::Bug)); } let capabilities = capabilities.unwrap(); @@ -342,22 +344,22 @@ impl ImapStream { let tag_start = format!("M{} ", (ret.cmd_id - 1)); loop { - ret.read_lines(&mut res, &String::new(), false).await?; + ret.read_lines(&mut res, &[], false).await?; let mut should_break = false; for l in res.split_rn() { - if l.starts_with("* CAPABILITY") { - capabilities = protocol_parser::capabilities(l.as_bytes()) + if l.starts_with(b"* CAPABILITY") { + capabilities = protocol_parser::capabilities(&l) .map(|(_, capabilities)| { HashSet::from_iter(capabilities.into_iter().map(|s: &[u8]| s.to_vec())) }) .ok(); } - if l.starts_with(tag_start.as_str()) { - if !l[tag_start.len()..].trim().starts_with("OK ") { + if l.starts_with(tag_start.as_bytes()) { + if !l[tag_start.len()..].trim().starts_with(b"OK ") { return Err(MeliError::new(format!( "Could not connect. Server replied with '{}'", - l[tag_start.len()..].trim() + String::from_utf8_lossy(l[tag_start.len()..].trim()) )) .set_err_kind(crate::error::ErrorKind::Authentication)); } @@ -375,7 +377,7 @@ impl ImapStream { drop(capabilities); ret.send_command(b"CAPABILITY").await?; ret.read_response(&mut res).await.unwrap(); - let capabilities = protocol_parser::capabilities(res.as_bytes())?.1; + let capabilities = protocol_parser::capabilities(&res)?.1; let capabilities = HashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec())); Ok((capabilities, ret)) } else { @@ -384,10 +386,10 @@ impl ImapStream { } } - pub async fn read_response(&mut self, ret: &mut String) -> Result<()> { + pub async fn read_response(&mut self, ret: &mut Vec) -> Result<()> { let id = match self.protocol { - ImapProtocol::IMAP { .. } => format!("M{} ", self.cmd_id - 1), - ImapProtocol::ManageSieve => String::new(), + ImapProtocol::IMAP { .. } => format!("M{} ", self.cmd_id - 1).into_bytes(), + ImapProtocol::ManageSieve => Vec::new(), }; self.read_lines(ret, &id, true).await?; Ok(()) @@ -395,8 +397,8 @@ impl ImapStream { pub async fn read_lines( &mut self, - ret: &mut String, - termination_string: &str, + ret: &mut Vec, + termination_string: &[u8], keep_termination_string: bool, ) -> Result<()> { let mut buf: Vec = vec![0; Connection::IO_BUF_SIZE]; @@ -406,31 +408,31 @@ impl ImapStream { match timeout(self.timeout, self.stream.read(&mut buf)).await? { Ok(0) => break, Ok(b) => { - ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) }); + ret.extend_from_slice(&buf[0..b]); if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") { - if ret[last_line_idx..].starts_with("* BYE") { + if ret[last_line_idx..].starts_with(b"* BYE") { return Err(MeliError::new("Disconnected")); } if let Some(prev_line) = - ret[last_line_idx..pos + last_line_idx].rfind("\r\n") + ret[last_line_idx..pos + last_line_idx].rfind(b"\r\n") { - last_line_idx += prev_line + "\r\n".len(); - pos -= prev_line + "\r\n".len(); + last_line_idx += prev_line + b"\r\n".len(); + pos -= prev_line + b"\r\n".len(); } - if Some(pos + "\r\n".len()) == ret.get(last_line_idx..).map(|r| r.len()) { + if Some(pos + b"\r\n".len()) == ret.get(last_line_idx..).map(|r| r.len()) { if !termination_string.is_empty() && ret[last_line_idx..].starts_with(termination_string) { debug!(&ret[last_line_idx..]); if !keep_termination_string { - ret.replace_range(last_line_idx.., ""); + ret.splice(last_line_idx.., std::iter::empty::()); } break; } else if termination_string.is_empty() { break; } } - last_line_idx += pos + "\r\n".len(); + last_line_idx += pos + b"\r\n".len(); } } Err(e) => { @@ -443,9 +445,9 @@ impl ImapStream { } pub async fn wait_for_continuation_request(&mut self) -> Result<()> { - let term = "+ ".to_string(); - let mut ret = String::new(); - self.read_lines(&mut ret, &term, false).await?; + let term = b"+ "; + let mut ret = Vec::new(); + self.read_lines(&mut ret, &term[..], false).await?; Ok(()) } @@ -546,7 +548,7 @@ impl ImapConnection { } } if debug!(self.stream.is_ok()) { - let mut ret = String::new(); + let mut ret = Vec::new(); if let Err(err) = try_await(async { self.send_command(b"NOOP").await?; self.read_response(&mut ret, RequiredResponses::empty()) @@ -586,7 +588,7 @@ impl ImapConnection { SyncPolicy::None => { /* do nothing, sync is disabled */ } _ => { /* Upgrade to Condstore */ - let mut ret = String::new(); + let mut ret = Vec::new(); if capabilities.contains(&b"ENABLE"[..]) { self.send_command(b"ENABLE CONDSTORE").await?; self.read_response(&mut ret, RequiredResponses::empty()) @@ -605,11 +607,11 @@ impl ImapConnection { } #[cfg(feature = "deflate_compression")] if capabilities.contains(&b"COMPRESS=DEFLATE"[..]) && deflate { - let mut ret = String::new(); + let mut ret = Vec::new(); self.send_command(b"COMPRESS DEFLATE").await?; self.read_response(&mut ret, RequiredResponses::empty()) .await?; - match ImapResponse::try_from(ret.as_str())? { + match ImapResponse::try_from(ret.as_slice())? { ImapResponse::No(code) | ImapResponse::Bad(code) | ImapResponse::Preauth(code) @@ -645,25 +647,25 @@ impl ImapConnection { pub fn read_response<'a>( &'a mut self, - ret: &'a mut String, + ret: &'a mut Vec, required_responses: RequiredResponses, ) -> Pin> + Send + 'a>> { Box::pin(async move { - let mut response = String::new(); + let mut response = Vec::new(); ret.clear(); self.stream.as_mut()?.read_response(&mut response).await?; *self.uid_store.is_online.lock().unwrap() = (Instant::now(), Ok(())); match self.server_conf.protocol { ImapProtocol::IMAP { .. } => { - let r: ImapResponse = ImapResponse::try_from(response.as_str())?; + let r: ImapResponse = ImapResponse::try_from(response.as_slice())?; match r { ImapResponse::Bye(ref response_code) => { self.stream = Err(MeliError::new(format!( "Offline: received BYE: {:?}", response_code ))); - ret.push_str(&response); + ret.extend_from_slice(&response); return r.into(); } ImapResponse::No(ref response_code) @@ -685,7 +687,7 @@ impl ImapConnection { level: crate::logging::LoggingLevel::ERROR, }, ); - ret.push_str(&response); + ret.extend_from_slice(&response); return r.into(); } ImapResponse::Bad(ref response_code) => { @@ -699,7 +701,7 @@ impl ImapConnection { level: crate::logging::LoggingLevel::ERROR, }, ); - ret.push_str(&response); + ret.extend_from_slice(&response); return r.into(); } _ => {} @@ -711,20 +713,24 @@ impl ImapConnection { for l in response.split_rn() { /*debug!("check line: {}", &l);*/ if required_responses.check(l) || !self.process_untagged(l).await? { - ret.push_str(l); + ret.extend_from_slice(l); } } Ok(()) } ImapProtocol::ManageSieve => { - ret.push_str(&response); + ret.extend_from_slice(&response); Ok(()) } } }) } - pub async fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { + pub async fn read_lines( + &mut self, + ret: &mut Vec, + termination_string: Vec, + ) -> Result<()> { self.stream .as_mut()? .read_lines(ret, &termination_string, false) @@ -783,7 +789,7 @@ impl ImapConnection { pub async fn select_mailbox( &mut self, mailbox_hash: MailboxHash, - ret: &mut String, + ret: &mut Vec, force: bool, ) -> Result> { if !force && self.stream.as_ref()?.current_mailbox == MailboxSelection::Select(mailbox_hash) @@ -809,7 +815,11 @@ impl ImapConnection { .await?; self.read_response(ret, RequiredResponses::SELECT_REQUIRED) .await?; - debug!("select response {}", ret); + debug!( + "{} select response {}", + imap_path, + String::from_utf8_lossy(&ret) + ); let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| { format!("Could not parse select response for mailbox {}", imap_path) })?; @@ -868,7 +878,7 @@ impl ImapConnection { pub async fn examine_mailbox( &mut self, mailbox_hash: MailboxHash, - ret: &mut String, + ret: &mut Vec, force: bool, ) -> Result> { if !force @@ -891,7 +901,7 @@ impl ImapConnection { .await?; self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED) .await?; - debug!("examine response {}", ret); + debug!("examine response {}", String::from_utf8_lossy(&ret)); let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| { format!("Could not parse select response for mailbox {}", imap_path) })?; @@ -915,7 +925,7 @@ impl ImapConnection { match self.stream.as_mut()?.current_mailbox.take() { MailboxSelection::Examine(_) | MailboxSelection::Select(_) => { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); if self .uid_store .capabilities @@ -970,7 +980,7 @@ impl ImapConnection { _select_response: &SelectResponse, ) -> Result<()> { debug_assert!(low > 0); - let mut response = String::new(); + let mut response = Vec::new(); self.send_command(format!("UID SEARCH {}:*", low).as_bytes()) .await?; self.read_response(&mut response, RequiredResponses::SEARCH) @@ -980,7 +990,7 @@ impl ImapConnection { let msn_index = msn_index_lck.entry(mailbox_hash).or_default(); let _ = msn_index.drain(low - 1..); msn_index.extend( - debug!(protocol_parser::search_results(response.as_bytes()))? + debug!(protocol_parser::search_results(&response))? .1 .into_iter(), ); diff --git a/melib/src/backends/imap/operations.rs b/melib/src/backends/imap/operations.rs index 1f6e363be..d1a6cfdb2 100644 --- a/melib/src/backends/imap/operations.rs +++ b/melib/src/backends/imap/operations.rs @@ -64,7 +64,7 @@ impl BackendOp for ImapOp { cache.bytes.is_some() }; if !exists_in_cache { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); { let mut conn = timeout(uid_store.timeout, connection.lock()).await?; conn.connect().await?; @@ -78,7 +78,7 @@ impl BackendOp for ImapOp { debug!( "fetch response is {} bytes and {} lines", response.len(), - response.lines().count() + String::from_utf8_lossy(&response).lines().count() ); let mut results = protocol_parser::fetch_responses(&response)?.1; if results.len() != 1 { @@ -114,7 +114,7 @@ impl BackendOp for ImapOp { } fn fetch_flags(&self) -> ResultFuture { - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let connection = self.connection.clone(); let mailbox_hash = self.mailbox_hash; let uid = self.uid; @@ -138,9 +138,9 @@ impl BackendOp for ImapOp { debug!( "fetch response is {} bytes and {} lines", response.len(), - response.lines().count() + String::from_utf8_lossy(&response).lines().count() ); - let v = protocol_parser::uid_fetch_flags_responses(response.as_bytes()) + let v = protocol_parser::uid_fetch_flags_responses(&response) .map(|(_, v)| v) .map_err(MeliError::from)?; if v.len() != 1 { diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index 334d3538a..fb9ff9a43 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -66,63 +66,63 @@ bitflags! { } impl RequiredResponses { - pub fn check(&self, line: &str) -> bool { - if !line.starts_with("* ") { + pub fn check(&self, line: &[u8]) -> bool { + if !line.starts_with(b"* ") { return false; } - let line = &line["* ".len()..]; + let line = &line[b"* ".len()..]; let mut ret = false; if self.intersects(RequiredResponses::CAPABILITY) { - ret |= line.starts_with("CAPABILITY"); + ret |= line.starts_with(b"CAPABILITY"); } if self.intersects(RequiredResponses::BYE) { - ret |= line.starts_with("BYE"); + ret |= line.starts_with(b"BYE"); } if self.intersects(RequiredResponses::FLAGS) { - ret |= line.starts_with("FLAGS"); + ret |= line.starts_with(b"FLAGS"); } if self.intersects(RequiredResponses::EXISTS) { - ret |= line.ends_with("EXISTS\r\n"); + ret |= line.ends_with(b"EXISTS\r\n"); } if self.intersects(RequiredResponses::RECENT) { - ret |= line.ends_with("RECENT\r\n"); + ret |= line.ends_with(b"RECENT\r\n"); } if self.intersects(RequiredResponses::UNSEEN) { - ret |= line.starts_with("UNSEEN"); + ret |= line.starts_with(b"UNSEEN"); } if self.intersects(RequiredResponses::PERMANENTFLAGS) { - ret |= line.starts_with("PERMANENTFLAGS"); + ret |= line.starts_with(b"PERMANENTFLAGS"); } if self.intersects(RequiredResponses::UIDNEXT) { - ret |= line.starts_with("UIDNEXT"); + ret |= line.starts_with(b"UIDNEXT"); } if self.intersects(RequiredResponses::UIDVALIDITY) { - ret |= line.starts_with("UIDVALIDITY"); + ret |= line.starts_with(b"UIDVALIDITY"); } if self.intersects(RequiredResponses::LIST) { - ret |= line.starts_with("LIST"); + ret |= line.starts_with(b"LIST"); } if self.intersects(RequiredResponses::LSUB) { - ret |= line.starts_with("LSUB"); + ret |= line.starts_with(b"LSUB"); } if self.intersects(RequiredResponses::STATUS) { - ret |= line.starts_with("STATUS"); + ret |= line.starts_with(b"STATUS"); } if self.intersects(RequiredResponses::EXPUNGE) { - ret |= line.ends_with("EXPUNGE\r\n"); + ret |= line.ends_with(b"EXPUNGE\r\n"); } if self.intersects(RequiredResponses::SEARCH) { - ret |= line.starts_with("SEARCH"); + ret |= line.starts_with(b"SEARCH"); } if self.intersects(RequiredResponses::FETCH) { let mut ptr = 0; for i in 0..line.len() { - if !line.as_bytes()[i].is_ascii_digit() { + if !line[i].is_ascii_digit() { ptr = i; break; } } - ret |= line[ptr..].trim_start().starts_with("FETCH"); + ret |= line[ptr..].trim_start().starts_with(b"FETCH"); } ret } @@ -130,19 +130,18 @@ impl RequiredResponses { #[test] fn test_imap_required_responses() { - let mut ret = String::new(); + let mut ret = Vec::new(); let required_responses = RequiredResponses::FETCH_REQUIRED; let response = - &"* 1040 FETCH (UID 1064 FLAGS ())\r\nM15 OK Fetch completed (0.001 + 0.299 secs).\r\n" - [0..]; + &b"* 1040 FETCH (UID 1064 FLAGS ())\r\nM15 OK Fetch completed (0.001 + 0.299 secs).\r\n"[..]; for l in response.split_rn() { /*debug!("check line: {}", &l);*/ if required_responses.check(l) { - ret.push_str(l); + ret.extend_from_slice(l); } } - assert_eq!(&ret, "* 1040 FETCH (UID 1064 FLAGS ())\r\n"); - let v = protocol_parser::uid_fetch_flags_responses(response.as_bytes()) + assert_eq!(ret.as_slice(), &b"* 1040 FETCH (UID 1064 FLAGS ())\r\n"[..]); + let v = protocol_parser::uid_fetch_flags_responses(response) .unwrap() .1; assert_eq!(v.len(), 1); @@ -150,9 +149,9 @@ fn test_imap_required_responses() { #[derive(Debug)] pub struct Alert(String); -pub type ImapParseResult<'a, T> = Result<(&'a str, T, Option)>; +pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option)>; pub struct ImapLineIterator<'a> { - slice: &'a str, + slice: &'a [u8], } #[derive(Debug, PartialEq)] @@ -210,38 +209,35 @@ impl std::fmt::Display for ResponseCode { } impl ResponseCode { - fn from(val: &str) -> ResponseCode { + fn from(val: &[u8]) -> ResponseCode { use ResponseCode::*; - if !val.starts_with('[') { + if !val.starts_with(b"[") { let msg = val.trim(); - return Alert(msg.to_string()); + return Alert(String::from_utf8_lossy(msg).to_string()); } let val = &val[1..]; - if val.starts_with("BADCHARSET") { - let charsets = val - .as_bytes() - .find(b"(") - .map(|pos| val[pos + 1..].trim().to_string()); - Badcharset(charsets) - } else if val.starts_with("READONLY") { + if val.starts_with(b"BADCHARSET") { + let charsets = val.find(b"(").map(|pos| val[pos + 1..].trim()); + Badcharset(charsets.map(|charsets| String::from_utf8_lossy(charsets).to_string())) + } else if val.starts_with(b"READONLY") { ReadOnly - } else if val.starts_with("READWRITE") { + } else if val.starts_with(b"READWRITE") { ReadWrite - } else if val.starts_with("TRYCREATE") { + } else if val.starts_with(b"TRYCREATE") { Trycreate - } else if val.starts_with("UIDNEXT") { + } else if val.starts_with(b"UIDNEXT") { //FIXME Uidnext(0) - } else if val.starts_with("UIDVALIDITY") { + } else if val.starts_with(b"UIDVALIDITY") { //FIXME Uidvalidity(0) - } else if val.starts_with("UNSEEN") { + } else if val.starts_with(b"UNSEEN") { //FIXME Unseen(0) } else { - let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim(); - Alert(msg.to_string()) + let msg = &val[val.find(b"] ").unwrap() + 1..].trim(); + Alert(String::from_utf8_lossy(msg).to_string()) } } } @@ -255,12 +251,11 @@ pub enum ImapResponse { Bye(ResponseCode), } -impl TryFrom<&'_ str> for ImapResponse { +impl TryFrom<&'_ [u8]> for ImapResponse { type Error = MeliError; - fn try_from(val: &'_ str) -> Result { - let val: &str = val.split_rn().last().unwrap_or(val.as_ref()); - debug!(&val); - let mut val = val[val.as_bytes().find(b" ").ok_or_else(|| { + fn try_from(val: &'_ [u8]) -> Result { + let val: &[u8] = val.split_rn().last().unwrap_or(val.as_ref()); + let mut val = val[val.find(b" ").ok_or_else(|| { MeliError::new(format!( "Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}", val @@ -268,8 +263,8 @@ impl TryFrom<&'_ str> for ImapResponse { })? + 1..] .trim(); // M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n - if val.ends_with(" secs).") { - val = &val[..val.as_bytes().rfind(b"(").ok_or_else(|| { + if val.ends_with(b" secs).") { + val = &val[..val.rfind(b"(").ok_or_else(|| { MeliError::new(format!( "Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}", val @@ -277,16 +272,16 @@ impl TryFrom<&'_ str> for ImapResponse { })?]; } - Ok(if val.starts_with("OK") { - Self::Ok(ResponseCode::from(&val["OK ".len()..])) - } else if val.starts_with("NO") { - Self::No(ResponseCode::from(&val["NO ".len()..])) - } else if val.starts_with("BAD") { - Self::Bad(ResponseCode::from(&val["BAD ".len()..])) - } else if val.starts_with("PREAUTH") { - Self::Preauth(ResponseCode::from(&val["PREAUTH ".len()..])) - } else if val.starts_with("BYE") { - Self::Bye(ResponseCode::from(&val["BYE ".len()..])) + Ok(if val.starts_with(b"OK") { + Self::Ok(ResponseCode::from(&val[b"OK ".len()..])) + } else if val.starts_with(b"NO") { + Self::No(ResponseCode::from(&val[b"NO ".len()..])) + } else if val.starts_with(b"BAD") { + Self::Bad(ResponseCode::from(&val[b"BAD ".len()..])) + } else if val.starts_with(b"PREAUTH") { + Self::Preauth(ResponseCode::from(&val[b"PREAUTH ".len()..])) + } else if val.starts_with(b"BYE") { + Self::Bye(ResponseCode::from(&val[b"BYE ".len()..])) } else { return Err(MeliError::new(format!( "Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}", @@ -313,47 +308,48 @@ impl Into> for ImapResponse { #[test] fn test_imap_response() { - assert_eq!(ImapResponse::try_from("M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n").unwrap(), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string()))); + assert_eq!(ImapResponse::try_from(&b"M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"[..]).unwrap(), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string()))); } impl<'a> std::iter::DoubleEndedIterator for ImapLineIterator<'a> { fn next_back(&mut self) -> Option { if self.slice.is_empty() { None - } else if let Some(pos) = self.slice.rfind("\r\n") { - if self.slice[..pos].is_empty() { - self.slice = &self.slice[..pos]; + } else if let Some(pos) = self.slice.rfind(b"\r\n") { + if self.slice.get(..pos).unwrap_or_default().is_empty() { + self.slice = self.slice.get(..pos).unwrap_or_default(); None - } else if let Some(prev_pos) = self.slice[..pos].rfind("\r\n") { - let ret = &self.slice[prev_pos + 2..pos + 2]; - self.slice = &self.slice[..prev_pos + 2]; + } else if let Some(prev_pos) = self.slice.get(..pos).unwrap_or_default().rfind(b"\r\n") + { + let ret = self.slice.get(prev_pos + 2..pos + 2).unwrap_or_default(); + self.slice = self.slice.get(..prev_pos + 2).unwrap_or_default(); Some(ret) } else { let ret = self.slice; - self.slice = &self.slice[ret.len()..]; + self.slice = self.slice.get(ret.len()..).unwrap_or_default(); Some(ret) } } else { let ret = self.slice; - self.slice = &self.slice[ret.len()..]; + self.slice = self.slice.get(ret.len()..).unwrap_or_default(); Some(ret) } } } impl<'a> Iterator for ImapLineIterator<'a> { - type Item = &'a str; + type Item = &'a [u8]; - fn next(&mut self) -> Option<&'a str> { + fn next(&mut self) -> Option<&'a [u8]> { if self.slice.is_empty() { None - } else if let Some(pos) = self.slice.find("\r\n") { - let ret = &self.slice[..pos + 2]; - self.slice = &self.slice[pos + 2..]; + } else if let Some(pos) = self.slice.find(b"\r\n") { + let ret = self.slice.get(..pos + 2).unwrap_or_default(); + self.slice = self.slice.get(pos + 2..).unwrap_or_default(); Some(ret) } else { let ret = self.slice; - self.slice = &self.slice[ret.len()..]; + self.slice = self.slice.get(ret.len()..).unwrap_or_default(); Some(ret) } } @@ -363,7 +359,7 @@ pub trait ImapLineSplit { fn split_rn(&self) -> ImapLineIterator; } -impl ImapLineSplit for str { +impl ImapLineSplit for [u8] { fn split_rn(&self) -> ImapLineIterator { ImapLineIterator { slice: self } } @@ -416,7 +412,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> { let separator: u8 = separator[0]; let mut f = ImapMailbox::default(); f.no_select = false; - f.is_subscribed = path == "INBOX"; + f.is_subscribed = path.eq_ignore_ascii_case("INBOX"); for p in properties.split(|&b| b == b' ') { if p.eq_ignore_ascii_case(b"\\NoSelect") || p.eq_ignore_ascii_case(b"\\NonExistent") { @@ -431,7 +427,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> { let _ = f.set_special_usage(SpecialUsageMailbox::Drafts); } } - f.imap_path = path.into(); + f.imap_path = path.to_string(); f.hash = get_path_hash!(&f.imap_path); f.path = if separator == b'/' { f.imap_path.clone() @@ -462,26 +458,27 @@ pub struct FetchResponse<'a> { pub envelope: Option, } -pub fn fetch_response(input: &str) -> ImapParseResult> { +pub fn fetch_response(input: &[u8]) -> ImapParseResult> { macro_rules! should_start_with { ($input:expr, $tag:literal) => { if !$input.starts_with($tag) { return Err(MeliError::new(format!( "Expected `{}` but got `{:.50}`", - $tag, &$input + String::from_utf8_lossy($tag), + String::from_utf8_lossy(&$input) ))); } }; } - should_start_with!(input, "* "); + should_start_with!(input, b"* "); - let mut i = "* ".len(); + let mut i = b"* ".len(); macro_rules! bounds { () => { if i == input.len() { return Err(MeliError::new(format!( "Expected more input. Got: `{:.50}`", - input + String::from_utf8_lossy(&input) ))); } }; @@ -494,13 +491,13 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { macro_rules! eat_whitespace { () => { - while (input.as_bytes()[i] as char).is_whitespace() { + while (input[i] as char).is_whitespace() { i += 1; bounds!(); } }; (break) => { - while (input.as_bytes()[i] as char).is_whitespace() { + while (input[i] as char).is_whitespace() { i += 1; bounds!(break); } @@ -516,8 +513,8 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { envelope: None, }; - while input.as_bytes()[i].is_ascii_digit() { - let b: u8 = input.as_bytes()[i] - 0x30; + while input[i].is_ascii_digit() { + let b: u8 = input[i] - 0x30; ret.message_sequence_number *= 10; ret.message_sequence_number += b as MessageSequenceNumber; i += 1; @@ -525,18 +522,17 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { } eat_whitespace!(); - should_start_with!(input[i..], "FETCH ("); - i += "FETCH (".len(); + should_start_with!(&input[i..], b"FETCH ("); + i += b"FETCH (".len(); let mut has_attachments = false; while i < input.len() { eat_whitespace!(break); bounds!(break); - if input[i..].starts_with("UID ") { - i += "UID ".len(); - if let Ok((rest, uid)) = take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>( - is_digit, - )(input[i..].as_bytes()) + if input[i..].starts_with(b"UID ") { + i += b"UID ".len(); + if let Ok((rest, uid)) = + take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>(is_digit)(&input[i..]) { i += input.len() - i - rest.len(); ret.uid = @@ -544,25 +540,24 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { } else { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH response. Got: `{:.40}`", - input + String::from_utf8_lossy(&input) )))); } - } else if input[i..].starts_with("FLAGS (") { - i += "FLAGS (".len(); + } else if input[i..].starts_with(b"FLAGS (") { + i += b"FLAGS (".len(); if let Ok((rest, flags)) = flags(&input[i..]) { ret.flags = Some(flags); i += (input.len() - i - rest.len()) + 1; } else { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH response. Got: `{:.40}`", - input + String::from_utf8_lossy(&input) )))); } - } else if input[i..].starts_with("MODSEQ (") { - i += "MODSEQ (".len(); - if let Ok((rest, modseq)) = take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>( - is_digit, - )(input[i..].as_bytes()) + } else if input[i..].starts_with(b"MODSEQ (") { + i += b"MODSEQ (".len(); + if let Ok((rest, modseq)) = + take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>(is_digit)(&input[i..]) { i += (input.len() - i - rest.len()) + 1; ret.modseq = u64::from_str(to_str!(modseq)) @@ -572,11 +567,11 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { } else { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`", - input + String::from_utf8_lossy(&input) )))); } - } else if input[i..].starts_with("RFC822 {") { - i += "RFC822 ".len(); + } else if input[i..].starts_with(b"RFC822 {") { + i += b"RFC822 ".len(); if let Ok((rest, body)) = length_data::<_, _, (&[u8], nom::error::ErrorKind), _>(delimited( tag("{"), @@ -584,41 +579,41 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }), tag("}\r\n"), - ))(input[i..].as_bytes()) + ))(&input[i..]) { ret.body = Some(body); i += input.len() - i - rest.len(); } else { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH response. Got: `{:.40}`", - input + String::from_utf8_lossy(&input) )))); } - } else if input[i..].starts_with("ENVELOPE (") { - i += "ENVELOPE ".len(); - if let Ok((rest, envelope)) = envelope(input[i..].as_bytes()) { + } else if input[i..].starts_with(b"ENVELOPE (") { + i += b"ENVELOPE ".len(); + if let Ok((rest, envelope)) = envelope(&input[i..]) { ret.envelope = Some(envelope); i += input.len() - i - rest.len(); } else { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH response. Got: `{:.40}`", - &input[i..] + String::from_utf8_lossy(&input[i..]) )))); } - } else if input[i..].starts_with("BODYSTRUCTURE ") { - i += "BODYSTRUCTURE ".len(); + } else if input[i..].starts_with(b"BODYSTRUCTURE ") { + i += b"BODYSTRUCTURE ".len(); let mut struct_ptr = i; let mut parenth_level = 0; let mut inside_quote = false; while struct_ptr != input.len() { if !inside_quote { - if input.as_bytes()[struct_ptr] == b'(' { + if input[struct_ptr] == b'(' { parenth_level += 1; - } else if input.as_bytes()[struct_ptr] == b')' { + } else if input[struct_ptr] == b')' { if parenth_level == 0 { return debug!(Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH response. Got: `{:.40}`", - &input[struct_ptr..] + String::from_utf8_lossy(&input[struct_ptr..]) )))); } parenth_level -= 1; @@ -626,30 +621,30 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { struct_ptr += 1; break; } - } else if input.as_bytes()[struct_ptr] == b'"' { + } else if input[struct_ptr] == b'"' { inside_quote = true; } - } else if input.as_bytes()[struct_ptr] == b'\"' - && (struct_ptr == 0 || (input.as_bytes()[struct_ptr - 1] != b'\\')) + } else if input[struct_ptr] == b'\"' + && (struct_ptr == 0 || (input[struct_ptr - 1] != b'\\')) { inside_quote = false; } struct_ptr += 1; } - has_attachments = bodystructure_has_attachments(&input.as_bytes()[i..struct_ptr]); + has_attachments = bodystructure_has_attachments(&input[i..struct_ptr]); i = struct_ptr; - } else if input[i..].starts_with(")\r\n") { - i += ")\r\n".len(); + } else if input[i..].starts_with(b")\r\n") { + i += b")\r\n".len(); break; } else { debug!( "Got unexpected token while parsing UID FETCH response:\n`{}`\n", - input + String::from_utf8_lossy(&input) ); return debug!(Err(MeliError::new(format!( "Got unexpected token while parsing UID FETCH response: `{:.40}`", - &input[i..] + String::from_utf8_lossy(&input[i..]) )))); } } @@ -661,11 +656,11 @@ pub fn fetch_response(input: &str) -> ImapParseResult> { Ok((&input[i..], ret, None)) } -pub fn fetch_responses(mut input: &str) -> ImapParseResult>> { +pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult>> { let mut ret = Vec::new(); let mut alert: Option = None; - while input.starts_with("* ") { + while input.starts_with(b"* ") { let next_response = fetch_response(input); match next_response { Ok((rest, el, el_alert)) => { @@ -683,7 +678,8 @@ pub fn fetch_responses(mut input: &str) -> ImapParseResult Err(err) => { return Err(MeliError::new(format!( "Unexpected input while parsing UID FETCH responses: `{:.40}`, {}", - input, err + String::from_utf8_lossy(&input), + err ))); } } @@ -694,7 +690,7 @@ pub fn fetch_responses(mut input: &str) -> ImapParseResult } else { return Err(MeliError::new(format!( "310Unexpected input while parsing UID FETCH responses: `{:.40}`", - input + String::from_utf8_lossy(&input) ))); } } @@ -842,27 +838,31 @@ pub enum UntaggedResponse<'s> { }, } -pub fn untagged_responses(input: &str) -> ImapParseResult>> { +pub fn untagged_responses(input: &[u8]) -> ImapParseResult>> { let orig_input = input; - let (input, _) = tag::<_, &str, (&str, nom::error::ErrorKind)>("* ")(input)?; - let (input, num) = map_res::<_, _, _, (&str, nom::error::ErrorKind), _, _, _>(digit1, |s| { - ImapNum::from_str(s) + let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"* ")(input)?; + let (input, num) = map_res::<_, _, _, (&[u8], nom::error::ErrorKind), _, _, _>(digit1, |s| { + ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })(input)?; - let (input, _) = tag::<_, &str, (&str, nom::error::ErrorKind)>(" ")(input)?; - let (input, _tag) = take_until::<_, &str, (&str, nom::error::ErrorKind)>("\r\n")(input)?; - let (input, _) = tag::<_, &str, (&str, nom::error::ErrorKind)>("\r\n")(input)?; + let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b" ")(input)?; + let (input, _tag) = + take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?; + let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?; debug!("Parse untagged response from {:?}", orig_input); Ok(( input, { use UntaggedResponse::*; match _tag { - "EXPUNGE" => Some(Expunge(num)), - "EXISTS" => Some(Exists(num)), - "RECENT" => Some(Recent(num)), - _ if _tag.starts_with("FETCH ") => Some(Fetch(fetch_response(orig_input)?.1)), + b"EXPUNGE" => Some(Expunge(num)), + b"EXISTS" => Some(Exists(num)), + b"RECENT" => Some(Recent(num)), + _ if _tag.starts_with(b"FETCH ") => Some(Fetch(fetch_response(orig_input)?.1)), _ => { - debug!("unknown untagged_response: {}", _tag); + debug!( + "unknown untagged_response: {}", + String::from_utf8_lossy(&_tag) + ); None } } @@ -876,14 +876,14 @@ fn test_untagged_responses() { use std::convert::TryInto; use UntaggedResponse::*; assert_eq!( - untagged_responses("* 2 EXISTS\r\n") + untagged_responses(b"* 2 EXISTS\r\n") .map(|(_, v, _)| v) .unwrap() .unwrap(), Exists(2) ); assert_eq!( - untagged_responses("* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n") + untagged_responses(b"* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n") .map(|(_, v, _)| v) .unwrap() .unwrap(), @@ -897,7 +897,7 @@ fn test_untagged_responses() { }) ); assert_eq!( - untagged_responses("* 1 FETCH (FLAGS (\\Seen))\r\n") + untagged_responses(b"* 1 FETCH (FLAGS (\\Seen))\r\n") .map(|(_, v, _)| v) .unwrap() .unwrap(), @@ -1001,59 +1001,70 @@ pub struct SelectResponse { * * OK [UIDVALIDITY 1554422056] UIDs valid * * OK [UIDNEXT 50] Predicted next UID */ -pub fn select_response(input: &str) -> Result { - if input.contains("* OK") { +pub fn select_response(input: &[u8]) -> Result { + if input.contains_subsequence(b"* OK") { let mut ret = SelectResponse::default(); for l in input.split_rn() { - if l.starts_with("* ") && l.ends_with(" EXISTS\r\n") { - ret.exists = ImapNum::from_str(&l["* ".len()..l.len() - " EXISTS\r\n".len()])?; - } else if l.starts_with("* ") && l.ends_with(" RECENT\r\n") { - ret.recent = ImapNum::from_str(&l["* ".len()..l.len() - " RECENT\r\n".len()])?; - } else if l.starts_with("* FLAGS (") { - ret.flags = flags(&l["* FLAGS (".len()..l.len() - ")".len()]).map(|(_, v)| v)?; - } else if l.starts_with("* OK [UNSEEN ") { - ret.unseen = MessageSequenceNumber::from_str( - &l["* OK [UNSEEN ".len()..l.find(']').unwrap()], - )?; - } else if l.starts_with("* OK [UIDVALIDITY ") { - ret.uidvalidity = - UIDVALIDITY::from_str(&l["* OK [UIDVALIDITY ".len()..l.find(']').unwrap()])?; - } else if l.starts_with("* OK [UIDNEXT ") { - ret.uidnext = UID::from_str(&l["* OK [UIDNEXT ".len()..l.find(']').unwrap()])?; - } else if l.starts_with("* OK [PERMANENTFLAGS (") { + if l.starts_with(b"* ") && l.ends_with(b" EXISTS\r\n") { + ret.exists = ImapNum::from_str(&String::from_utf8_lossy( + &l[b"* ".len()..l.len() - b" EXISTS\r\n".len()], + ))?; + } else if l.starts_with(b"* ") && l.ends_with(b" RECENT\r\n") { + ret.recent = ImapNum::from_str(&String::from_utf8_lossy( + &l[b"* ".len()..l.len() - b" RECENT\r\n".len()], + ))?; + } else if l.starts_with(b"* FLAGS (") { + ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?; + } else if l.starts_with(b"* OK [UNSEEN ") { + ret.unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy( + &l[b"* OK [UNSEEN ".len()..l.find(b"]").unwrap()], + ))?; + } else if l.starts_with(b"* OK [UIDVALIDITY ") { + ret.uidvalidity = UIDVALIDITY::from_str(&String::from_utf8_lossy( + &l[b"* OK [UIDVALIDITY ".len()..l.find(b"]").unwrap()], + ))?; + } else if l.starts_with(b"* OK [UIDNEXT ") { + ret.uidnext = UID::from_str(&String::from_utf8_lossy( + &l[b"* OK [UIDNEXT ".len()..l.find(b"]").unwrap()], + ))?; + } else if l.starts_with(b"* OK [PERMANENTFLAGS (") { ret.permanentflags = - flags(&l["* OK [PERMANENTFLAGS (".len()..l.find(')').unwrap()]) + flags(&l[b"* OK [PERMANENTFLAGS (".len()..l.find(b")").unwrap()]) .map(|(_, v)| v)?; - ret.can_create_flags = l.contains("\\*"); - } else if l.contains("OK [READ-WRITE]") { + ret.can_create_flags = l.contains_subsequence(b"\\*"); + } else if l.contains_subsequence(b"OK [READ-WRITE]" as &[u8]) { ret.read_only = false; - } else if l.contains("OK [READ-ONLY]") { + } else if l.contains_subsequence(b"OK [READ-ONLY]") { ret.read_only = true; - } else if l.starts_with("* OK [HIGHESTMODSEQ ") { - let res: IResult<&str, &str> = take_until("]")(&l["* OK [HIGHESTMODSEQ ".len()..]); + } else if l.starts_with(b"* OK [HIGHESTMODSEQ ") { + let res: IResult<&[u8], &[u8]> = + take_until(&b"]"[..])(&l[b"* OK [HIGHESTMODSEQ ".len()..]); let (_, highestmodseq) = res?; ret.highestmodseq = Some( - std::num::NonZeroU64::new(u64::from_str(&highestmodseq)?) - .map(|u| Ok(ModSequence(u))) - .unwrap_or(Err(())), + std::num::NonZeroU64::new(u64::from_str(&String::from_utf8_lossy( + &highestmodseq, + ))?) + .map(|u| Ok(ModSequence(u))) + .unwrap_or(Err(())), ); - } else if l.starts_with("* OK [NOMODSEQ") { + } else if l.starts_with(b"* OK [NOMODSEQ") { ret.highestmodseq = Some(Err(())); } else if !l.is_empty() { - debug!("select response: {}", l); + debug!("select response: {}", String::from_utf8_lossy(&l)); } } Ok(ret) } else { - debug!("BAD/NO response in select: {}", input); - Err(MeliError::new(input.to_string())) + let ret = String::from_utf8_lossy(&input).to_string(); + debug!("BAD/NO response in select: {}", &ret); + Err(MeliError::new(ret)) } } #[test] fn test_select_response() { use std::convert::TryInto; - let r = "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n"; + let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n"; assert_eq!( select_response(r).expect("Could not parse IMAP select response"), @@ -1076,7 +1087,7 @@ fn test_select_response() { highestmodseq: None } ); - let r = "* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [HIGHESTMODSEQ 715194045007]\r\n* A142 OK [READ-WRITE] SELECT completed\r\n"; + let r = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [HIGHESTMODSEQ 715194045007]\r\n* A142 OK [READ-WRITE] SELECT completed\r\n"; assert_eq!( select_response(r).expect("Could not parse IMAP select response"), @@ -1098,7 +1109,7 @@ fn test_select_response() { ))), } ); - let r = "* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n"; + let r = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n"; assert_eq!( select_response(r).expect("Could not parse IMAP select response"), @@ -1120,41 +1131,41 @@ fn test_select_response() { ); } -pub fn flags(input: &str) -> IResult<&str, (Flag, Vec)> { +pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec)> { let mut ret = Flag::default(); let mut keywords = Vec::new(); let mut input = input; - while !input.starts_with(')') && !input.is_empty() { - if input.starts_with('\\') { + while !input.starts_with(b")") && !input.is_empty() { + if input.starts_with(b"\\") { input = &input[1..]; } let mut match_end = 0; while match_end < input.len() { - if input[match_end..].starts_with(' ') || input[match_end..].starts_with(')') { + if input[match_end..].starts_with(b" ") || input[match_end..].starts_with(b")") { break; } match_end += 1; } match &input[..match_end] { - "Answered" => { + b"Answered" => { ret.set(Flag::REPLIED, true); } - "Flagged" => { + b"Flagged" => { ret.set(Flag::FLAGGED, true); } - "Deleted" => { + b"Deleted" => { ret.set(Flag::TRASHED, true); } - "Seen" => { + b"Seen" => { ret.set(Flag::SEEN, true); } - "Draft" => { + b"Draft" => { ret.set(Flag::DRAFT, true); } f => { - keywords.push(f.to_string()); + keywords.push(String::from_utf8_lossy(&f).into()); } } input = &input[match_end..]; @@ -1164,11 +1175,10 @@ pub fn flags(input: &str) -> IResult<&str, (Flag, Vec)> { } pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec)> { - let i = unsafe { std::str::from_utf8_unchecked(input) }; - match flags(i) { - Ok((rest, ret)) => Ok((rest.as_bytes(), ret)), - Err(nom::Err::Error(err)) => Err(nom::Err::Error(err.as_bytes())), - Err(nom::Err::Failure(err)) => Err(nom::Err::Error(err.as_bytes())), + match flags(input) { + Ok((rest, ret)) => Ok((rest, ret)), + Err(nom::Err::Error(err)) => Err(nom::Err::Error(err)), + Err(nom::Err::Failure(err)) => Err(nom::Err::Error(err)), Err(nom::Err::Incomplete(_)) => { Err(nom::Err::Error((input, "byte_flags(): incomplete").into())) } @@ -1505,12 +1515,12 @@ pub fn status_response(input: &[u8]) -> IResult<&[u8], StatusResponse> { // ; is considered to be INBOX and not an astring. // ; Refer to section 5.1 for further // ; semantic details of mailbox names. -pub fn mailbox_token<'i>(input: &'i [u8]) -> IResult<&'i [u8], &'i str> { +pub fn mailbox_token<'i>(input: &'i [u8]) -> IResult<&'i [u8], std::borrow::Cow<'i, str>> { let (input, astring) = astring_token(input)?; if astring.eq_ignore_ascii_case(b"INBOX") { - return Ok((input, "INBOX")); + return Ok((input, "INBOX".into())); } - Ok((input, to_str!(astring))) + Ok((input, String::from_utf8_lossy(astring))) } // astring = 1*ASTRING-CHAR / string diff --git a/melib/src/backends/imap/untagged.rs b/melib/src/backends/imap/untagged.rs index 38f7fb2e7..529d145a7 100644 --- a/melib/src/backends/imap/untagged.rs +++ b/melib/src/backends/imap/untagged.rs @@ -33,7 +33,7 @@ use crate::error::*; use std::convert::TryInto; impl ImapConnection { - pub async fn process_untagged(&mut self, line: &str) -> Result { + pub async fn process_untagged(&mut self, line: &[u8]) -> Result { macro_rules! try_fail { ($mailbox_hash: expr, $($result:expr)+) => { $(if let Err(err) = $result { @@ -59,7 +59,7 @@ impl ImapConnection { let mut cache_handle = super::cache::DefaultCache::get(self.uid_store.clone())?; #[cfg(feature = "sqlite3")] let mut cache_handle = super::cache::Sqlite3Cache::get(self.uid_store.clone())?; - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let untagged_response = match super::protocol_parser::untagged_responses(line).map(|(_, v, _)| v) { Ok(None) | Err(_) => { @@ -233,7 +233,7 @@ impl ImapConnection { self.send_command(b"UID SEARCH RECENT").await self.read_response(&mut response, RequiredResponses::SEARCH).await ); - match super::protocol_parser::search_results_raw(response.as_bytes()) + match super::protocol_parser::search_results_raw(&response) .map(|(_, v)| v) .map_err(MeliError::from) { @@ -339,7 +339,7 @@ impl ImapConnection { debug!( "UID SEARCH RECENT err: {}\nresp: {}", e.to_string(), - &response + to_str!(&response) ); } } @@ -381,9 +381,9 @@ impl ImapConnection { ).await self.read_response(&mut response, RequiredResponses::SEARCH).await ); - debug!(&response); + debug!(to_str!(&response)); match super::protocol_parser::search_results( - response.split_rn().next().unwrap_or("").as_bytes(), + response.split_rn().next().unwrap_or(b""), ) .map(|(_, v)| v) { @@ -392,7 +392,7 @@ impl ImapConnection { return Ok(false); } Err(e) => { - debug!(&response); + debug!(to_str!(&response)); debug!(e); return Ok(false); } diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index 7b6f09ede..28a65f6de 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -74,12 +74,12 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { } }; let mailbox_hash = mailbox.hash(); - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let select_response = conn .select_mailbox(mailbox_hash, &mut response, true) .await? .unwrap(); - debug!("select response {}", &response); + debug!("select response {}", String::from_utf8_lossy(&response)); { let mut uidvalidities = uid_store.uidvalidity.lock().unwrap(); @@ -153,14 +153,14 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { } watch = now; } - if to_str!(&line) + if line .split_rn() .filter(|l| { - !l.starts_with("+ ") - && !l.starts_with("* ok") - && !l.starts_with("* ok") - && !l.starts_with("* Ok") - && !l.starts_with("* OK") + !l.starts_with(b"+ ") + && !l.starts_with(b"* ok") + && !l.starts_with(b"* ok") + && !l.starts_with(b"* Ok") + && !l.starts_with(b"* OK") }) .count() == 0 @@ -173,13 +173,13 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { .conn .read_response(&mut response, RequiredResponses::empty()) .await?; - for l in to_str!(&line).split_rn() { + for l in line.split_rn() { debug!("process_untagged {:?}", &l); - if l.starts_with("+ ") - || l.starts_with("* ok") - || l.starts_with("* ok") - || l.starts_with("* Ok") - || l.starts_with("* OK") + if l.starts_with(b"+ ") + || l.starts_with(b"* ok") + || l.starts_with(b"* ok") + || l.starts_with(b"* Ok") + || l.starts_with(b"* OK") { debug!("ignore continuation mark"); continue; @@ -214,7 +214,7 @@ pub async fn examine_updates( let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?; #[cfg(feature = "sqlite3")] let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?; - let mut response = String::with_capacity(8 * 1024); + let mut response = Vec::with_capacity(8 * 1024); let select_response = conn .examine_mailbox(mailbox_hash, &mut response, true) .await? @@ -249,9 +249,12 @@ pub async fn examine_updates( conn.send_command(b"UID SEARCH RECENT").await?; conn.read_response(&mut response, RequiredResponses::SEARCH) .await?; - let v = protocol_parser::search_results(response.as_bytes()).map(|(_, v)| v)?; + let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?; if v.is_empty() { - debug!("search response was empty: {}", response); + debug!( + "search response was empty: {}", + String::from_utf8_lossy(&response) + ); return Ok(()); } let mut cmd = "UID FETCH ".to_string(); diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index bbb79e53d..4eee76106 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -177,9 +177,19 @@ macro_rules! is_whitespace { pub trait BytesExt { fn rtrim(&self) -> &Self; fn ltrim(&self) -> &Self; + fn trim_start(&self) -> &Self { + self.ltrim() + } + fn trim_end(&self) -> &Self { + self.rtrim() + } fn trim(&self) -> &Self; - fn find(&self, needle: &[u8]) -> Option; - fn rfind(&self, needle: &[u8]) -> Option; + fn find>(&self, needle: T) -> Option; + fn contains_subsequence>(&self, needle: T) -> bool { + self.find(needle.as_ref()).is_some() + } + + fn rfind>(&self, needle: T) -> Option; fn replace(&self, from: &[u8], to: &[u8]) -> Vec; fn is_quoted(&self) -> bool; } @@ -202,8 +212,10 @@ impl BytesExt for [u8] { fn trim(&self) -> &[u8] { self.rtrim().ltrim() } + // https://stackoverflow.com/a/35907071 - fn find(&self, needle: &[u8]) -> Option { + fn find>(&self, needle: T) -> Option { + let needle = needle.as_ref(); if needle.is_empty() { return None; } @@ -211,7 +223,8 @@ impl BytesExt for [u8] { .position(|window| window == needle) } - fn rfind(&self, needle: &[u8]) -> Option { + fn rfind>(&self, needle: T) -> Option { + let needle = needle.as_ref(); if needle.is_empty() { return None; }