2021-02-20 05:03:33 +00:00
|
|
|
//! IMAP Client
|
|
|
|
//! ===
|
|
|
|
//!
|
|
|
|
//! The IMAP client in this module is implemented as a state machine in the type system: methods
|
|
|
|
//! that are not supported in a particular state (ex. fetch in an unauthenticated state) cannot be
|
|
|
|
//! expressed in the type system entirely.
|
2021-02-20 07:30:58 +00:00
|
|
|
//!
|
|
|
|
//! Because there's many client types for the different types of clients, you'll want to start
|
|
|
|
//! here:
|
|
|
|
//!
|
2021-02-21 13:57:28 +00:00
|
|
|
//! - [`ClientBuilder`][self::ClientBuilder] : Constructs the config for the IMAP client
|
|
|
|
//!
|
|
|
|
//! If you choose not to use the high-level type-safe features of `ClientBuilder`, then you can
|
|
|
|
//! also choose to access the lower level [`Client`][self::inner::Client] directly.
|
2021-02-21 14:02:26 +00:00
|
|
|
//!
|
|
|
|
//! Example
|
|
|
|
//! ---
|
|
|
|
//!
|
|
|
|
//! The following example connects to `mywebsite.com:143` using STARTTLS.
|
|
|
|
//!
|
|
|
|
//! ```no_run
|
|
|
|
//! # use anyhow::Result;
|
|
|
|
//! # use panorama_imap::client::ClientConfigBuilder;
|
|
|
|
//! # async fn test() -> Result<()> {
|
|
|
|
//! let config = ClientConfigBuilder::default()
|
|
|
|
//! .hostname("mywebsite.com".to_owned())
|
|
|
|
//! .port(143)
|
|
|
|
//! .tls(false)
|
|
|
|
//! .build().unwrap();
|
|
|
|
//! let insecure = config.open().await?;
|
|
|
|
//! let unauth = insecure.upgrade().await?;
|
|
|
|
//! # Ok(())
|
|
|
|
//! # }
|
|
|
|
//! ```
|
2021-02-20 05:03:33 +00:00
|
|
|
|
2021-02-23 04:01:39 +00:00
|
|
|
pub mod auth;
|
2021-02-20 05:03:33 +00:00
|
|
|
mod inner;
|
|
|
|
|
2021-03-09 14:05:31 +00:00
|
|
|
use std::pin::Pin;
|
2021-02-20 05:03:33 +00:00
|
|
|
use std::sync::Arc;
|
2021-03-09 14:05:31 +00:00
|
|
|
use std::task::{Context, Poll};
|
2021-02-20 05:03:33 +00:00
|
|
|
|
|
|
|
use anyhow::Result;
|
2021-03-09 11:21:23 +00:00
|
|
|
use futures::{
|
|
|
|
future::{self, FutureExt},
|
|
|
|
stream::{Stream, StreamExt},
|
|
|
|
};
|
2021-03-09 14:05:31 +00:00
|
|
|
use tokio::{
|
|
|
|
net::TcpStream,
|
|
|
|
sync::{mpsc, oneshot},
|
|
|
|
task::JoinHandle,
|
|
|
|
};
|
2021-02-21 13:54:46 +00:00
|
|
|
use tokio_rustls::{
|
|
|
|
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
|
2021-02-20 05:03:33 +00:00
|
|
|
};
|
|
|
|
|
2021-03-07 02:04:58 +00:00
|
|
|
use crate::command::{Command, FetchItems, SearchCriteria};
|
2021-03-09 00:26:18 +00:00
|
|
|
use crate::response::{
|
|
|
|
AttributeValue, Envelope, MailboxData, Response, ResponseData, ResponseDone,
|
|
|
|
};
|
2021-02-23 02:47:00 +00:00
|
|
|
|
2021-03-07 00:56:35 +00:00
|
|
|
pub use self::inner::{Client, ResponseStream};
|
2021-02-20 05:03:33 +00:00
|
|
|
|
2021-02-20 07:30:58 +00:00
|
|
|
/// Struct used to start building the config for a client.
|
|
|
|
///
|
2021-03-02 22:47:33 +00:00
|
|
|
/// Call [`.build`][1] to _build_ the config, then run [`.open`][2] to actually start opening
|
2021-02-20 07:30:58 +00:00
|
|
|
/// the connection to the server.
|
|
|
|
///
|
2021-02-21 13:54:46 +00:00
|
|
|
/// [1]: self::ClientConfigBuilder::build
|
2021-03-03 01:45:45 +00:00
|
|
|
/// [2]: self::ClientConfig::connect
|
2021-02-21 13:54:46 +00:00
|
|
|
pub type ClientBuilder = ClientConfigBuilder;
|
2021-02-20 05:24:46 +00:00
|
|
|
|
2021-02-20 05:03:33 +00:00
|
|
|
/// An IMAP client that hasn't been connected yet.
|
|
|
|
#[derive(Builder, Clone, Debug)]
|
2021-02-21 13:54:46 +00:00
|
|
|
pub struct ClientConfig {
|
2021-02-20 05:03:33 +00:00
|
|
|
/// The hostname of the IMAP server. If using TLS, must be an address
|
|
|
|
hostname: String,
|
|
|
|
|
|
|
|
/// The port of the IMAP server.
|
|
|
|
port: u16,
|
|
|
|
|
|
|
|
/// Whether or not the client is using an encrypted stream.
|
|
|
|
///
|
|
|
|
/// To upgrade the connection later, use the upgrade method.
|
|
|
|
tls: bool,
|
|
|
|
}
|
|
|
|
|
2021-02-21 13:54:46 +00:00
|
|
|
impl ClientConfig {
|
2021-02-21 13:42:40 +00:00
|
|
|
pub async fn open(self) -> Result<ClientUnauthenticated> {
|
2021-02-20 05:03:33 +00:00
|
|
|
let hostname = self.hostname.as_ref();
|
|
|
|
let port = self.port;
|
|
|
|
let conn = TcpStream::connect((hostname, port)).await?;
|
|
|
|
|
|
|
|
if self.tls {
|
2021-02-21 13:54:46 +00:00
|
|
|
let mut tls_config = RustlsConfig::new();
|
2021-02-20 05:03:33 +00:00
|
|
|
tls_config
|
|
|
|
.root_store
|
|
|
|
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
|
|
|
let tls_config = TlsConnector::from(Arc::new(tls_config));
|
|
|
|
let dnsname = DNSNameRef::try_from_ascii_str(hostname).unwrap();
|
|
|
|
let conn = tls_config.connect(dnsname, conn).await?;
|
|
|
|
|
2021-03-07 00:56:35 +00:00
|
|
|
let mut inner = Client::new(conn, self);
|
|
|
|
inner.wait_for_greeting().await?;
|
2021-02-23 04:01:39 +00:00
|
|
|
return Ok(ClientUnauthenticated::Encrypted(inner));
|
2021-02-21 13:42:40 +00:00
|
|
|
} else {
|
2021-03-07 00:56:35 +00:00
|
|
|
let mut inner = Client::new(conn, self);
|
|
|
|
inner.wait_for_greeting().await?;
|
2021-02-23 04:01:39 +00:00
|
|
|
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
2021-02-20 05:03:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum ClientUnauthenticated {
|
2021-02-23 04:01:39 +00:00
|
|
|
Encrypted(Client<TlsStream<TcpStream>>),
|
|
|
|
Unencrypted(Client<TcpStream>),
|
2021-02-20 05:03:33 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 07:30:58 +00:00
|
|
|
impl ClientUnauthenticated {
|
2021-02-21 13:42:40 +00:00
|
|
|
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) => {
|
2021-02-23 04:01:39 +00:00
|
|
|
Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?))
|
2021-02-21 13:42:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-23 04:01:39 +00:00
|
|
|
/// Exposing low-level execute
|
2021-03-07 00:56:35 +00:00
|
|
|
async fn execute(&mut self, cmd: Command) -> Result<ResponseStream> {
|
2021-02-23 02:47:00 +00:00
|
|
|
match self {
|
2021-02-23 04:01:39 +00:00
|
|
|
ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await,
|
|
|
|
ClientUnauthenticated::Unencrypted(e) => e.execute(cmd).await,
|
2021-02-23 02:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks if the server that the client is talking to has support for the given capability.
|
|
|
|
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
2021-02-20 07:30:58 +00:00
|
|
|
match self {
|
2021-02-23 04:01:39 +00:00
|
|
|
ClientUnauthenticated::Encrypted(e) => e.has_capability(cap).await,
|
|
|
|
ClientUnauthenticated::Unencrypted(e) => e.has_capability(cap).await,
|
2021-02-20 07:30:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-20 05:03:33 +00:00
|
|
|
|
2021-02-23 04:01:39 +00:00
|
|
|
pub enum ClientAuthenticated {
|
|
|
|
Encrypted(Client<TlsStream<TcpStream>>),
|
|
|
|
Unencrypted(Client<TcpStream>),
|
2021-02-20 05:03:33 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 04:01:39 +00:00
|
|
|
impl ClientAuthenticated {
|
|
|
|
/// Exposing low-level execute
|
2021-03-07 00:56:35 +00:00
|
|
|
async fn execute(&mut self, cmd: Command) -> Result<ResponseStream> {
|
2021-02-23 04:01:39 +00:00
|
|
|
match self {
|
|
|
|
ClientAuthenticated::Encrypted(e) => e.execute(cmd).await,
|
|
|
|
ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await,
|
|
|
|
}
|
|
|
|
}
|
2021-02-20 05:03:33 +00:00
|
|
|
|
2021-03-09 14:05:31 +00:00
|
|
|
fn sender(&self) -> mpsc::UnboundedSender<String> {
|
|
|
|
match self {
|
|
|
|
ClientAuthenticated::Encrypted(e) => e.write_tx.clone(),
|
|
|
|
ClientAuthenticated::Unencrypted(e) => e.write_tx.clone(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 11:21:23 +00:00
|
|
|
/// Checks if the server that the client is talking to has support for the given capability.
|
|
|
|
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
|
|
|
match self {
|
|
|
|
ClientAuthenticated::Encrypted(e) => e.has_capability(cap).await,
|
|
|
|
ClientAuthenticated::Unencrypted(e) => e.has_capability(cap).await,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-24 10:46:21 +00:00
|
|
|
/// Runs the LIST command
|
2021-03-02 22:47:33 +00:00
|
|
|
pub async fn list(&mut self) -> Result<Vec<String>> {
|
2021-02-23 04:30:20 +00:00
|
|
|
let cmd = Command::List {
|
|
|
|
reference: "".to_owned(),
|
|
|
|
mailbox: "*".to_owned(),
|
|
|
|
};
|
2021-03-03 01:45:45 +00:00
|
|
|
|
2021-03-07 00:56:35 +00:00
|
|
|
let res = self.execute(cmd).await?;
|
|
|
|
let (_, data) = res.wait().await?;
|
2021-03-02 22:47:33 +00:00
|
|
|
|
2021-03-07 00:56:35 +00:00
|
|
|
let mut folders = Vec::new();
|
|
|
|
for resp in data {
|
|
|
|
if let Response::MailboxData(MailboxData::List { name, .. }) = resp {
|
|
|
|
folders.push(name.to_owned());
|
|
|
|
}
|
|
|
|
}
|
2021-03-02 22:47:33 +00:00
|
|
|
|
2021-03-07 00:56:35 +00:00
|
|
|
Ok(folders)
|
2021-02-23 04:01:39 +00:00
|
|
|
}
|
2021-02-24 10:46:21 +00:00
|
|
|
|
|
|
|
/// Runs the SELECT command
|
|
|
|
pub async fn select(&mut self, mailbox: impl AsRef<str>) -> Result<()> {
|
|
|
|
let cmd = Command::Select {
|
|
|
|
mailbox: mailbox.as_ref().to_owned(),
|
|
|
|
};
|
2021-03-07 02:04:58 +00:00
|
|
|
let stream = self.execute(cmd).await?;
|
|
|
|
let (done, data) = stream.wait().await?;
|
|
|
|
for resp in data {
|
|
|
|
debug!("execute called returned: {:?}", resp);
|
|
|
|
}
|
2021-03-03 01:45:45 +00:00
|
|
|
|
|
|
|
// nuke the capabilities cache
|
|
|
|
self.nuke_capabilities();
|
|
|
|
|
2021-02-24 10:46:21 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2021-02-24 12:43:50 +00:00
|
|
|
|
2021-03-07 02:04:58 +00:00
|
|
|
/// Runs the SEARCH command
|
|
|
|
pub async fn uid_search(&mut self) -> Result<Vec<u32>> {
|
|
|
|
let cmd = Command::UidSearch {
|
|
|
|
criteria: SearchCriteria::All,
|
|
|
|
};
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2021-03-09 14:05:31 +00:00
|
|
|
/// Runs the FETCH command
|
|
|
|
pub async fn fetch(
|
|
|
|
&mut self,
|
|
|
|
uids: &[u32],
|
|
|
|
) -> Result<impl Stream<Item = (u32, Vec<AttributeValue>)>> {
|
|
|
|
let cmd = Command::Fetch {
|
|
|
|
uids: uids.to_vec(),
|
|
|
|
items: FetchItems::All,
|
|
|
|
};
|
|
|
|
debug!("fetch: {}", cmd);
|
|
|
|
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(),
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2021-03-07 02:04:58 +00:00
|
|
|
/// Runs the UID FETCH command
|
2021-03-09 11:21:23 +00:00
|
|
|
pub async fn uid_fetch(
|
|
|
|
&mut self,
|
|
|
|
uids: &[u32],
|
|
|
|
) -> Result<impl Stream<Item = (u32, Vec<AttributeValue>)>> {
|
2021-03-07 02:04:58 +00:00
|
|
|
let cmd = Command::UidFetch {
|
|
|
|
uids: uids.to_vec(),
|
|
|
|
items: FetchItems::All,
|
|
|
|
};
|
|
|
|
debug!("uid fetch: {}", cmd);
|
|
|
|
let stream = self.execute(cmd).await?;
|
2021-03-09 11:21:23 +00:00
|
|
|
// 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(),
|
|
|
|
}))
|
2021-03-07 02:04:58 +00:00
|
|
|
}
|
|
|
|
|
2021-03-03 01:45:45 +00:00
|
|
|
/// Runs the IDLE command
|
2021-02-24 12:43:50 +00:00
|
|
|
#[cfg(feature = "rfc2177-idle")]
|
2021-03-09 09:27:25 +00:00
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))]
|
2021-03-09 14:05:31 +00:00
|
|
|
pub async fn idle(&mut self) -> Result<IdleToken> {
|
2021-02-26 06:03:23 +00:00
|
|
|
let cmd = Command::Idle;
|
2021-03-07 00:56:35 +00:00
|
|
|
let stream = self.execute(cmd).await?;
|
2021-03-09 14:05:31 +00:00
|
|
|
let sender = self.sender();
|
|
|
|
Ok(IdleToken { stream, sender })
|
2021-02-24 12:43:50 +00:00
|
|
|
}
|
2021-03-03 01:45:45 +00:00
|
|
|
|
|
|
|
fn nuke_capabilities(&mut self) {
|
|
|
|
// TODO: do something here
|
|
|
|
}
|
2021-02-20 05:03:33 +00:00
|
|
|
}
|
2021-03-09 14:05:31 +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) {
|
|
|
|
self.sender.send(format!("DONE\r\n"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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)
|
|
|
|
}
|
|
|
|
}
|