diff --git a/melib/src/backends/nntp.rs b/melib/src/backends/nntp.rs index a0a8a960..47bea20e 100644 --- a/melib/src/backends/nntp.rs +++ b/melib/src/backends/nntp.rs @@ -36,7 +36,7 @@ use crate::async_workers::{Async, WorkContext}; use crate::backends::*; use crate::conf::AccountSettings; use crate::email::*; -use crate::error::{MeliError, Result}; +use crate::error::{MeliError, Result, ResultIntoMeliError}; use futures::lock::Mutex as FutureMutex; use futures::stream::Stream; use std::collections::{hash_map::DefaultHasher, BTreeMap}; @@ -59,10 +59,11 @@ pub static SUPPORTED_CAPABILITIES: &[&str] = &[ pub struct NntpServerConf { pub server_hostname: String, pub server_username: String, - //pub server_password: String, + pub server_password: String, pub server_port: u16, pub use_starttls: bool, pub use_tls: bool, + pub require_auth: bool, pub danger_accept_invalid_certs: bool, pub extension_use: NntpExtensionUse, } @@ -404,14 +405,24 @@ impl NntpType { }; */ let server_port = get_conf_val!(s["server_port"], 119)?; - let use_tls = get_conf_val!(s["use_tls"], true)?; - let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 993))?; + let use_tls = get_conf_val!(s["use_tls"], server_port == 563)?; + let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 563))?; let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?; + let require_auth = get_conf_val!(s["require_auth"], true)?; let server_conf = NntpServerConf { server_hostname: server_hostname.to_string(), - server_username: String::new(), - //server_password, + server_username: if require_auth { + get_conf_val!(s["server_username"])?.to_string() + } else { + get_conf_val!(s["server_username"], String::new())? + }, + server_password: if require_auth { + get_conf_val!(s["server_password"])?.to_string() + } else { + get_conf_val!(s["server_password"], String::new())? + }, + require_auth, server_port, use_tls, use_starttls, @@ -451,7 +462,7 @@ impl NntpType { let uid_store: Arc = Arc::new(UIDStore { account_hash, account_name, - offline_cache: get_conf_val!(s["X_header_caching"], false)?, + offline_cache: false, //get_conf_val!(s["X_header_caching"], false)?, mailboxes: Arc::new(FutureMutex::new(mailboxes)), ..UIDStore::default() }); @@ -482,13 +493,14 @@ impl NntpType { }) }; conn.send_command(command.as_bytes()).await?; - conn.read_response(&mut res, true).await?; - if !res.starts_with("215 ") { - return Err(MeliError::new(format!( - "Could not get newsgroups {}: expected LIST ACTIVE response but got: {}", - &conn.uid_store.account_name, res - ))); - } + conn.read_response(&mut res, true, &["215 "]) + .await + .chain_err_summary(|| { + format!( + "Could not get newsgroups {}: expected LIST ACTIVE response but got: {}", + &conn.uid_store.account_name, res + ) + })?; debug!(&res); let mut mailboxes_lck = conn.uid_store.mailboxes.lock().await; for l in res.split_rn().skip(1) { @@ -517,14 +529,23 @@ impl NntpType { ))); } let server_port = get_conf_val!(s["server_port"], 119)?; - let use_tls = get_conf_val!(s["use_tls"], true)?; - let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 993))?; + let use_tls = get_conf_val!(s["use_tls"], server_port == 563)?; + let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 563))?; if !use_tls && use_starttls { return Err(MeliError::new(format!( "Configuration error ({}): incompatible use_tls and use_starttls values: use_tls = false, use_starttls = true", s.name.as_str(), ))); } + #[cfg(feature = "deflate_compression")] + get_conf_val!(s["use_deflate"], true)?; + #[cfg(not(feature = "deflate_compression"))] + if s.extra.contains_key("use_deflate") { + return Err(MeliError::new(format!( + "Configuration error ({}): setting `use_deflate` is set but this version of meli isn't compiled with DEFLATE support.", + s.name.as_str(), + ))); + } get_conf_val!(s["danger_accept_invalid_certs"], false)?; Ok(()) } @@ -552,13 +573,14 @@ async fn fetch_envs( .to_string(); conn.send_command(format!("GROUP {}", path).as_bytes()) .await?; - conn.read_response(&mut res, false).await?; - if !res.starts_with("211 ") { - return Err(MeliError::new(format!( - "{} Could not select newsgroup {}: expected GROUP response but got: {}", - &uid_store.account_name, path, res - ))); - } + conn.read_response(&mut res, false, &["211 "]) + .await + .chain_err_summary(|| { + format!( + "{} Could not select newsgroup {}: expected GROUP response but got: {}", + &uid_store.account_name, path, res + ) + })?; /* * Parameters group Name of newsgroup @@ -578,22 +600,22 @@ async fn fetch_envs( let high = usize::from_str(&s[3]).unwrap_or(0); drop(s); - conn.send_command(format!("OVER {}-{}", high.saturating_sub(250), high).as_bytes()) + conn.send_command(format!("OVER {}-{}", high.saturating_sub(100), high).as_bytes()) .await?; - conn.read_response(&mut res, true).await?; - if !res.starts_with("224 ") { - return Err(MeliError::new(format!( - "{} Could not select newsgroup {}: expected OVER response but got: {}", - &uid_store.account_name, path, res - ))); - } + conn.read_response(&mut res, true, &["224 "]) + .await + .chain_err_summary(|| { + format!( + "{} Could not select newsgroup {}: expected OVER response but got: {}", + &uid_store.account_name, path, res + ) + })?; let mut ret = Vec::with_capacity(total); //hash_index: Arc>>, //uid_index: Arc>>, let mut hash_index_lck = uid_store.hash_index.lock().unwrap(); let mut uid_index_lck = uid_store.uid_index.lock().unwrap(); for l in res.split_rn().skip(1) { - debug!(&l); let (_, (num, env)) = debug!(protocol_parser::over_article(&l))?; hash_index_lck.insert(env.hash(), (num, mailbox_hash)); uid_index_lck.insert((mailbox_hash, num), env.hash()); diff --git a/melib/src/backends/nntp/connection.rs b/melib/src/backends/nntp/connection.rs index 4f9ad43a..82e55dd3 100644 --- a/melib/src/backends/nntp/connection.rs +++ b/melib/src/backends/nntp/connection.rs @@ -103,27 +103,6 @@ impl NntpStream { current_mailbox: MailboxSelection::None, }; - ret.read_response(&mut res, false).await?; - ret.send_command(b"CAPABILITIES").await?; - ret.read_response(&mut res, true).await?; - if !res.starts_with("101 ") { - return Err(MeliError::new(format!( - "Could not connect to {}: expected CAPABILITIES response but got:{}", - &server_conf.server_hostname, res - ))); - } - let capabilities: Vec<&str> = res.lines().skip(1).collect(); - - if !capabilities - .iter() - .any(|cap| cap.eq_ignore_ascii_case("VERSION 2")) - { - return Err(MeliError::new(format!( - "Could not connect to {}: server is not NNTP compliant", - &server_conf.server_hostname - ))); - } - if server_conf.use_tls { let mut connector = TlsConnector::builder(); if server_conf.danger_accept_invalid_certs { @@ -134,6 +113,37 @@ impl NntpStream { .chain_err_kind(crate::error::ErrorKind::Network)?; if server_conf.use_starttls { + ret.read_response(&mut res, false, &["200 ", "201 "]) + .await?; + ret.send_command(b"CAPABILITIES").await?; + ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES")) + .await?; + if !res.starts_with("101 ") { + return Err(MeliError::new(format!( + "Could not connect to {}: expected CAPABILITIES response but got:{}", + &server_conf.server_hostname, res + ))); + } + let capabilities: Vec<&str> = res.lines().skip(1).collect(); + + if !capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case("VERSION 2")) + { + return Err(MeliError::new(format!( + "Could not connect to {}: server is not NNTP compliant", + &server_conf.server_hostname + ))); + } + if !capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case("STARTTLS")) + { + return Err(MeliError::new(format!( + "Could not connect to {}: server does not support STARTTLS", + &server_conf.server_hostname + ))); + } ret.stream .write_all(b"STARTTLS\r\n") .await @@ -142,7 +152,8 @@ impl NntpStream { .flush() .await .chain_err_kind(crate::error::ErrorKind::Network)?; - ret.read_response(&mut res, false).await?; + ret.read_response(&mut res, false, command_to_replycodes("STARTTLS")) + .await?; if !res.starts_with("382 ") { return Err(MeliError::new(format!( "Could not connect to {}: could not begin TLS negotiation, got: {}", @@ -192,8 +203,11 @@ impl NntpStream { //) //.await?; + ret.read_response(&mut res, false, &["200 ", "201 "]) + .await?; ret.send_command(b"CAPABILITIES").await?; - ret.read_response(&mut res, true).await?; + ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES")) + .await?; if !res.starts_with("101 ") { return Err(MeliError::new(format!( "Could not connect to {}: expected CAPABILITIES response but got:{}", @@ -201,46 +215,92 @@ impl NntpStream { ))); } let capabilities: HashSet = res.lines().skip(1).map(|l| l.to_string()).collect(); - #[cfg(feature = "deflate_compression")] + if !capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case("VERSION 2")) + { + return Err(MeliError::new(format!( + "Could not connect to {}: server is not NNTP compliant", + &server_conf.server_hostname + ))); + } + + if server_conf.require_auth { + if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) { + ret.send_command( + format!("AUTHINFO USER {}", server_conf.server_username).as_bytes(), + ) + .await?; + ret.read_response(&mut res, false, command_to_replycodes("AUTHINFO USER")) + .await + .chain_err_summary(|| format!("Authentication state error: {}", res)) + .chain_err_kind(ErrorKind::Authentication)?; + if res.starts_with("381 ") { + ret.send_command( + format!("AUTHINFO PASS {}", server_conf.server_password).as_bytes(), + ) + .await?; + ret.read_response(&mut res, false, command_to_replycodes("AUTHINFO PASS")) + .await + .chain_err_summary(|| format!("Authentication state error: {}", res)) + .chain_err_kind(ErrorKind::Authentication)?; + } + } else { + return Err(MeliError::new(format!( + "Could not connect: no supported auth mechanisms in server capabilities: {:?}", + capabilities + )) + .set_err_kind(ErrorKind::Authentication)); + } + } + #[cfg(feature = "deflate_compression")] if capabilities.contains("COMPRESS DEFLATE") && ret.extension_use.deflate { ret.send_command(b"COMPRESS DEFLATE").await?; - ret.read_response(&mut res, false).await?; - if !res.starts_with("206 ") { - crate::log( + ret.read_response(&mut res, false, command_to_replycodes("COMPRESS DEFLATE")) + .await + .chain_err_summary(|| { format!( - "Could not use COMPRESS=DEFLATE in account `{}`: server replied with `{}`", + "Could not use COMPRESS DEFLATE in account `{}`: server replied with `{}`", server_conf.server_hostname, res - ), - crate::LoggingLevel::WARN, - ); - } else { - let NntpStream { - stream, + ) + })?; + let NntpStream { + stream, + extension_use, + current_mailbox, + } = ret; + let stream = stream.into_inner()?; + return Ok(( + capabilities, + NntpStream { + stream: AsyncWrapper::new(stream.deflate())?, extension_use, current_mailbox, - } = ret; - let stream = stream.into_inner()?; - return Ok(( - capabilities, - NntpStream { - stream: AsyncWrapper::new(stream.deflate())?, - extension_use, - current_mailbox, - }, - )); - } + }, + )); } Ok((capabilities, ret)) } - pub async fn read_response(&mut self, ret: &mut String, is_multiline: bool) -> Result<()> { - self.read_lines(ret, is_multiline).await?; + pub async fn read_response( + &mut self, + ret: &mut String, + is_multiline: bool, + expected_reply_code: &[&str], + ) -> Result<()> { + self.read_lines(ret, is_multiline, expected_reply_code) + .await?; Ok(()) } - pub async fn read_lines(&mut self, ret: &mut String, is_multiline: bool) -> Result<()> { + pub async fn read_lines( + &mut self, + ret: &mut String, + is_multiline: bool, + expected_reply_code: &[&str], + ) -> Result<()> { let mut buf: Vec = vec![0; Connection::IO_BUF_SIZE]; ret.clear(); let mut last_line_idx: usize = 0; @@ -249,6 +309,24 @@ impl NntpStream { Ok(0) => break, Ok(b) => { ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) }); + if ret.len() > 4 { + if ret.starts_with("205 ") { + return Err(MeliError::new(format!("Disconnected: {}", ret))); + } else if ret.starts_with("501 ") || ret.starts_with("500 ") { + return Err(MeliError::new(format!("Syntax error: {}", ret))); + } else if ret.starts_with("403 ") { + return Err(MeliError::new(format!("Internal error: {}", ret))); + } else if ret.starts_with("502 ") + || ret.starts_with("480 ") + || ret.starts_with("483 ") + || ret.starts_with("401 ") + { + return Err(MeliError::new(format!("Connection state error: {}", ret)) + .set_err_kind(ErrorKind::Authentication)); + } else if !expected_reply_code.iter().any(|r| ret.starts_with(r)) { + return Err(MeliError::new(format!("Unexpected reply code: {}", ret))); + } + } if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") { if !is_multiline { break; @@ -256,9 +334,6 @@ impl NntpStream { ret.replace_range(pos + "\r\n".len()..pos + "\r\n.\r\n".len(), ""); break; } - if ret[last_line_idx..].starts_with("205 ") { - return Err(MeliError::new(format!("Disconnected: {}", ret))); - } if let Some(prev_line) = ret[last_line_idx..pos + last_line_idx].rfind("\r\n") { @@ -339,15 +414,27 @@ impl NntpConnection { &'a mut self, ret: &'a mut String, is_multiline: bool, + expected_reply_code: &'static [&str], ) -> Pin> + Send + 'a>> { Box::pin(async move { ret.clear(); - self.stream.as_mut()?.read_response(ret, is_multiline).await + self.stream + .as_mut()? + .read_response(ret, is_multiline, expected_reply_code) + .await }) } - pub async fn read_lines(&mut self, ret: &mut String, is_multiline: bool) -> Result<()> { - self.stream.as_mut()?.read_lines(ret, is_multiline).await?; + pub async fn read_lines( + &mut self, + ret: &mut String, + is_multiline: bool, + expected_reply_code: &[&str], + ) -> Result<()> { + self.stream + .as_mut()? + .read_lines(ret, is_multiline, expected_reply_code) + .await?; Ok(()) } @@ -377,3 +464,31 @@ impl NntpConnection { } } } + +fn command_to_replycodes(c: &str) -> &'static [&'static str] { + if c.starts_with("OVER") { + &["224 "] + } else if c.starts_with("LIST") { + &["215 "] + } else if c.starts_with("STARTTLS") { + &["382 "] + } else if c.starts_with("GROUP") { + &["211 "] + } else if c.starts_with("CAPABILITIES") { + &["101 "] + } else if c.starts_with("ARTICLE") { + &["220 "] + } else if c.starts_with("DATE") { + &["111 "] + } else if c.starts_with("NEWNEWS") { + &["230 "] + } else if c.starts_with("AUTHINFO USER") { + &["281 ", "381 "] + } else if c.starts_with("AUTHINFO PASS") { + &["281 "] + } else if c.starts_with("COMPRESS DEFLATE") { + &["206 "] + } else { + &[] + } +} diff --git a/melib/src/backends/nntp/operations.rs b/melib/src/backends/nntp/operations.rs index 0dc60d22..61895e0e 100644 --- a/melib/src/backends/nntp/operations.rs +++ b/melib/src/backends/nntp/operations.rs @@ -65,7 +65,7 @@ impl BackendOp for NntpOp { .to_string(); conn.send_command(format!("GROUP {}", path).as_bytes()) .await?; - conn.read_response(&mut res, false).await?; + conn.read_response(&mut res, false, &["211 "]).await?; if !res.starts_with("211 ") { return Err(MeliError::new(format!( "{} Could not select newsgroup {}: expected GROUP response but got: {}", @@ -74,7 +74,7 @@ impl BackendOp for NntpOp { } conn.send_command(format!("ARTICLE {}", uid).as_bytes()) .await?; - conn.read_response(&mut res, true).await?; + conn.read_response(&mut res, true, &["220 "]).await?; if !res.starts_with("220 ") { return Err(MeliError::new(format!( "{} Could not select article {}: expected ARTICLE response but got: {}",