2021-08-09 04:30:08 +00:00
|
|
|
use std::pin::Pin;
|
|
|
|
use std::task::{Context, Poll};
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
use futures::{
|
|
|
|
future::{self, FutureExt},
|
|
|
|
stream::{Stream, StreamExt},
|
|
|
|
};
|
|
|
|
use tokio::{net::TcpStream, sync::mpsc};
|
2021-08-09 05:36:10 +00:00
|
|
|
use tokio_rustls::client::TlsStream;
|
2021-08-08 03:33:37 +00:00
|
|
|
|
2021-08-09 04:30:08 +00:00
|
|
|
use crate::proto::{
|
2021-08-09 05:36:10 +00:00
|
|
|
bytes::Bytes,
|
2021-08-09 04:30:08 +00:00
|
|
|
command::{
|
|
|
|
Command, CommandFetch, CommandList, CommandSearch, CommandSelect, FetchItems,
|
|
|
|
SearchCriteria,
|
|
|
|
},
|
|
|
|
response::{
|
|
|
|
Condition, Flag, Mailbox, MailboxData, MailboxList, MessageAttribute, Response,
|
|
|
|
ResponseCode, Status,
|
|
|
|
},
|
|
|
|
};
|
2021-08-08 03:33:37 +00:00
|
|
|
|
2021-08-09 04:30:08 +00:00
|
|
|
use super::inner::Inner;
|
|
|
|
use super::response_stream::ResponseStream;
|
2021-08-09 05:36:10 +00:00
|
|
|
use super::upgrade::upgrade;
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
/// An IMAP client that hasn't been connected yet.
|
|
|
|
#[derive(Builder, Clone, Debug)]
|
2021-08-09 04:30:08 +00:00
|
|
|
#[builder(build_fn(private))]
|
|
|
|
pub struct Config {
|
2021-08-08 03:33:37 +00:00
|
|
|
/// The hostname of the IMAP server. If using TLS, must be an address
|
2021-08-09 04:30:08 +00:00
|
|
|
pub hostname: String,
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
/// The port of the IMAP server.
|
2021-08-09 04:30:08 +00:00
|
|
|
pub port: u16,
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
/// Whether or not the client is using an encrypted stream.
|
|
|
|
///
|
|
|
|
/// To upgrade the connection later, use the upgrade method.
|
2021-08-09 05:36:10 +00:00
|
|
|
#[builder(default = "true")]
|
2021-08-09 04:30:08 +00:00
|
|
|
pub tls: bool,
|
2021-08-09 05:36:10 +00:00
|
|
|
|
|
|
|
/// Whether or not to verify hostname
|
|
|
|
#[builder(default = "true")]
|
|
|
|
pub verify_hostname: bool,
|
2021-08-08 03:33:37 +00:00
|
|
|
}
|
|
|
|
|
2021-08-09 04:30:08 +00:00
|
|
|
impl ConfigBuilder {
|
2021-08-09 05:36:10 +00:00
|
|
|
pub async fn open(&self) -> Result<ClientUnauthenticated> {
|
2021-08-09 04:30:08 +00:00
|
|
|
let config = self.build()?;
|
|
|
|
|
|
|
|
let hostname = config.hostname.as_ref();
|
|
|
|
let port = config.port;
|
2021-08-08 03:33:37 +00:00
|
|
|
let conn = TcpStream::connect((hostname, port)).await?;
|
|
|
|
|
2021-08-09 04:30:08 +00:00
|
|
|
if config.tls {
|
2021-08-09 05:36:10 +00:00
|
|
|
let conn = upgrade(conn, hostname).await?;
|
2021-08-09 04:30:08 +00:00
|
|
|
let mut inner = Inner::new(conn, config).await?;
|
2021-08-08 03:33:37 +00:00
|
|
|
inner.wait_for_greeting().await?;
|
|
|
|
return Ok(ClientUnauthenticated::Encrypted(inner));
|
|
|
|
} else {
|
2021-08-09 04:30:08 +00:00
|
|
|
let mut inner = Inner::new(conn, config).await?;
|
2021-08-08 03:33:37 +00:00
|
|
|
inner.wait_for_greeting().await?;
|
|
|
|
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum ClientUnauthenticated {
|
2021-08-09 04:30:08 +00:00
|
|
|
Encrypted(Inner<TlsStream<TcpStream>>),
|
|
|
|
Unencrypted(Inner<TcpStream>),
|
2021-08-08 03:33:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ClientUnauthenticated {
|
|
|
|
pub async fn upgrade(self) -> Result<ClientUnauthenticated> {
|
|
|
|
match self {
|
|
|
|
// this is a no-op, we don't need to upgrade
|
|
|
|
ClientUnauthenticated::Encrypted(_) => Ok(self),
|
|
|
|
ClientUnauthenticated::Unencrypted(e) => {
|
|
|
|
Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 05:36:10 +00:00
|
|
|
client_expose!(async execute(cmd: Command) -> Result<ResponseStream>);
|
2021-08-09 04:30:08 +00:00
|
|
|
client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>);
|
2021-08-08 03:33:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub enum ClientAuthenticated {
|
2021-08-09 04:30:08 +00:00
|
|
|
Encrypted(Inner<TlsStream<TcpStream>>),
|
|
|
|
Unencrypted(Inner<TcpStream>),
|
2021-08-08 03:33:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ClientAuthenticated {
|
2021-08-09 05:36:10 +00:00
|
|
|
client_expose!(async execute(cmd: Command) -> Result<ResponseStream>);
|
2021-08-09 04:30:08 +00:00
|
|
|
client_expose!(async has_capability(cap: impl AsRef<str>) -> Result<bool>);
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
fn sender(&self) -> mpsc::UnboundedSender<String> {
|
|
|
|
match self {
|
|
|
|
ClientAuthenticated::Encrypted(e) => e.write_tx.clone(),
|
|
|
|
ClientAuthenticated::Unencrypted(e) => e.write_tx.clone(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the LIST command
|
2021-08-09 04:30:08 +00:00
|
|
|
pub async fn list(&mut self) -> Result<Vec<Mailbox>> {
|
|
|
|
let cmd = Command::List(CommandList {
|
2021-08-09 05:36:10 +00:00
|
|
|
reference: Bytes::from(""),
|
|
|
|
mailbox: Bytes::from("*"),
|
2021-08-09 04:30:08 +00:00
|
|
|
});
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
let res = self.execute(cmd).await?;
|
|
|
|
let (_, data) = res.wait().await?;
|
|
|
|
|
|
|
|
let mut folders = Vec::new();
|
|
|
|
for resp in data {
|
2021-08-09 04:30:08 +00:00
|
|
|
if let Response::MailboxData(MailboxData::List(MailboxList { mailbox, .. })) = resp {
|
|
|
|
folders.push(mailbox);
|
2021-08-08 03:33:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(folders)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the SELECT command
|
|
|
|
pub async fn select(&mut self, mailbox: impl AsRef<str>) -> Result<SelectResponse> {
|
2021-08-09 04:30:08 +00:00
|
|
|
let cmd = Command::Select(CommandSelect {
|
2021-08-09 05:36:10 +00:00
|
|
|
mailbox: Bytes::from(mailbox.as_ref().to_owned()),
|
2021-08-09 04:30:08 +00:00
|
|
|
});
|
2021-08-08 03:33:37 +00:00
|
|
|
|
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
let (_, data) = stream.wait().await?;
|
|
|
|
|
|
|
|
let mut select = SelectResponse::default();
|
|
|
|
for resp in data {
|
|
|
|
match resp {
|
|
|
|
Response::MailboxData(MailboxData::Flags(flags)) => select.flags = flags,
|
|
|
|
Response::MailboxData(MailboxData::Exists(exists)) => select.exists = Some(exists),
|
|
|
|
Response::MailboxData(MailboxData::Recent(recent)) => select.recent = Some(recent),
|
2021-08-09 04:30:08 +00:00
|
|
|
Response::Tagged(
|
|
|
|
_,
|
|
|
|
Condition {
|
|
|
|
status: Status::Ok,
|
|
|
|
code: Some(code),
|
|
|
|
..
|
|
|
|
},
|
|
|
|
) => match code {
|
2021-08-08 03:33:37 +00:00
|
|
|
ResponseCode::Unseen(value) => select.unseen = Some(value),
|
|
|
|
ResponseCode::UidNext(value) => select.uid_next = Some(value),
|
|
|
|
ResponseCode::UidValidity(value) => select.uid_validity = Some(value),
|
|
|
|
_ => {}
|
|
|
|
},
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(select)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the SEARCH command
|
|
|
|
pub async fn uid_search(&mut self) -> Result<Vec<u32>> {
|
2021-08-09 04:30:08 +00:00
|
|
|
let cmd = Command::UidSearch(CommandSearch {
|
2021-08-08 03:33:37 +00:00
|
|
|
criteria: SearchCriteria::All,
|
2021-08-09 04:30:08 +00:00
|
|
|
});
|
2021-08-08 03:33:37 +00:00
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
let (_, data) = stream.wait().await?;
|
|
|
|
for resp in data {
|
|
|
|
if let Response::MailboxData(MailboxData::Search(uids)) = resp {
|
|
|
|
return Ok(uids);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bail!("could not find the SEARCH response")
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the FETCH command
|
|
|
|
pub async fn fetch(
|
|
|
|
&mut self,
|
|
|
|
uids: &[u32],
|
|
|
|
items: FetchItems,
|
2021-08-09 04:30:08 +00:00
|
|
|
) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> {
|
|
|
|
let cmd = Command::Fetch(CommandFetch {
|
|
|
|
ids: uids.to_vec(),
|
2021-08-08 03:33:37 +00:00
|
|
|
items,
|
2021-08-09 04:30:08 +00:00
|
|
|
});
|
|
|
|
debug!("fetch: {:?}", cmd);
|
2021-08-08 03:33:37 +00:00
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
// let (done, data) = stream.wait().await?;
|
|
|
|
Ok(stream.filter_map(|resp| match resp {
|
|
|
|
Response::Fetch(n, attrs) => future::ready(Some((n, attrs))).boxed(),
|
|
|
|
Response::Done(_) => future::ready(None).boxed(),
|
|
|
|
_ => future::pending().boxed(),
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the UID FETCH command
|
|
|
|
pub async fn uid_fetch(
|
|
|
|
&mut self,
|
|
|
|
uids: &[u32],
|
|
|
|
items: FetchItems,
|
2021-08-09 04:30:08 +00:00
|
|
|
) -> Result<impl Stream<Item = (u32, Vec<MessageAttribute>)>> {
|
|
|
|
let cmd = Command::UidFetch(CommandFetch {
|
|
|
|
ids: uids.to_vec(),
|
2021-08-08 03:33:37 +00:00
|
|
|
items,
|
2021-08-09 04:30:08 +00:00
|
|
|
});
|
|
|
|
debug!("uid fetch: {:?}", cmd);
|
2021-08-08 03:33:37 +00:00
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
// let (done, data) = stream.wait().await?;
|
|
|
|
Ok(stream.filter_map(|resp| match resp {
|
|
|
|
Response::Fetch(n, attrs) => future::ready(Some((n, attrs))).boxed(),
|
|
|
|
Response::Done(_) => future::ready(None).boxed(),
|
|
|
|
_ => future::pending().boxed(),
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the IDLE command
|
|
|
|
#[cfg(feature = "rfc2177-idle")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))]
|
|
|
|
pub async fn idle(&mut self) -> Result<IdleToken> {
|
|
|
|
let cmd = Command::Idle;
|
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
let sender = self.sender();
|
|
|
|
Ok(IdleToken { stream, sender })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
2021-08-09 04:30:08 +00:00
|
|
|
pub struct SelectResponse {
|
|
|
|
pub flags: Vec<Flag>,
|
2021-08-08 03:33:37 +00:00
|
|
|
pub exists: Option<u32>,
|
|
|
|
pub recent: Option<u32>,
|
|
|
|
pub uid_next: Option<u32>,
|
|
|
|
pub uid_validity: Option<u32>,
|
|
|
|
pub unseen: Option<u32>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A token that represents an idling connection.
|
|
|
|
///
|
2021-08-09 04:30:08 +00:00
|
|
|
/// Dropping this token indicates that the idling should be completed, and the
|
|
|
|
/// DONE command will be sent to the server as a result.
|
2021-08-08 03:33:37 +00:00
|
|
|
#[cfg(feature = "rfc2177-idle")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))]
|
|
|
|
pub struct IdleToken {
|
|
|
|
pub stream: ResponseStream,
|
|
|
|
sender: mpsc::UnboundedSender<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "rfc2177-idle")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))]
|
|
|
|
impl Drop for IdleToken {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
// TODO: should ignore this?
|
|
|
|
self.sender.send(format!("DONE\r\n")).unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "rfc2177-idle")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))]
|
|
|
|
impl Stream for IdleToken {
|
|
|
|
type Item = Response;
|
|
|
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
|
|
|
let stream = Pin::new(&mut self.stream);
|
|
|
|
Stream::poll_next(stream, cx)
|
|
|
|
}
|
|
|
|
}
|