use std::borrow::Borrow;
use std::convert::{TryFrom, TryInto};
use std::net::ToSocketAddrs;
use log::*;
use crate::error::{Error, Result};
use crate::raw::connection::{ConnectionConfig, NntpConnection};
use crate::raw::response::RawResponse;
use crate::types::command as cmd;
use crate::types::prelude::*;
#[derive(Debug)]
pub struct NntpClient {
conn: NntpConnection,
config: ClientConfig,
capabilities: Capabilities,
group: Option<Group>,
}
impl NntpClient {
pub fn conn(&mut self) -> &mut NntpConnection {
&mut self.conn
}
pub fn command(&mut self, c: impl NntpCommand) -> Result<RawResponse> {
let resp = self.conn.command(&c)?;
Ok(resp)
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
pub fn group(&self) -> Option<&Group> {
self.group.as_ref()
}
pub fn select_group(&mut self, name: impl AsRef<str>) -> Result<Group> {
let resp = self.conn.command(&cmd::Group(name.as_ref().to_string()))?;
match resp.code() {
ResponseCode::Known(Kind::GroupSelected) => {
let group = Group::try_from(&resp)?;
self.group = Some(group.clone());
Ok(group)
}
ResponseCode::Known(Kind::NoSuchNewsgroup) => Err(Error::failure(resp)),
code => Err(Error::Failure {
code,
msg: Some(format!("{}", resp.first_line_to_utf8_lossy())),
resp,
}),
}
}
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
pub fn update_capabilities(&mut self) -> Result<&Capabilities> {
let resp = self
.conn
.command(&cmd::Capabilities)?
.fail_unless(Kind::Capabilities)?;
let capabilities = Capabilities::try_from(&resp)?;
self.capabilities = capabilities;
Ok(&self.capabilities)
}
pub fn article(&mut self, article: cmd::Article) -> Result<BinaryArticle> {
let resp = self.conn.command(&article)?.fail_unless(Kind::Article)?;
resp.borrow().try_into()
}
pub fn body(&mut self, body: cmd::Body) -> Result<Body> {
let resp = self.conn.command(&body)?.fail_unless(Kind::Head)?;
resp.borrow().try_into()
}
pub fn head(&mut self, head: cmd::Head) -> Result<Head> {
let resp = self.conn.command(&head)?.fail_unless(Kind::Head)?;
resp.borrow().try_into()
}
pub fn stat(&mut self, stat: cmd::Stat) -> Result<Option<Stat>> {
let resp = self.conn.command(&stat)?;
match resp.code() {
ResponseCode::Known(Kind::ArticleExists) => resp.borrow().try_into().map(Some),
ResponseCode::Known(Kind::NoArticleWithMessageId)
| ResponseCode::Known(Kind::InvalidCurrentArticleNumber)
| ResponseCode::Known(Kind::NoArticleWithNumber) => Ok(None),
_ => Err(Error::failure(resp)),
}
}
pub fn close(&mut self) -> Result<RawResponse> {
let resp = self
.conn
.command(&cmd::Quit)?
.fail_unless(Kind::ConnectionClosing)?;
Ok(resp)
}
}
#[derive(Clone, Debug, Default)]
pub struct ClientConfig {
authinfo: Option<(String, String)>,
group: Option<String>,
conn_config: ConnectionConfig,
}
impl ClientConfig {
pub fn authinfo_user_pass(
&mut self,
username: impl AsRef<str>,
password: impl AsRef<str>,
) -> &mut Self {
self.authinfo = Some((username.as_ref().to_string(), password.as_ref().to_string()));
self
}
pub fn group(&mut self, name: Option<impl AsRef<str>>) -> &mut Self {
self.group = name.map(|s| s.as_ref().to_string());
self
}
pub fn default_tls(&mut self, domain: String) -> Result<&mut Self> {
self.conn_config.default_tls(domain)?;
Ok(self)
}
pub fn connection_config(&mut self, config: ConnectionConfig) -> &mut Self {
self.conn_config = config;
self
}
pub fn connect(&self, addr: impl ToSocketAddrs) -> Result<NntpClient> {
let (mut conn, conn_response) = NntpConnection::connect(addr, self.conn_config.clone())?;
debug!(
"Connected. Server returned `{}`",
conn_response.first_line_to_utf8_lossy()
);
if let Some((username, password)) = &self.authinfo {
if self.conn_config.tls_config.is_none() {
warn!("TLS is not enabled, credentials will be sent in the clear!");
}
debug!("Authenticating with AUTHINFO USER/PASS");
authenticate(&mut conn, username, password)?;
}
debug!("Retrieving capabilities...");
let capabilities = get_capabilities(&mut conn)?;
let group = if let Some(name) = &self.group {
debug!("Connecting to group {}...", name);
select_group(&mut conn, name)?.into()
} else {
debug!("No initial group specified");
None
};
Ok(NntpClient {
conn,
config: self.clone(),
capabilities,
group,
})
}
}
impl RawResponse {}
fn authenticate(
conn: &mut NntpConnection,
username: impl AsRef<str>,
password: impl AsRef<str>,
) -> Result<()> {
debug!("Sending AUTHINFO USER");
let user_resp = conn.command(&cmd::AuthInfo::User(username.as_ref().to_string()))?;
if user_resp.code != ResponseCode::from(381) {
return Err(Error::Failure {
code: user_resp.code,
resp: user_resp,
msg: Some("AUTHINFO USER failed".to_string()),
});
}
debug!("Sending AUTHINFO PASS");
let pass_resp = conn.command(&cmd::AuthInfo::Pass(password.as_ref().to_string()))?;
if pass_resp.code() != ResponseCode::Known(Kind::AuthenticationAccepted) {
return Err(Error::Failure {
code: pass_resp.code,
resp: pass_resp,
msg: Some("AUTHINFO PASS failed".to_string()),
});
}
debug!("Successfully authenticated");
Ok(())
}
fn get_capabilities(conn: &mut NntpConnection) -> Result<Capabilities> {
let resp = conn.command(&cmd::Capabilities)?;
if resp.code() != ResponseCode::Known(Kind::Capabilities) {
Err(Error::failure(resp))
} else {
Capabilities::try_from(&resp)
}
}
fn select_group(conn: &mut NntpConnection, group: impl AsRef<str>) -> Result<Group> {
let resp = conn.command(&cmd::Group(group.as_ref().to_string()))?;
match resp.code() {
ResponseCode::Known(Kind::GroupSelected) => Group::try_from(&resp),
ResponseCode::Known(Kind::NoSuchNewsgroup) => Err(Error::failure(resp)),
code => Err(Error::Failure {
code,
msg: Some(format!("{}", resp.first_line_to_utf8_lossy())),
resp,
}),
}
}