diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index 45abddffd..5a3cfe617 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -19,6 +19,7 @@ * along with meli. If not, see . */ +use super::protocol_parser::ImapLineSplit; use crate::email::parser::BytesExt; use crate::error::*; use std::io::Read; @@ -231,7 +232,47 @@ impl ImapStream { } conn_result? }; + let mut res = String::with_capacity(8 * 1024); let mut ret = ImapStream { cmd_id, stream }; + ret.send_command(b"CAPABILITY")?; + ret.read_response(&mut res)?; + let capabilities: std::result::Result, _> = res + .split_rn() + .find(|l| l.starts_with("* CAPABILITY")) + .ok_or_else(|| MeliError::new("")) + .and_then(|res| { + protocol_parser::capabilities(res.as_bytes()) + .to_full_result() + .map_err(|_| MeliError::new("")) + }); + + if capabilities.is_err() { + return Err(MeliError::new(format!( + "Could not connect to {}: expected CAPABILITY response but got:{}", + &server_conf.server_hostname, res + ))); + } + + let capabilities = capabilities.unwrap(); + if !capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case(b"IMAP4rev1")) + { + return Err(MeliError::new(format!( + "Could not connect to {}: server is not IMAP4rev1 compliant", + &server_conf.server_hostname + ))); + } else if capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case(b"LOGINDISABLED")) + { + return Err(MeliError::new(format!( + "Could not connect to {}: server does not accept logins [LOGINDISABLED]", + &server_conf.server_hostname + ))); + } + + let mut capabilities = None; ret.send_command( format!( "LOGIN \"{}\" \"{}\"", @@ -239,32 +280,39 @@ impl ImapStream { ) .as_bytes(), )?; - let mut res = String::with_capacity(8 * 1024); let tag_start = format!("M{} ", (ret.cmd_id - 1)); + loop { ret.read_lines(&mut res, &String::new())?; - if res.starts_with("* OK") { - if let Some(pos) = res.as_bytes().find(b"\r\n") { - let pos = pos + "\r\n".len(); - res.replace_range(..pos, ""); + let mut should_break = false; + for l in res.split_rn() { + if l.starts_with("* CAPABILITY") { + capabilities = protocol_parser::capabilities(l.as_bytes()) + .to_full_result() + .map(|capabilities| { + FnvHashSet::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 ") { + return Err(MeliError::new(format!( + "Could not connect. Server replied with '{}'", + l[tag_start.len()..].trim() + ))); + } + should_break = true; } } - - if res.starts_with(tag_start.as_str()) { - if !res[tag_start.len()..].trim().starts_with("OK ") { - return Err(MeliError::new(format!( - "Could not connect. Server replied with '{}'", - res[tag_start.len()..].trim() - ))); - } + if should_break { break; } } - let capabilities: std::result::Result, _> = - protocol_parser::capabilities(res.as_bytes()).to_full_result(); - - if capabilities.is_err() { + if capabilities.is_none() { /* sending CAPABILITY after LOGIN automatically is an RFC recommendation, so check * for lazy servers */ drop(capabilities); @@ -274,9 +322,7 @@ impl ImapStream { let capabilities = FnvHashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec())); Ok((capabilities, ret)) } else { - let capabilities = capabilities?; - let capabilities = FnvHashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec())); - + let capabilities = capabilities.unwrap(); Ok((capabilities, ret)) } } diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index 0094822a8..77a9bac4e 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -5,6 +5,38 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::str::FromStr; +pub struct ImapLineIterator<'a> { + slice: &'a str, +} + +impl<'a> Iterator for ImapLineIterator<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + 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..]; + Some(ret) + } else { + let ret = self.slice; + self.slice = &self.slice[ret.len()..]; + Some(ret) + } + } +} + +pub trait ImapLineSplit { + fn split_rn(&self) -> ImapLineIterator; +} + +impl ImapLineSplit for str { + fn split_rn(&self) -> ImapLineIterator { + ImapLineIterator { slice: self } + } +} + macro_rules! to_str ( ($v:expr) => (unsafe{ std::str::from_utf8_unchecked($v) }) ); @@ -181,14 +213,16 @@ macro_rules! flags_to_imap_list { * ============== * * "M0 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in\r\n" +* "* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENT TOKEN AUTH=OAUTHBEARER AUTH=XOAUTH\r\n" +* "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN\r\n" */ named!( pub capabilities>, do_parse!( - take_until!("[CAPABILITY ") - >> tag!("[CAPABILITY ") - >> ret: terminated!(separated_nonempty_list_complete!(tag!(" "), is_not!(" ]")), tag!("]")) + take_until!("CAPABILITY ") + >> tag!("CAPABILITY ") + >> ret: separated_nonempty_list_complete!(tag!(" "), is_not!(" ]\r\n")) >> take_until!("\r\n") >> tag!("\r\n") >> ({ ret })