use base64;
use bufstream::BufStream;
use chrono::{DateTime, FixedOffset};
#[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream};
use nom;
use std::collections::HashSet;
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::ops::{Deref, DerefMut};
use std::str;
use std::sync::mpsc;
use super::authenticator::Authenticator;
use super::error::{Error, ParseError, Result, ValidateError};
use super::extensions;
use super::parse::*;
use super::types::*;
static TAG_PREFIX: &str = "a";
const INITIAL_TAG: u32 = 0;
const CR: u8 = 0x0d;
const LF: u8 = 0x0a;
macro_rules! quote {
($x:expr) => {
format!("\"{}\"", $x.replace(r"\", r"\\").replace("\"", "\\\""))
};
}
trait OptionExt<E> {
fn err(self) -> std::result::Result<(), E>;
}
impl<E> OptionExt<E> for Option<E> {
fn err(self) -> std::result::Result<(), E> {
match self {
Some(e) => Err(e),
None => Ok(()),
}
}
}
fn validate_str(value: &str) -> Result<String> {
validate_str_noquote(value)?;
Ok(quote!(value))
}
fn validate_str_noquote(value: &str) -> Result<&str> {
value
.matches(|c| c == '\n' || c == '\r')
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
}
fn validate_sequence_set(value: &str) -> Result<&str> {
value
.matches(|c: char| c.is_ascii_whitespace())
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
}
#[derive(Debug)]
pub struct Session<T: Read + Write> {
conn: Connection<T>,
unsolicited_responses_tx: mpsc::Sender<UnsolicitedResponse>,
pub unsolicited_responses: mpsc::Receiver<UnsolicitedResponse>,
}
#[derive(Debug)]
pub struct Client<T: Read + Write> {
conn: Connection<T>,
}
#[derive(Debug)]
#[doc(hidden)]
pub struct Connection<T: Read + Write> {
pub(crate) stream: BufStream<T>,
tag: u32,
pub debug: bool,
pub greeting_read: bool,
}
impl<T: Read + Write> Deref for Client<T> {
type Target = Connection<T>;
fn deref(&self) -> &Connection<T> {
&self.conn
}
}
impl<T: Read + Write> DerefMut for Client<T> {
fn deref_mut(&mut self) -> &mut Connection<T> {
&mut self.conn
}
}
impl<T: Read + Write> Deref for Session<T> {
type Target = Connection<T>;
fn deref(&self) -> &Connection<T> {
&self.conn
}
}
impl<T: Read + Write> DerefMut for Session<T> {
fn deref_mut(&mut self) -> &mut Connection<T> {
&mut self.conn
}
}
#[cfg(feature = "tls")]
pub fn connect<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
match TcpStream::connect(addr) {
Ok(stream) => {
let ssl_stream = match TlsConnector::connect(ssl_connector, domain.as_ref(), stream) {
Ok(s) => s,
Err(e) => return Err(Error::TlsHandshake(e)),
};
let mut socket = Client::new(ssl_stream);
socket.read_greeting()?;
Ok(socket)
}
Err(e) => Err(Error::Io(e)),
}
}
#[cfg(feature = "tls")]
pub fn connect_starttls<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
match TcpStream::connect(addr) {
Ok(stream) => {
let mut socket = Client::new(stream);
socket.read_greeting()?;
socket.run_command_and_check_ok("STARTTLS")?;
TlsConnector::connect(
ssl_connector,
domain.as_ref(),
socket.conn.stream.into_inner()?,
)
.map(Client::new)
.map_err(Error::TlsHandshake)
}
Err(e) => Err(Error::Io(e)),
}
}
impl Client<TcpStream> {
#[cfg(feature = "tls")]
pub fn secure<S: AsRef<str>>(
mut self,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
self.run_command_and_check_ok("STARTTLS")?;
TlsConnector::connect(
ssl_connector,
domain.as_ref(),
self.conn.stream.into_inner()?,
)
.map(Client::new)
.map_err(Error::TlsHandshake)
}
}
macro_rules! ok_or_unauth_client_err {
($r:expr, $self:expr) => {
match $r {
Ok(o) => o,
Err(e) => return Err((e, $self)),
}
};
}
impl<T: Read + Write> Client<T> {
pub fn new(stream: T) -> Client<T> {
Client {
conn: Connection {
stream: BufStream::new(stream),
tag: INITIAL_TAG,
debug: false,
greeting_read: false,
},
}
}
pub fn login<U: AsRef<str>, P: AsRef<str>>(
mut self,
username: U,
password: P,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
let u = ok_or_unauth_client_err!(validate_str(username.as_ref()), self);
let p = ok_or_unauth_client_err!(validate_str(password.as_ref()), self);
ok_or_unauth_client_err!(
self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)),
self
);
Ok(Session::new(self.conn))
}
pub fn authenticate<A: Authenticator, S: AsRef<str>>(
mut self,
auth_type: S,
authenticator: &A,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
ok_or_unauth_client_err!(
self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref())),
self
);
self.do_auth_handshake(authenticator)
}
fn do_auth_handshake<A: Authenticator>(
mut self,
authenticator: &A,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
loop {
let mut line = Vec::new();
ok_or_unauth_client_err!(self.readline(&mut line), self);
if line.starts_with(b"* ") {
continue;
}
if line.starts_with(b"+ ") || &line == b"+\r\n" {
let challenge = if &line == b"+\r\n" {
Vec::new()
} else {
let line_str = ok_or_unauth_client_err!(
match str::from_utf8(line.as_slice()) {
Ok(line_str) => Ok(line_str),
Err(e) => Err(Error::Parse(ParseError::DataNotUtf8(line, e))),
},
self
);
let data =
ok_or_unauth_client_err!(parse_authenticate_response(line_str), self);
ok_or_unauth_client_err!(
base64::decode(data).map_err(|e| Error::Parse(ParseError::Authentication(
data.to_string(),
Some(e)
))),
self
)
};
let raw_response = &authenticator.process(&challenge);
let auth_response = base64::encode(raw_response);
ok_or_unauth_client_err!(
self.write_line(auth_response.into_bytes().as_slice()),
self
);
} else {
ok_or_unauth_client_err!(self.read_response_onto(&mut line), self);
return Ok(Session::new(self.conn));
}
}
}
}
impl<T: Read + Write> Session<T> {
fn new(conn: Connection<T>) -> Self {
let (tx, rx) = mpsc::channel();
Session {
conn,
unsolicited_responses: rx,
unsolicited_responses_tx: tx,
}
}
pub fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
self.run_command_and_read_response(&format!(
"SELECT {}",
validate_str(mailbox_name.as_ref())?
))
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
}
pub fn examine<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
self.run_command_and_read_response(&format!(
"EXAMINE {}",
validate_str(mailbox_name.as_ref())?
))
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
}
pub fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ZeroCopyResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
if sequence_set.as_ref().is_empty() {
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
} else {
self.run_command_and_read_response(&format!(
"FETCH {} {}",
validate_sequence_set(sequence_set.as_ref())?,
validate_str_noquote(query.as_ref())?
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
}
pub fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> ZeroCopyResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
if uid_set.as_ref().is_empty() {
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
} else {
self.run_command_and_read_response(&format!(
"UID FETCH {} {}",
validate_sequence_set(uid_set.as_ref())?,
validate_str_noquote(query.as_ref())?
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
}
pub fn noop(&mut self) -> Result<()> {
self.run_command_and_read_response("NOOP")
.and_then(|lines| parse_noop(lines, &mut self.unsolicited_responses_tx))
}
pub fn logout(&mut self) -> Result<()> {
self.run_command_and_check_ok("LOGOUT")
}
pub fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
self.run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name.as_ref())?))
}
pub fn delete<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
self.run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name.as_ref())?))
}
pub fn rename<S1: AsRef<str>, S2: AsRef<str>>(&mut self, from: S1, to: S2) -> Result<()> {
self.run_command_and_check_ok(&format!(
"RENAME {} {}",
quote!(from.as_ref()),
quote!(to.as_ref())
))
}
pub fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox.as_ref())))
}
pub fn unsubscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox.as_ref())))
}
pub fn capabilities(&mut self) -> ZeroCopyResult<Capabilities> {
self.run_command_and_read_response("CAPABILITY")
.and_then(|lines| parse_capabilities(lines, &mut self.unsolicited_responses_tx))
}
pub fn expunge(&mut self) -> Result<Vec<Seq>> {
self.run_command_and_read_response("EXPUNGE")
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
}
pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Vec<Uid>> {
self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref()))
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
}
pub fn check(&mut self) -> Result<()> {
self.run_command_and_check_ok("CHECK")
}
pub fn close(&mut self) -> Result<()> {
self.run_command_and_check_ok("CLOSE")
}
pub fn store<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ZeroCopyResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
self.run_command_and_read_response(&format!(
"STORE {} {}",
sequence_set.as_ref(),
query.as_ref()
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
pub fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ZeroCopyResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
self.run_command_and_read_response(&format!(
"UID STORE {} {}",
uid_set.as_ref(),
query.as_ref()
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
pub fn copy<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
sequence_set: S1,
mailbox_name: S2,
) -> Result<()> {
self.run_command_and_check_ok(&format!(
"COPY {} {}",
sequence_set.as_ref(),
mailbox_name.as_ref()
))
}
pub fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> Result<()> {
self.run_command_and_check_ok(&format!(
"UID COPY {} {}",
uid_set.as_ref(),
mailbox_name.as_ref()
))
}
pub fn mv<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
sequence_set: S1,
mailbox_name: S2,
) -> Result<()> {
self.run_command_and_check_ok(&format!(
"MOVE {} {}",
sequence_set.as_ref(),
validate_str(mailbox_name.as_ref())?
))
}
pub fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> Result<()> {
self.run_command_and_check_ok(&format!(
"UID MOVE {} {}",
uid_set.as_ref(),
validate_str(mailbox_name.as_ref())?
))
}
pub fn list(
&mut self,
reference_name: Option<&str>,
mailbox_pattern: Option<&str>,
) -> ZeroCopyResult<Vec<Name>> {
self.run_command_and_read_response(&format!(
"LIST {} {}",
quote!(reference_name.unwrap_or("")),
mailbox_pattern.unwrap_or("\"\"")
))
.and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx))
}
pub fn lsub(
&mut self,
reference_name: Option<&str>,
mailbox_pattern: Option<&str>,
) -> ZeroCopyResult<Vec<Name>> {
self.run_command_and_read_response(&format!(
"LSUB {} {}",
quote!(reference_name.unwrap_or("")),
mailbox_pattern.unwrap_or("")
))
.and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx))
}
pub fn status<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
mailbox_name: S1,
data_items: S2,
) -> Result<Mailbox> {
self.run_command_and_read_response(&format!(
"STATUS {} {}",
validate_str(mailbox_name.as_ref())?,
data_items.as_ref()
))
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
}
pub fn idle(&mut self) -> Result<extensions::idle::Handle<'_, T>> {
extensions::idle::Handle::make(self)
}
pub fn append<S: AsRef<str>, B: AsRef<[u8]>>(&mut self, mailbox: S, content: B) -> Result<()> {
self.append_with_flags(mailbox, content, &[])
}
pub fn append_with_flags<S: AsRef<str>, B: AsRef<[u8]>>(
&mut self,
mailbox: S,
content: B,
flags: &[Flag<'_>],
) -> Result<()> {
self.append_with_flags_and_date(mailbox, content, flags, None)
}
pub fn append_with_flags_and_date<S: AsRef<str>, B: AsRef<[u8]>>(
&mut self,
mailbox: S,
content: B,
flags: &[Flag<'_>],
date: impl Into<Option<DateTime<FixedOffset>>>,
) -> Result<()> {
let content = content.as_ref();
let flagstr = flags
.iter()
.filter(|f| **f != Flag::Recent)
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(" ");
let datestr = match date.into() {
Some(date) => format!(" \"{}\"", date.format("%d-%h-%Y %T %z")),
None => "".to_string(),
};
self.run_command(&format!(
"APPEND \"{}\" ({}){} {{{}}}",
mailbox.as_ref(),
flagstr,
datestr,
content.len()
))?;
let mut v = Vec::new();
self.readline(&mut v)?;
if !v.starts_with(b"+") {
return Err(Error::Append);
}
self.stream.write_all(content)?;
self.stream.write_all(b"\r\n")?;
self.stream.flush()?;
self.read_response().map(|_| ())
}
pub fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
self.run_command_and_read_response(&format!("SEARCH {}", query.as_ref()))
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
}
pub fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
self.run_command_and_read_response(&format!("UID SEARCH {}", query.as_ref()))
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
}
pub fn run_command_and_check_ok<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
self.run_command_and_read_response(command).map(|_| ())
}
pub fn run_command<S: AsRef<str>>(&mut self, untagged_command: S) -> Result<()> {
self.conn.run_command(untagged_command.as_ref())
}
pub fn run_command_and_read_response<S: AsRef<str>>(
&mut self,
untagged_command: S,
) -> Result<Vec<u8>> {
self.conn
.run_command_and_read_response(untagged_command.as_ref())
}
}
impl<T: Read + Write> Connection<T> {
pub fn read_greeting(&mut self) -> Result<Vec<u8>> {
assert!(!self.greeting_read, "Greeting can only be read once");
let mut v = Vec::new();
self.readline(&mut v)?;
self.greeting_read = true;
Ok(v)
}
fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
self.run_command_and_read_response(command).map(|_| ())
}
fn run_command(&mut self, untagged_command: &str) -> Result<()> {
let command = self.create_command(untagged_command);
self.write_line(command.into_bytes().as_slice())
}
fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result<Vec<u8>> {
self.run_command(untagged_command)?;
self.read_response()
}
pub(crate) fn read_response(&mut self) -> Result<Vec<u8>> {
let mut v = Vec::new();
self.read_response_onto(&mut v)?;
Ok(v)
}
pub(crate) fn read_response_onto(&mut self, data: &mut Vec<u8>) -> Result<()> {
let mut continue_from = None;
let mut try_first = !data.is_empty();
let match_tag = format!("{}{}", TAG_PREFIX, self.tag);
loop {
let line_start = if try_first {
try_first = false;
0
} else {
let start_new = data.len();
self.readline(data)?;
continue_from.take().unwrap_or(start_new)
};
let break_with = {
use imap_proto::{parse_response, Response, Status};
let line = &data[line_start..];
match parse_response(line) {
Ok((
_,
Response::Done {
tag,
status,
information,
..
},
)) => {
assert_eq!(tag.as_bytes(), match_tag.as_bytes());
Some(match status {
Status::Bad | Status::No => {
Err((status, information.map(ToString::to_string)))
}
Status::Ok => Ok(()),
status => Err((status, None)),
})
}
Ok((..)) => None,
Err(nom::Err::Incomplete(..)) => {
continue_from = Some(line_start);
None
}
_ => Some(Err((Status::Bye, None))),
}
};
match break_with {
Some(Ok(_)) => {
data.truncate(line_start);
break Ok(());
}
Some(Err((status, expl))) => {
use imap_proto::Status;
match status {
Status::Bad => {
break Err(Error::Bad(
expl.unwrap_or_else(|| "no explanation given".to_string()),
));
}
Status::No => {
break Err(Error::No(
expl.unwrap_or_else(|| "no explanation given".to_string()),
));
}
_ => break Err(Error::Parse(ParseError::Invalid(data.split_off(0)))),
}
}
None => {}
}
}
}
pub(crate) fn readline(&mut self, into: &mut Vec<u8>) -> Result<usize> {
use std::io::BufRead;
let read = self.stream.read_until(LF, into)?;
if read == 0 {
return Err(Error::ConnectionLost);
}
if self.debug {
let len = into.len();
let line = &into[(len - read)..(len - 2)];
eprint!("S: {}\n", String::from_utf8_lossy(line));
}
Ok(read)
}
fn create_command(&mut self, command: &str) -> String {
self.tag += 1;
format!("{}{} {}", TAG_PREFIX, self.tag, command)
}
pub(crate) fn write_line(&mut self, buf: &[u8]) -> Result<()> {
self.stream.write_all(buf)?;
self.stream.write_all(&[CR, LF])?;
self.stream.flush()?;
if self.debug {
eprint!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::error::Result;
use super::super::mock_stream::MockStream;
use super::*;
macro_rules! mock_session {
($s:expr) => {
Session::new(Client::new($s).conn)
};
}
#[test]
fn read_response() {
let response = "a0 OK Logged in.\r\n";
let mock_stream = MockStream::new(response.as_bytes().to_vec());
let mut client = Client::new(mock_stream);
let actual_response = client.read_response().unwrap();
assert_eq!(Vec::<u8>::new(), actual_response);
}
#[test]
fn fetch_body() {
let response = "a0 OK Logged in.\r\n\
* 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\
a0 OK FETCH completed\r\n";
let mock_stream = MockStream::new(response.as_bytes().to_vec());
let mut session = mock_session!(mock_stream);
session.read_response().unwrap();
session.read_response().unwrap();
}
#[test]
fn read_greeting() {
let greeting = "* OK Dovecot ready.\r\n";
let mock_stream = MockStream::new(greeting.as_bytes().to_vec());
let mut client = Client::new(mock_stream);
client.read_greeting().unwrap();
}
#[test]
fn readline_delay_read() {
let greeting = "* OK Dovecot ready.\r\n";
let expected_response: String = greeting.to_string();
let mock_stream = MockStream::default()
.with_buf(greeting.as_bytes().to_vec())
.with_delay();
let mut client = Client::new(mock_stream);
let mut v = Vec::new();
client.readline(&mut v).unwrap();
let actual_response = String::from_utf8(v).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn readline_eof() {
let mock_stream = MockStream::default().with_eof();
let mut client = Client::new(mock_stream);
let mut v = Vec::new();
if let Err(Error::ConnectionLost) = client.readline(&mut v) {
} else {
unreachable!("EOF read did not return connection lost");
}
}
#[test]
#[should_panic]
fn readline_err() {
let mock_stream = MockStream::default().with_err();
let mut client = Client::new(mock_stream);
let mut v = Vec::new();
client.readline(&mut v).unwrap();
}
#[test]
fn create_command() {
let base_command = "CHECK";
let mock_stream = MockStream::default();
let mut imap_stream = Client::new(mock_stream);
let expected_command = format!("a1 {}", base_command);
let command = imap_stream.create_command(&base_command);
assert!(
command == expected_command,
"expected command doesn't equal actual command"
);
let expected_command2 = format!("a2 {}", base_command);
let command2 = imap_stream.create_command(&base_command);
assert!(
command2 == expected_command2,
"expected command doesn't equal actual command"
);
}
#[test]
fn authenticate() {
let response = b"+ YmFy\r\n\
a1 OK Logged in\r\n"
.to_vec();
let command = "a1 AUTHENTICATE PLAIN\r\n\
Zm9v\r\n";
let mock_stream = MockStream::new(response);
let client = Client::new(mock_stream);
enum Authenticate {
Auth,
};
impl Authenticator for Authenticate {
type Response = Vec<u8>;
fn process(&self, challenge: &[u8]) -> Self::Response {
assert!(challenge == b"bar", "Invalid authenticate challenge");
b"foo".to_vec()
}
}
let session = client.authenticate("PLAIN", &Authenticate::Auth).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid authenticate command"
);
}
#[test]
fn login() {
let response = b"a1 OK Logged in\r\n".to_vec();
let username = "username";
let password = "password";
let command = format!("a1 LOGIN {} {}\r\n", quote!(username), quote!(password));
let mock_stream = MockStream::new(response);
let client = Client::new(mock_stream);
let session = client.login(username, password).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid login command"
);
}
#[test]
fn logout() {
let response = b"a1 OK Logout completed.\r\n".to_vec();
let command = format!("a1 LOGOUT\r\n");
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.logout().unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid logout command"
);
}
#[test]
fn rename() {
let response = b"a1 OK RENAME completed\r\n".to_vec();
let current_mailbox_name = "INBOX";
let new_mailbox_name = "NEWINBOX";
let command = format!(
"a1 RENAME {} {}\r\n",
quote!(current_mailbox_name),
quote!(new_mailbox_name)
);
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session
.rename(current_mailbox_name, new_mailbox_name)
.unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid rename command"
);
}
#[test]
fn subscribe() {
let response = b"a1 OK SUBSCRIBE completed\r\n".to_vec();
let mailbox = "INBOX";
let command = format!("a1 SUBSCRIBE {}\r\n", quote!(mailbox));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.subscribe(mailbox).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid subscribe command"
);
}
#[test]
fn unsubscribe() {
let response = b"a1 OK UNSUBSCRIBE completed\r\n".to_vec();
let mailbox = "INBOX";
let command = format!("a1 UNSUBSCRIBE {}\r\n", quote!(mailbox));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.unsubscribe(mailbox).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid unsubscribe command"
);
}
#[test]
fn expunge() {
let response = b"a1 OK EXPUNGE completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.expunge().unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 EXPUNGE\r\n".to_vec(),
"Invalid expunge command"
);
}
#[test]
fn uid_expunge() {
let response = b"* 2 EXPUNGE\r\n\
* 3 EXPUNGE\r\n\
* 4 EXPUNGE\r\n\
a1 OK UID EXPUNGE completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.uid_expunge("2:4").unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 UID EXPUNGE 2:4\r\n".to_vec(),
"Invalid expunge command"
);
}
#[test]
fn check() {
let response = b"a1 OK CHECK completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.check().unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 CHECK\r\n".to_vec(),
"Invalid check command"
);
}
#[test]
fn examine() {
let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
* OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\
* 1 EXISTS\r\n\
* 1 RECENT\r\n\
* OK [UNSEEN 1] First unseen.\r\n\
* OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
* OK [UIDNEXT 2] Predicted next UID\r\n\
a1 OK [READ-ONLY] Select completed.\r\n"
.to_vec();
let expected_mailbox = Mailbox {
flags: vec![
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Seen,
Flag::Draft,
],
exists: 1,
recent: 1,
unseen: Some(1),
permanent_flags: vec![],
uid_next: Some(2),
uid_validity: Some(1257842737),
};
let mailbox_name = "INBOX";
let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let mailbox = session.examine(mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid examine command"
);
assert_eq!(mailbox, expected_mailbox);
}
#[test]
fn select() {
let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
* OK [PERMANENTFLAGS (\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)] \
Read-only mailbox.\r\n\
* 1 EXISTS\r\n\
* 1 RECENT\r\n\
* OK [UNSEEN 1] First unseen.\r\n\
* OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
* OK [UIDNEXT 2] Predicted next UID\r\n\
a1 OK [READ-ONLY] Select completed.\r\n"
.to_vec();
let expected_mailbox = Mailbox {
flags: vec![
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Seen,
Flag::Draft,
],
exists: 1,
recent: 1,
unseen: Some(1),
permanent_flags: vec![
Flag::MayCreate,
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Draft,
Flag::Seen,
],
uid_next: Some(2),
uid_validity: Some(1257842737),
};
let mailbox_name = "INBOX";
let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let mailbox = session.select(mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid select command"
);
assert_eq!(mailbox, expected_mailbox);
}
#[test]
fn search() {
let response = b"* SEARCH 1 2 3 4 5\r\n\
a1 OK Search completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let ids = session.search("Unseen").unwrap();
let ids: HashSet<u32> = ids.iter().cloned().collect();
assert!(
session.stream.get_ref().written_buf == b"a1 SEARCH Unseen\r\n".to_vec(),
"Invalid search command"
);
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
}
#[test]
fn uid_search() {
let response = b"* SEARCH 1 2 3 4 5\r\n\
a1 OK Search completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let ids = session.uid_search("Unseen").unwrap();
let ids: HashSet<Uid> = ids.iter().cloned().collect();
assert!(
session.stream.get_ref().written_buf == b"a1 UID SEARCH Unseen\r\n".to_vec(),
"Invalid search command"
);
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
}
#[test]
fn capability() {
let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\
a1 OK CAPABILITY completed\r\n"
.to_vec();
let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let capabilities = session.capabilities().unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(),
"Invalid capability command"
);
assert_eq!(capabilities.len(), 4);
for e in expected_capabilities {
assert!(capabilities.has_str(e));
}
}
#[test]
fn create() {
let response = b"a1 OK CREATE completed\r\n".to_vec();
let mailbox_name = "INBOX";
let command = format!("a1 CREATE {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.create(mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid create command"
);
}
#[test]
fn delete() {
let response = b"a1 OK DELETE completed\r\n".to_vec();
let mailbox_name = "INBOX";
let command = format!("a1 DELETE {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.delete(mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid delete command"
);
}
#[test]
fn noop() {
let response = b"a1 OK NOOP completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.noop().unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 NOOP\r\n".to_vec(),
"Invalid noop command"
);
}
#[test]
fn close() {
let response = b"a1 OK CLOSE completed\r\n".to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.close().unwrap();
assert!(
session.stream.get_ref().written_buf == b"a1 CLOSE\r\n".to_vec(),
"Invalid close command"
);
}
#[test]
fn store() {
generic_store(" ", |c, set, query| c.store(set, query));
}
#[test]
fn uid_store() {
generic_store(" UID ", |c, set, query| c.uid_store(set, query));
}
fn generic_store<F, T>(prefix: &str, op: F)
where
F: FnOnce(&mut Session<MockStream>, &str, &str) -> Result<T>,
{
let res = "* 2 FETCH (FLAGS (\\Deleted \\Seen))\r\n\
* 3 FETCH (FLAGS (\\Deleted))\r\n\
* 4 FETCH (FLAGS (\\Deleted \\Flagged \\Seen))\r\n\
a1 OK STORE completed\r\n";
generic_with_uid(res, "STORE", "2.4", "+FLAGS (\\Deleted)", prefix, op);
}
#[test]
fn copy() {
generic_copy(" ", |c, set, query| c.copy(set, query))
}
#[test]
fn uid_copy() {
generic_copy(" UID ", |c, set, query| c.uid_copy(set, query))
}
fn generic_copy<F, T>(prefix: &str, op: F)
where
F: FnOnce(&mut Session<MockStream>, &str, &str) -> Result<T>,
{
generic_with_uid(
"OK COPY completed\r\n",
"COPY",
"2:4",
"MEETING",
prefix,
op,
);
}
#[test]
fn mv() {
let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
* 2 EXPUNGE\r\n\
* 1 EXPUNGE\r\n\
a1 OK Move completed\r\n"
.to_vec();
let mailbox_name = "MEETING";
let command = format!("a1 MOVE 1:2 {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.mv("1:2", mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid move command"
);
}
#[test]
fn uid_mv() {
let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
* 2 EXPUNGE\r\n\
* 1 EXPUNGE\r\n\
a1 OK Move completed\r\n"
.to_vec();
let mailbox_name = "MEETING";
let command = format!("a1 UID MOVE 41:42 {}\r\n", quote!(mailbox_name));
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
session.uid_mv("41:42", mailbox_name).unwrap();
assert!(
session.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid uid move command"
);
}
#[test]
fn fetch() {
generic_fetch(" ", |c, seq, query| c.fetch(seq, query))
}
#[test]
fn uid_fetch() {
generic_fetch(" UID ", |c, seq, query| c.uid_fetch(seq, query))
}
fn generic_fetch<F, T>(prefix: &str, op: F)
where
F: FnOnce(&mut Session<MockStream>, &str, &str) -> Result<T>,
{
generic_with_uid("OK FETCH completed\r\n", "FETCH", "1", "BODY[]", prefix, op);
}
fn generic_with_uid<F, T>(res: &str, cmd: &str, seq: &str, query: &str, prefix: &str, op: F)
where
F: FnOnce(&mut Session<MockStream>, &str, &str) -> Result<T>,
{
let resp = format!("a1 {}\r\n", res).as_bytes().to_vec();
let line = format!("a1{}{} {} {}\r\n", prefix, cmd, seq, query);
let mut session = mock_session!(MockStream::new(resp));
let _ = op(&mut session, seq, query);
assert!(
session.stream.get_ref().written_buf == line.as_bytes().to_vec(),
"Invalid command"
);
}
#[test]
fn quote_backslash() {
assert_eq!("\"test\\\\text\"", quote!(r"test\text"));
}
#[test]
fn quote_dquote() {
assert_eq!("\"test\\\"text\"", quote!("test\"text"));
}
#[test]
fn validate_random() {
assert_eq!(
"\"~iCQ_k;>[&\\\"sVCvUW`e<<P!wJ\"",
&validate_str("~iCQ_k;>[&\"sVCvUW`e<<P!wJ").unwrap()
);
}
#[test]
fn validate_newline() {
if let Err(ref e) = validate_str("test\nstring") {
if let &Error::Validate(ref ve) = e {
if ve.0 == '\n' {
return;
}
}
panic!("Wrong error: {:?}", e);
}
panic!("No error");
}
#[test]
#[allow(unreachable_patterns)]
fn validate_carriage_return() {
if let Err(ref e) = validate_str("test\rstring") {
if let &Error::Validate(ref ve) = e {
if ve.0 == '\r' {
return;
}
}
panic!("Wrong error: {:?}", e);
}
panic!("No error");
}
}