From e96e9789db72b610a1d4cdc2137fb03def383d78 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 25 Jun 2024 23:30:17 +0300 Subject: [PATCH] melib/imap: don't discard pre-auth capabilities The library user can retrieve capabilities from a mail backend, and they might want to see pre-auth capabilities as well. Signed-off-by: Manos Pitsidianakis --- melib/src/imap/connection.rs | 130 +++++++++++++++++++---------------- melib/src/imap/mod.rs | 4 +- 2 files changed, 73 insertions(+), 61 deletions(-) diff --git a/melib/src/imap/connection.rs b/melib/src/imap/connection.rs index 1e8c75a0..4b466a54 100644 --- a/melib/src/imap/connection.rs +++ b/melib/src/imap/connection.rs @@ -19,28 +19,10 @@ * along with meli. If not, see . */ -use crate::{ - backends::{BackendEvent, MailboxHash, RefreshEvent}, - email::parser::BytesExt, - error::*, - imap::{ - protocol_parser::{self, ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse}, - Capabilities, ImapServerConf, UIDStore, - }, - text::Truncate, - utils::{ - connections::{std_net::connect as tcp_stream_connect, Connection}, - futures::timeout, - }, - LogLevel, -}; -extern crate native_tls; use std::{ borrow::Cow, - collections::HashSet, convert::TryFrom, future::Future, - iter::FromIterator, pin::Pin, sync::Arc, time::{Duration, Instant, SystemTime}, @@ -62,9 +44,26 @@ use imap_codec::{ }, CommandCodec, }; +use indexmap::IndexSet; use native_tls::TlsConnector; pub use smol::Async as AsyncWrapper; +use crate::{ + backends::{BackendEvent, MailboxHash, RefreshEvent}, + email::parser::BytesExt, + error::*, + imap::{ + protocol_parser::{self, ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse}, + Capabilities, ImapServerConf, UIDStore, + }, + text::Truncate, + utils::{ + connections::{std_net::connect as tcp_stream_connect, Connection}, + futures::timeout, + }, + LogLevel, +}; + const IMAP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60 * 28); macro_rules! imap_log { @@ -343,32 +342,47 @@ impl ImapStream { ); ret.send_command(CommandBody::Capability).await?; ret.read_response(&mut res).await?; - let capabilities: std::result::Result, _> = res - .split_rn() - .find(|l| l.starts_with(b"* CAPABILITY")) - .ok_or_else(|| Error::new("")) - .and_then(|res| { - protocol_parser::capabilities(res) - .map_err(|_| Error::new("")) - .map(|(_, v)| v) - }); - - let capabilities = match capabilities { - Err(_err) => { - log::debug!( - "Could not connect to {}: expected CAPABILITY response but got: {} `{}`", - &server_conf.server_hostname, - _err, - String::from_utf8_lossy(&res) - ); - return Err(Error::new(format!( - "Could not connect to {}: expected CAPABILITY response but got: `{}`", - &server_conf.server_hostname, - String::from_utf8_lossy(&res).as_ref().trim_at_boundary(40) - )) - .set_kind(ErrorKind::ProtocolError)); + + fn parse_capabilities(bytes: &[u8], hostname: &str) -> Result>> { + protocol_parser::capabilities(bytes) + .map(|(_, v)| { + v.into_iter() + .map(|v| v.to_vec().into_boxed_slice()) + .collect() + }) + .map_err(|err| { + Error::new(format!( + "Could not connect to {}: could not parse CAPABILITY response: `{}`", + hostname, + String::from_utf8_lossy(bytes).as_ref().trim_at_boundary(40) + )) + .set_source(Some(Arc::new(Error::from(err)))) + .set_kind(ErrorKind::ProtocolError) + }) + } + + let mut capabilities: IndexSet> = { + let capabilities = res + .split_rn() + .find(|l| l.starts_with(b"* CAPABILITY")) + .ok_or_else(|| { + Error::new(format!( + "Could not connect to {}: could not parse CAPABILITY response: `{}`", + server_conf.server_hostname, + String::from_utf8_lossy(&res).as_ref().trim_at_boundary(40) + )) + .set_details("Response does not start with `* CAPABILITY`.") + .set_kind(ErrorKind::ProtocolError) + }) + .and_then(|res| parse_capabilities(res, &server_conf.server_hostname)); + + match capabilities { + Err(err) => { + log::debug!("{}: {}", uid_store.account_name, err); + return Err(err); + } + Ok(v) => v, } - Ok(v) => v, }; if !capabilities @@ -392,6 +406,7 @@ impl ImapStream { ImapProtocol::IMAP { extension_use: ImapExtensionUse { oauth2, .. }, } if oauth2 => { + // [ref:FIXME]: differentiate between OAUTH2 and XOAUTH2 if !capabilities .iter() .any(|cap| cap.eq_ignore_ascii_case(b"AUTH=XOAUTH2")) @@ -467,18 +482,16 @@ impl ImapStream { } } let tag_start = format!("M{} ", (ret.cmd_id - 1)); - let mut capabilities = None; + let mut got_new_capabilities = false; loop { ret.read_lines(&mut res, None, false).await?; let mut should_break = false; for l in res.split_rn() { 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(); + got_new_capabilities = true; + capabilities + .extend(parse_capabilities(l, &server_conf.server_hostname)?.into_iter()); } if l.starts_with(tag_start.as_bytes()) { @@ -497,17 +510,16 @@ impl ImapStream { } } - if let Some(capabilities) = capabilities { - Ok((capabilities, ret)) - } else { - /* sending CAPABILITY after LOGIN automatically is an RFC recommendation, so - * check for lazy servers */ - ret.send_command(CommandBody::Capability).await?; - ret.read_response(&mut res).await?; - let capabilities = protocol_parser::capabilities(&res)?.1; - let capabilities = HashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec())); - Ok((capabilities, ret)) + if got_new_capabilities { + return Ok((capabilities, ret)); } + + // sending CAPABILITY after LOGIN automatically is an RFC recommendation, so + // check for lazy servers. + ret.send_command(CommandBody::Capability).await?; + ret.read_response(&mut res).await?; + capabilities.extend(parse_capabilities(&res, &server_conf.server_hostname)?.into_iter()); + Ok((capabilities, ret)) } pub async fn read_response(&mut self, ret: &mut Vec) -> Result<()> { diff --git a/melib/src/imap/mod.rs b/melib/src/imap/mod.rs index 0e5fbcc2..9466e837 100644 --- a/melib/src/imap/mod.rs +++ b/melib/src/imap/mod.rs @@ -80,7 +80,7 @@ pub type MessageSequenceNumber = ImapNum; pub static SUPPORTED_CAPABILITIES: &[&str] = &[ "AUTH=ANONYMOUS", - "AUTH=OAUTH2", + "AUTH=XOAUTH2", "COMPRESS=DEFLATE", "CONDSTORE", "ENABLE", @@ -115,7 +115,7 @@ pub struct ImapServerConf { pub timeout: Option, } -type Capabilities = HashSet>; +type Capabilities = indexmap::IndexSet>; #[macro_export] macro_rules! get_conf_val {