melib/imap: detect untagged CAPABILITY responses

Gmail sends an untagged CAPABILITY response before accepting login, so
be smarter when logging in
async
Manos Pitsidianakis 2019-12-11 00:01:22 +02:00
parent 8235af9237
commit 569127fac5
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
2 changed files with 103 additions and 23 deletions

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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<Vec<&[u8]>, _> = 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<Vec<&[u8]>, _> =
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))
}
}

View File

@ -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<Vec<&[u8]>>,
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 })