A significant documentation upgrade

Fixes #77.
Touches on #74.
Fixes #70 through documentation (I think?)
Fixes #62.
This commit is contained in:
Jon Gjengset 2018-11-21 16:33:24 -05:00
parent bddfab357f
commit f83742dc3d
No known key found for this signature in database
GPG key ID: D64AC9D67176DC71
17 changed files with 1112 additions and 492 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "imap" name = "imap"
version = "0.8.1" version = "0.9.0"
authors = ["Matt McCoy <mattnenterprise@yahoo.com>", authors = ["Matt McCoy <mattnenterprise@yahoo.com>",
"Jon Gjengset <jon@thesquareplanet.com>"] "Jon Gjengset <jon@thesquareplanet.com>"]
documentation = "https://docs.rs/imap/" documentation = "https://docs.rs/imap/"

View file

@ -10,54 +10,57 @@
[![Build Status](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap?svg=true)](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap) [![Build Status](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap?svg=true)](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap)
[![Coverage Status](https://coveralls.io/repos/github/mattnenterprise/rust-imap/badge.svg?branch=master)](https://coveralls.io/github/mattnenterprise/rust-imap?branch=master) [![Coverage Status](https://coveralls.io/repos/github/mattnenterprise/rust-imap/badge.svg?branch=master)](https://coveralls.io/github/mattnenterprise/rust-imap?branch=master)
IMAP client bindings for Rust. This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC
3501](https://tools.ietf.org/html/rfc3501)). After authenticating with the server, IMAP lets
you list, fetch, and search for e-mails, as well as monitor mailboxes for changes.
## Usage To connect, use the `imap::connect` function. This gives you an unauthenticated `imap::Client`. You can
then use `Client::login` or `Client::authenticate` to perform username/password or
challenge/response authentication respectively. This in turn gives you an authenticated
`Session`, which lets you access the mailboxes at the server.
Here is a basic example of using the client. Below is a basic client example. See the `examples/` directory for more.
See the `examples/` directory for more examples.
```rust ```rust
extern crate imap;
extern crate native_tls; extern crate native_tls;
// To connect to the gmail IMAP server with this you will need to allow unsecure apps access. fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
// See: https://support.google.com/accounts/answer/6010255?hl=en let domain = "imap.example.com";
// Look at the `examples/gmail_oauth2.rs` for how to connect to gmail securely. let tls = native_tls::TlsConnector::builder().build().unwrap();
fn main() {
let domain = "imap.gmail.com";
let port = 993;
let socket_addr = (domain, port);
let ssl_connector = native_tls::TlsConnector::builder().build().unwrap();
let mut imap_socket = Client::secure_connect(socket_addr, domain, &ssl_connector).unwrap();
imap_socket.login("username", "password").unwrap(); // we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
match imap_socket.capabilities() { // the client we have here is unauthenticated.
Ok(capabilities) => { // to do anything useful with the e-mails, we need to log in
for capability in capabilities.iter() { let mut imap_session = client
println!("{}", capability); .login("me@example.com", "password")
} .map_err(|e| e.0)?;
}
Err(e) => println!("Error parsing capabilities: {}", e), // we want to fetch the first email in the INBOX mailbox
imap_session.select("INBOX")?;
// fetch message number 1 in this mailbox, along with its RFC822 field.
// RFC 822 dictates the format of the body of e-mails
let messages = imap_session.fetch("1", "RFC822")?;
let message = if let Some(m) = messages.iter().next() {
m
} else {
return Ok(None);
}; };
match imap_socket.select("INBOX") { // extract the message's body
Ok(mailbox) => { let body = message.rfc822().expect("message did not have a body!");
println!("{}", mailbox); let body = std::str::from_utf8(body)
} .expect("message was not valid utf-8")
Err(e) => println!("Error selecting INBOX: {}", e), .to_string();
};
match imap_socket.fetch("2", "body[text]") { // be nice to the server and log out
Ok(messages) => { imap_session.logout()?;
for message in messages.iter() {
print!("{:?}", message);
}
}
Err(e) => println!("Error Fetching email 2: {}", e),
};
imap_socket.logout().unwrap(); Ok(Some(body))
} }
``` ```
@ -71,5 +74,5 @@ at your option.
## Contribution ## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any for inclusion in the work by you, as defined in the Apache-2.0 license, shall
additional terms or conditions. be dual licensed as above, without any additional terms or conditions.

View file

@ -22,5 +22,5 @@ at your option.
## Contribution ## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any for inclusion in the work by you, as defined in the Apache-2.0 license, shall
additional terms or conditions. be dual licensed as above, without any additional terms or conditions.

View file

@ -1,46 +1,47 @@
extern crate imap; extern crate imap;
extern crate native_tls; extern crate native_tls;
use native_tls::TlsConnector; fn main() {
// To connect to the gmail IMAP server with this you will need to allow unsecure apps access. // To connect to the gmail IMAP server with this you will need to allow unsecure apps access.
// See: https://support.google.com/accounts/answer/6010255?hl=en // See: https://support.google.com/accounts/answer/6010255?hl=en
// Look at the gmail_oauth2.rs example on how to connect to a gmail server securely. // Look at the gmail_oauth2.rs example on how to connect to a gmail server securely.
fn main() { fetch_inbox_top().unwrap();
let domain = "imap.gmail.com";
let port = 993;
let socket_addr = (domain, port);
let ssl_connector = TlsConnector::builder().build().unwrap();
let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap();
let mut imap_session = match client.login("username", "password") {
Ok(c) => c,
Err((e, _unauth_client)) => {
eprintln!("failed to login: {}", e);
return;
} }
fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
let domain = "imap.example.com";
let tls = native_tls::TlsConnector::builder().build().unwrap();
// we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
// the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in
let mut imap_session = client
.login("me@example.com", "password")
.map_err(|e| e.0)?;
// we want to fetch the first email in the INBOX mailbox
imap_session.select("INBOX")?;
// fetch message number 1 in this mailbox, along with its RFC822 field.
// RFC 822 dictates the format of the body of e-mails
let messages = imap_session.fetch("1", "RFC822")?;
let message = if let Some(m) = messages.iter().next() {
m
} else {
return Ok(None);
}; };
match imap_session.capabilities() { // extract the message's body
Ok(capabilities) => for capability in capabilities.iter() { let body = message.rfc822().expect("message did not have a body!");
println!("{}", capability); let body = std::str::from_utf8(body)
}, .expect("message was not valid utf-8")
Err(e) => println!("Error parsing capability: {}", e), .to_string();
};
match imap_session.select("INBOX") { // be nice to the server and log out
Ok(mailbox) => { imap_session.logout()?;
println!("{}", mailbox);
} Ok(Some(body))
Err(e) => println!("Error selecting INBOX: {}", e),
};
match imap_session.fetch("2", "body[text]") {
Ok(msgs) => for msg in &msgs {
print!("{:?}", msg);
},
Err(e) => println!("Error Fetching email 2: {}", e),
};
imap_session.logout().unwrap();
} }

View file

@ -2,7 +2,6 @@ extern crate base64;
extern crate imap; extern crate imap;
extern crate native_tls; extern crate native_tls;
use imap::authenticator::Authenticator;
use native_tls::TlsConnector; use native_tls::TlsConnector;
struct GmailOAuth2 { struct GmailOAuth2 {
@ -10,7 +9,7 @@ struct GmailOAuth2 {
access_token: String, access_token: String,
} }
impl Authenticator for GmailOAuth2 { impl imap::Authenticator for GmailOAuth2 {
type Response = String; type Response = String;
#[allow(unused_variables)] #[allow(unused_variables)]
fn process(&self, data: &[u8]) -> Self::Response { fn process(&self, data: &[u8]) -> Self::Response {
@ -30,7 +29,7 @@ fn main() {
let port = 993; let port = 993;
let socket_addr = (domain, port); let socket_addr = (domain, port);
let ssl_connector = TlsConnector::builder().build().unwrap(); let ssl_connector = TlsConnector::builder().build().unwrap();
let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); let client = imap::connect(socket_addr, domain, &ssl_connector).unwrap();
let mut imap_session = match client.authenticate("XOAUTH2", gmail_auth) { let mut imap_session = match client.authenticate("XOAUTH2", gmail_auth) {
Ok(c) => c, Ok(c) => c,
@ -46,9 +45,11 @@ fn main() {
}; };
match imap_session.fetch("2", "body[text]") { match imap_session.fetch("2", "body[text]") {
Ok(msgs) => for msg in &msgs { Ok(msgs) => {
for msg in &msgs {
print!("{:?}", msg); print!("{:?}", msg);
}, }
}
Err(e) => println!("Error Fetching email 2: {}", e), Err(e) => println!("Error Fetching email 2: {}", e),
}; };

View file

@ -1,16 +1,10 @@
/// This will allow plugable authentication mechanisms. /// This trait allows for pluggable authentication schemes. It is used by `Client::authenticate` to
/// /// [authenticate using SASL](https://tools.ietf.org/html/rfc3501#section-6.2.2).
/// This trait is used by `Client::authenticate` to [authenticate
/// using SASL](https://tools.ietf.org/html/rfc3501#section-6.2.2).
pub trait Authenticator { pub trait Authenticator {
/// Type of the response to the challenge. This will usually be a /// The type of the response to the challenge. This will usually be a `Vec<u8>` or `String`.
/// `Vec<u8>` or `String`. It must not be Base64 encoded: the
/// library will do it.
type Response: AsRef<[u8]>; type Response: AsRef<[u8]>;
/// For each server challenge is passed to `process`. The library
/// has already decoded the Base64 string into bytes. /// Each base64-decoded server challenge is passed to `process`.
/// /// The returned byte-string is base64-encoded and then sent back to the server.
/// The `process` function should return its response, not Base64 fn process(&self, challenge: &[u8]) -> Self::Response;
/// encoded: the library will do it.
fn process(&self, &[u8]) -> Self::Response;
} }

View file

@ -1,16 +1,16 @@
extern crate base64; use base64;
use bufstream::BufStream; use bufstream::BufStream;
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use nom; use nom;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::{self, Read, Write}; use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs}; use std::net::{TcpStream, ToSocketAddrs};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration;
use super::authenticator::Authenticator; use super::authenticator::Authenticator;
use super::error::{Error, ParseError, Result, ValidateError}; use super::error::{Error, ParseError, Result, ValidateError};
use super::extensions;
use super::parse::{ use super::parse::{
parse_authenticate_response, parse_capabilities, parse_fetches, parse_ids, parse_mailbox, parse_authenticate_response, parse_capabilities, parse_fetches, parse_ids, parse_mailbox,
parse_names, parse_names,
@ -41,9 +41,8 @@ fn validate_str(value: &str) -> Result<String> {
/// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from /// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from
/// a succesful login attempt. /// a succesful login attempt.
/// // Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying // primitives type.
/// primitives type.
#[derive(Debug)] #[derive(Debug)]
pub struct Session<T: Read + Write> { pub struct Session<T: Read + Write> {
conn: Connection<T>, conn: Connection<T>,
@ -56,9 +55,8 @@ pub struct Session<T: Read + Write> {
/// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first /// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first
/// connecting. A succesfull call to [`login`](struct.Client.html#method.login) will return a /// connecting. A succesfull call to [`login`](struct.Client.html#method.login) will return a
/// [`Session`](struct.Session.html) instance, providing the usual IMAP methods. /// [`Session`](struct.Session.html) instance, providing the usual IMAP methods.
/// // Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying // primitives type.
/// primitives type.
#[derive(Debug)] #[derive(Debug)]
pub struct Client<T: Read + Write> { pub struct Client<T: Read + Write> {
conn: Connection<T>, conn: Connection<T>,
@ -67,9 +65,13 @@ pub struct Client<T: Read + Write> {
/// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful /// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful
/// login) use a `Connection` internally for the TCP stream primitives. /// login) use a `Connection` internally for the TCP stream primitives.
#[derive(Debug)] #[derive(Debug)]
#[doc(hidden)]
pub struct Connection<T: Read + Write> { pub struct Connection<T: Read + Write> {
stream: BufStream<T>, pub(crate) stream: BufStream<T>,
tag: u32, tag: u32,
/// Enable debug mode for this connection so that all client-server interactions are printed to
/// `STDERR`.
pub debug: bool, pub debug: bool,
} }
@ -103,161 +105,13 @@ impl<T: Read + Write> DerefMut for Session<T> {
} }
} }
/// `IdleHandle` allows a client to block waiting for changes to the remote mailbox. /// Connect to a server using an insecure TCP connection.
/// ///
/// The handle blocks using the IMAP IDLE command specificed in [RFC /// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// 2177](https://tools.ietf.org/html/rfc2177). /// [`Session`]), use [`Client::login`] or [`Client::authenticate`].
/// ///
/// As long a the handle is active, the mailbox cannot be otherwise accessed. /// Consider using [`connect`] for a secured connection where possible.
#[derive(Debug)] /// You can upgrade an insecure client to a secure one using [`Client::secure`].
pub struct IdleHandle<'a, T: Read + Write + 'a> {
session: &'a mut Session<T>,
keepalive: Duration,
done: bool,
}
/// Must be implemented for a transport in order for a `Session` using that transport to support
/// operations with timeouts.
///
/// Examples of where this is useful is for `IdleHandle::wait_keepalive` and
/// `IdleHandle::wait_timeout`.
pub trait SetReadTimeout {
/// Set the timeout for subsequent reads to the given one.
///
/// If `timeout` is `None`, the read timeout should be removed.
///
/// See also `std::net::TcpStream::set_read_timeout`.
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()>;
}
impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> {
fn new(session: &'a mut Session<T>) -> Result<Self> {
let mut h = IdleHandle {
session,
keepalive: Duration::from_secs(29 * 60),
done: false,
};
h.init()?;
Ok(h)
}
fn init(&mut self) -> Result<()> {
// https://tools.ietf.org/html/rfc2177
//
// The IDLE command takes no arguments.
self.session.run_command("IDLE")?;
// A tagged response will be sent either
//
// a) if there's an error, or
// b) *after* we send DONE
let mut v = Vec::new();
self.session.readline(&mut v)?;
if v.starts_with(b"+") {
self.done = false;
return Ok(());
}
self.session.read_response_onto(&mut v)?;
// We should *only* get a continuation on an error (i.e., it gives BAD or NO).
unreachable!();
}
fn terminate(&mut self) -> Result<()> {
if !self.done {
self.done = true;
self.session.write_line(b"DONE")?;
self.session.read_response().map(|_| ())
} else {
Ok(())
}
}
/// Internal helper that doesn't consume self.
///
/// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`.
fn wait_inner(&mut self) -> Result<()> {
let mut v = Vec::new();
match self.session.readline(&mut v).map(|_| ()) {
Err(Error::Io(ref e))
if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock =>
{
// we need to refresh the IDLE connection
self.terminate()?;
self.init()?;
self.wait_inner()
}
r => r,
}
}
/// Block until the selected mailbox changes.
pub fn wait(mut self) -> Result<()> {
self.wait_inner()
}
}
impl<'a, T: SetReadTimeout + Read + Write + 'a> IdleHandle<'a, T> {
/// Set the keep-alive interval to use when `wait_keepalive` is called.
///
/// The interval defaults to 29 minutes as dictated by RFC 2177.
pub fn set_keepalive(&mut self, interval: Duration) {
self.keepalive = interval;
}
/// Block until the selected mailbox changes.
///
/// This method differs from `IdleHandle::wait` in that it will periodically refresh the IDLE
/// connection, to prevent the server from timing out our connection. The keepalive interval is
/// set to 29 minutes by default, as dictated by RFC 2177, but can be changed using
/// `set_keepalive`.
///
/// This is the recommended method to use for waiting.
pub fn wait_keepalive(self) -> Result<()> {
// The server MAY consider a client inactive if it has an IDLE command
// running, and if such a server has an inactivity timeout it MAY log
// the client off implicitly at the end of its timeout period. Because
// of that, clients using IDLE are advised to terminate the IDLE and
// re-issue it at least every 29 minutes to avoid being logged off.
// This still allows a client to receive immediate mailbox updates even
// though it need only "poll" at half hour intervals.
let keepalive = self.keepalive;
self.wait_timeout(keepalive)
}
/// Block until the selected mailbox changes, or until the given amount of time has expired.
pub fn wait_timeout(mut self, timeout: Duration) -> Result<()> {
self.session
.stream
.get_mut()
.set_read_timeout(Some(timeout))?;
let res = self.wait_inner();
self.session.stream.get_mut().set_read_timeout(None).is_ok();
res
}
}
impl<'a, T: Read + Write + 'a> Drop for IdleHandle<'a, T> {
fn drop(&mut self) {
// we don't want to panic here if we can't terminate the Idle
self.terminate().is_ok();
}
}
impl<'a> SetReadTimeout for TcpStream {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
TcpStream::set_read_timeout(self, timeout).map_err(Error::Io)
}
}
impl<'a> SetReadTimeout for TlsStream<TcpStream> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
self.get_ref().set_read_timeout(timeout).map_err(Error::Io)
}
}
/// Creates a new client. The usual IMAP commands are part of the [`Session`](struct.Session.html)
/// type, returned from a succesful call to [`Client::login`](struct.Client.html#method.login).
/// ```rust,no_run /// ```rust,no_run
/// # extern crate native_tls; /// # extern crate native_tls;
/// # extern crate imap; /// # extern crate imap;
@ -265,14 +119,14 @@ impl<'a> SetReadTimeout for TlsStream<TcpStream> {
/// # use native_tls::TlsConnector; /// # use native_tls::TlsConnector;
/// # fn main() { /// # fn main() {
/// // a plain, unencrypted TCP connection /// // a plain, unencrypted TCP connection
/// let client = imap::client::connect(("imap.example.org", 143)).unwrap(); /// let client = imap::connect_insecure(("imap.example.org", 143)).unwrap();
/// ///
/// // upgrade to SSL /// // upgrade to SSL
/// let ssl_connector = TlsConnector::builder().build().unwrap(); /// let tls = TlsConnector::builder().build().unwrap();
/// let ssl_client = client.secure("imap.example.org", &ssl_connector); /// let tls_client = client.secure("imap.example.org", &tls);
/// # } /// # }
/// ``` /// ```
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> { pub fn connect_insecure<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> {
match TcpStream::connect(addr) { match TcpStream::connect(addr) {
Ok(stream) => { Ok(stream) => {
let mut socket = Client::new(stream); let mut socket = Client::new(stream);
@ -284,23 +138,27 @@ pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> {
} }
} }
/// Creates a `Client` with an SSL wrapper. The usual IMAP commands are part of the /// Connect to a server using a TLS-encrypted connection.
/// [`Session`](struct.Session.html) type, returned from a succesful call to ///
/// [`Client::login`](struct.Client.html#method.login). /// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// ```rust,no_run /// [`Session`]), use [`Client::login`] or [`Client::authenticate`].
///
/// The domain must be passed in separately from the `TlsConnector` so that the certificate of the
/// IMAP server can be validated.
///
/// # Examples
///
/// ```no_run
/// # extern crate native_tls; /// # extern crate native_tls;
/// # extern crate imap; /// # extern crate imap;
/// # use std::io; /// # use std::io;
/// # use native_tls::TlsConnector; /// # use native_tls::TlsConnector;
/// # fn main() { /// # fn main() {
/// let ssl_connector = TlsConnector::builder().build().unwrap(); /// let tls = TlsConnector::builder().build().unwrap();
/// let ssl_client = imap::client::secure_connect( /// let client = imap::connect(("imap.example.org", 993), "imap.example.org", &tls).unwrap();
/// ("imap.example.org", 993),
/// "imap.example.org",
/// &ssl_connector).unwrap();
/// # } /// # }
/// ``` /// ```
pub fn secure_connect<A: ToSocketAddrs>( pub fn connect<A: ToSocketAddrs>(
addr: A, addr: A,
domain: &str, domain: &str,
ssl_connector: &TlsConnector, ssl_connector: &TlsConnector,
@ -321,9 +179,9 @@ pub fn secure_connect<A: ToSocketAddrs>(
} }
impl Client<TcpStream> { impl Client<TcpStream> {
/// This will upgrade a regular TCP connection to use SSL. /// This will upgrade an IMAP client from using a regular TCP connection to use TLS.
/// ///
/// Use the domain parameter for openssl's SNI and hostname verification. /// The domain parameter is required to perform hostname verification.
pub fn secure( pub fn secure(
mut self, mut self,
domain: &str, domain: &str,
@ -354,7 +212,10 @@ macro_rules! ok_or_unauth_client_err {
} }
impl<T: Read + Write> Client<T> { impl<T: Read + Write> Client<T> {
/// Creates a new client with the underlying stream. /// Creates a new client over the given stream.
///
/// This method primarily exists for writing tests that mock the underlying transport, but can
/// also be used to support IMAP over custom tunnels.
pub fn new(stream: T) -> Client<T> { pub fn new(stream: T) -> Client<T> {
Client { Client {
conn: Connection { conn: Connection {
@ -365,7 +226,94 @@ impl<T: Read + Write> Client<T> {
} }
} }
/// Authenticate will authenticate with the server, using the authenticator given. /// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is
/// returned; on error the original `Client` instance is returned in addition to the error.
/// This is because `login` takes ownership of `self`, so in order to try again (e.g. after
/// prompting the user for credetials), ownership of the original `Client` needs to be
/// transferred back to the caller.
///
/// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # fn main() {
/// # let tls_connector = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(
/// ("imap.example.org", 993),
/// "imap.example.org",
/// &tls_connector).unwrap();
///
/// match client.login("user", "pass") {
/// Ok(s) => {
/// // you are successfully authenticated!
/// },
/// Err((e, orig_client)) => {
/// eprintln!("error logging in: {}", e);
/// // prompt user and try again with orig_client here
/// return;
/// }
/// }
/// # }
/// ```
pub fn login(
mut self,
username: &str,
password: &str,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
let u = ok_or_unauth_client_err!(validate_str(username), self);
let p = ok_or_unauth_client_err!(validate_str(password), self);
ok_or_unauth_client_err!(
self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)),
self
);
Ok(Session::new(self.conn))
}
/// Authenticate with the server using the given custom `authenticator` to handle the server's
/// challenge.
///
/// ```no_run
/// extern crate imap;
/// extern crate native_tls;
/// use native_tls::TlsConnector;
///
/// struct OAuth2 {
/// user: String,
/// access_token: String,
/// }
///
/// impl imap::Authenticator for OAuth2 {
/// type Response = String;
/// fn process(&self, _: &[u8]) -> Self::Response {
/// format!(
/// "user={}\x01auth=Bearer {}\x01\x01",
/// self.user, self.access_token
/// )
/// }
/// }
///
/// fn main() {
/// let auth = OAuth2 {
/// user: String::from("me@example.com"),
/// access_token: String::from("<access_token>"),
/// };
/// let domain = "imap.example.com";
/// let tls = TlsConnector::builder().build().unwrap();
/// let client = imap::connect((domain, 993), domain, &tls).unwrap();
/// match client.authenticate("XOAUTH2", auth) {
/// Ok(session) => {
/// // you are successfully authenticated!
/// },
/// Err((e, orig_client)) => {
/// eprintln!("error authenticating: {}", e);
/// // prompt user and try again with orig_client here
/// return;
/// }
/// };
/// }
/// ```
pub fn authenticate<A: Authenticator>( pub fn authenticate<A: Authenticator>(
mut self, mut self,
auth_type: &str, auth_type: &str,
@ -397,9 +345,7 @@ impl<T: Read + Write> Client<T> {
); );
let challenge = ok_or_unauth_client_err!( let challenge = ok_or_unauth_client_err!(
base64::decode(data.as_str()) base64::decode(data.as_str())
.map_err(|_| .map_err(|e| Error::Parse(ParseError::Authentication(data, Some(e)))),
Error::Parse(ParseError::Authentication(data))
),
self self
); );
let raw_response = &authenticator.process(&challenge); let raw_response = &authenticator.process(&challenge);
@ -414,62 +360,31 @@ impl<T: Read + Write> Client<T> {
} }
} }
} }
/// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is
/// returned; on error the original `Client` instance is returned in addition to the error.
/// This is because `login` takes ownership of `self`, so in order to try again (e.g. after
/// prompting the user for credetials), ownership of the original `Client` needs to be
/// transferred back to the caller.
///
/// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # fn main() {
/// # let ssl_connector = TlsConnector::builder().build().unwrap();
/// let ssl_client = imap::client::secure_connect(
/// ("imap.example.org", 993),
/// "imap.example.org",
/// &ssl_connector).unwrap();
///
/// // try to login
/// let session = match ssl_client.login("user", "pass") {
/// Ok(s) => s,
/// Err((e, orig_client)) => {
/// eprintln!("error logging in: {}", e);
/// // prompt user and try again with orig_client here
/// return;
/// }
/// };
///
/// // use session for IMAP commands
/// # }
pub fn login(
mut self,
username: &str,
password: &str,
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
let u = ok_or_unauth_client_err!(validate_str(username), self);
let p = ok_or_unauth_client_err!(validate_str(password), self);
ok_or_unauth_client_err!(
self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)),
self
);
Ok(Session::new(self.conn))
}
} }
impl<T: Read + Write> Session<T> { impl<T: Read + Write> Session<T> {
// not public, just to avoid duplicating the channel creation code // not public, just to avoid duplicating the channel creation code
fn new(conn: Connection<T>) -> Self { fn new(conn: Connection<T>) -> Self {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
Session { conn, unsolicited_responses: rx, unsolicited_responses_tx: tx } Session {
conn,
unsolicited_responses: rx,
unsolicited_responses_tx: tx,
}
} }
/// Selects a mailbox /// Selects a mailbox
/// ///
/// The `SELECT` command selects a mailbox so that messages in the mailbox can be accessed.
/// Note that earlier versions of this protocol only required the FLAGS, EXISTS, and RECENT
/// untagged data; consequently, client implementations SHOULD implement default behavior for
/// missing data as discussed with the individual item.
///
/// Only one mailbox can be selected at a time in a connection; simultaneous access to multiple
/// mailboxes requires multiple connections. The `SELECT` command automatically deselects any
/// currently selected mailbox before attempting the new selection. Consequently, if a mailbox
/// is selected and a `SELECT` command that fails is attempted, no mailbox is selected.
///
/// Note that the server *is* allowed to unilaterally send things to the client for messages in /// Note that the server *is* allowed to unilaterally send things to the client for messages in
/// a selected mailbox whose status has changed. See the note on [unilateral server responses /// a selected mailbox whose status has changed. See the note on [unilateral server responses
/// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that if you use /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that if you use
@ -477,11 +392,15 @@ impl<T: Read + Write> Session<T> {
/// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the /// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the
/// `unsolicited_responses` channel of the [`Session`](struct.Session.html). /// `unsolicited_responses` channel of the [`Session`](struct.Session.html).
pub fn select(&mut self, mailbox_name: &str) -> Result<Mailbox> { pub fn select(&mut self, mailbox_name: &str) -> Result<Mailbox> {
// TODO: also note READ/WRITE vs READ-only mode!
self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?))
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
} }
/// Examine is identical to Select, but the selected mailbox is identified as read-only /// The `EXAMINE` command is identical to [`Session::select`] and returns the same output;
/// however, the selected mailbox is identified as read-only. No changes to the permanent state
/// of the mailbox, including per-user state, are permitted; in particular, `EXAMINE` MUST NOT
/// cause messages to lose [`Flag::Recent`].
pub fn examine(&mut self, mailbox_name: &str) -> Result<Mailbox> { pub fn examine(&mut self, mailbox_name: &str) -> Result<Mailbox> {
self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?))
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
@ -578,22 +497,63 @@ impl<T: Read + Write> Session<T> {
self.run_command_and_check_ok("CLOSE") self.run_command_and_check_ok("CLOSE")
} }
/// Store alters data associated with a message in the mailbox. /// The [`STORE` command](https://tools.ietf.org/html/rfc3501#section-6.4.6) alters data
/// associated with a message in the mailbox. Normally, `STORE` will return the updated value
/// of the data with an untagged FETCH response. A suffix of `.SILENT` in `query` prevents the
/// untagged `FETCH`, and the server SHOULD assume that the client has determined the updated
/// value itself or does not care about the updated value.
///
/// The currently defined data items that can be stored are:
///
/// ```text
/// FLAGS <flag list>
/// Replace the flags for the message (other than \Recent) with the
/// argument. The new value of the flags is returned as if a FETCH
/// of those flags was done.
///
/// FLAGS.SILENT <flag list>
/// Equivalent to FLAGS, but without returning a new value.
///
/// +FLAGS <flag list>
/// Add the argument to the flags for the message. The new value
/// of the flags is returned as if a FETCH of those flags was done.
///
/// +FLAGS.SILENT <flag list>
/// Equivalent to +FLAGS, but without returning a new value.
///
/// -FLAGS <flag list>
/// Remove the argument from the flags for the message. The new
/// value of the flags is returned as if a FETCH of those flags was
/// done.
///
/// -FLAGS.SILENT <flag list>
/// Equivalent to -FLAGS, but without returning a new value.
/// ```
pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> { pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query)) self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
} }
/// Equivalent to [`Session::store`], except that all identifiers in `sequence_set` are
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
pub fn uid_store(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> { pub fn uid_store(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query)) self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
} }
/// Copy copies the specified message to the end of the specified destination mailbox. /// The [`COPY` command](https://tools.ietf.org/html/rfc3501#section-6.4.7) copies the
/// specified message(s) to the end of the specified destination mailbox. The flags and
/// internal date of the message(s) SHOULD be preserved, and [`Flag::Recent`] SHOULD be set, in
/// the copy.
///
/// If the `COPY` command is unsuccessful for any reason, server implementations MUST restore
/// the destination mailbox to its state before the `COPY` attempt.
pub fn copy(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> { pub fn copy(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name)) self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name))
} }
/// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
pub fn uid_copy(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> { pub fn uid_copy(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name)) self.run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name))
} }
@ -602,6 +562,34 @@ impl<T: Read + Write> Session<T> {
/// named `mv` instead of `move` due to it being a reserved keyword. /// named `mv` instead of `move` due to it being a reserved keyword.
/// The MOVE command is defined in [RFC 6851 - "Internet Message Access Protocol (IMAP) /// The MOVE command is defined in [RFC 6851 - "Internet Message Access Protocol (IMAP)
/// - MOVE Extension"](https://tools.ietf.org/html/rfc6851#section-3). /// - MOVE Extension"](https://tools.ietf.org/html/rfc6851#section-3).
///
/// The [`MOVE` command](https://tools.ietf.org/html/rfc6851#section-3.1) is an IMAP extension
/// outlined in [RFC 6851](https://tools.ietf.org/html/rfc6851#section-3.1). It takes two
/// arguments: a sequence set and a named mailbox. Each message included in the set is moved,
/// rather than copied, from the selected (source) mailbox to the named (target) mailbox.
///
/// This means that a new message is created in the target mailbox with a
/// new [`Uid`], the original message is removed from the source mailbox, and
/// it appears to the client as a single action. This has the same
/// effect for each message as this sequence:
///
/// 1. COPY
/// 2. STORE +FLAGS.SILENT \DELETED
/// 3. EXPUNGE
///
/// Although the effect of the `MOVE` is the same as the preceding steps, the semantics are not
/// identical: The intermediate states produced by those steps do not occur, and the response
/// codes are different. In particular, though the `COPY` and `EXPUNGE` response codes will be
/// returned, response codes for a STORE MUST NOT be generated and [`Flag::Deleted`] MUST NOT
/// be set for any message.
///
/// Because a `MOVE` applies to a set of messages, it might fail partway through the set.
/// Regardless of whether the command is successful in moving the entire set, each individual
/// message SHOULD either be moved or unaffected. The server MUST leave each message in a
/// state where it is in at least one of the source or target mailboxes (no message can be lost
/// or orphaned). The server SHOULD NOT leave any message in both mailboxes (it would be bad
/// for a partial failure to result in a bunch of duplicate messages). This is true even if
/// the server returns with [`Error::No`].
pub fn mv(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> { pub fn mv(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(&format!( self.run_command_and_check_ok(&format!(
"MOVE {} {}", "MOVE {} {}",
@ -610,9 +598,10 @@ impl<T: Read + Write> Session<T> {
)) ))
} }
/// Moves each message in the uid set into the destination mailbox. /// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are
/// The UID MOVE command is defined in [RFC 6851 - "Internet Message Access Protocol (IMAP) /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8)
/// - MOVE Extension"](https://tools.ietf.org/html/rfc6851#section-3). /// and the [semantics of `MOVE` and `UID
/// MOVE`](https://tools.ietf.org/html/rfc6851#section-3.3).
pub fn uid_mv(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> { pub fn uid_mv(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(&format!( self.run_command_and_check_ok(&format!(
"UID MOVE {} {}", "UID MOVE {} {}",
@ -663,8 +652,8 @@ impl<T: Read + Write> Session<T> {
/// Returns a handle that can be used to block until the state of the currently selected /// Returns a handle that can be used to block until the state of the currently selected
/// mailbox changes. /// mailbox changes.
pub fn idle(&mut self) -> Result<IdleHandle<T>> { pub fn idle(&mut self) -> Result<extensions::idle::Handle<T>> {
IdleHandle::new(self) extensions::idle::Handle::new(self)
} }
/// The APPEND command adds a mail to a mailbox. /// The APPEND command adds a mail to a mailbox.
@ -690,7 +679,7 @@ impl<T: Read + Write> Session<T> {
/// Searches the mailbox for messages that match the given criteria and returns /// Searches the mailbox for messages that match the given criteria and returns
/// the list of unique identifier numbers of those messages. /// the list of unique identifier numbers of those messages.
pub fn uid_search(&mut self, query: &str) -> Result<HashSet<u32>> { pub fn uid_search(&mut self, query: &str) -> Result<HashSet<Uid>> {
self.run_command_and_read_response(&format!("UID SEARCH {}", query)) self.run_command_and_read_response(&format!("UID SEARCH {}", query))
.and_then(|lines| parse_ids(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_ids(lines, &mut self.unsolicited_responses_tx))
} }
@ -738,13 +727,13 @@ impl<T: Read + Write> Connection<T> {
self.read_response() self.read_response()
} }
fn read_response(&mut self) -> Result<Vec<u8>> { pub(crate) fn read_response(&mut self) -> Result<Vec<u8>> {
let mut v = Vec::new(); let mut v = Vec::new();
self.read_response_onto(&mut v)?; self.read_response_onto(&mut v)?;
Ok(v) Ok(v)
} }
fn read_response_onto(&mut self, data: &mut Vec<u8>) -> Result<()> { pub(crate) fn read_response_onto(&mut self, data: &mut Vec<u8>) -> Result<()> {
let mut continue_from = None; let mut continue_from = None;
let mut try_first = !data.is_empty(); let mut try_first = !data.is_empty();
let match_tag = format!("{}{}", TAG_PREFIX, self.tag); let match_tag = format!("{}{}", TAG_PREFIX, self.tag);
@ -816,7 +805,7 @@ impl<T: Read + Write> Connection<T> {
} }
} }
fn readline(&mut self, into: &mut Vec<u8>) -> Result<usize> { pub(crate) fn readline(&mut self, into: &mut Vec<u8>) -> Result<usize> {
use std::io::BufRead; use std::io::BufRead;
let read = self.stream.read_until(LF, into)?; let read = self.stream.read_until(LF, into)?;
if read == 0 { if read == 0 {
@ -827,7 +816,7 @@ impl<T: Read + Write> Connection<T> {
// Remove CRLF // Remove CRLF
let len = into.len(); let len = into.len();
let line = &into[(len - read)..(len - 2)]; let line = &into[(len - read)..(len - 2)];
print!("S: {}\n", String::from_utf8_lossy(line)); eprint!("S: {}\n", String::from_utf8_lossy(line));
} }
Ok(read) Ok(read)
@ -838,12 +827,12 @@ impl<T: Read + Write> Connection<T> {
format!("{}{} {}", TAG_PREFIX, self.tag, command) format!("{}{} {}", TAG_PREFIX, self.tag, command)
} }
fn write_line(&mut self, buf: &[u8]) -> Result<()> { pub(crate) fn write_line(&mut self, buf: &[u8]) -> Result<()> {
self.stream.write_all(buf)?; self.stream.write_all(buf)?;
self.stream.write_all(&[CR, LF])?; self.stream.write_all(&[CR, LF])?;
self.stream.flush()?; self.stream.flush()?;
if self.debug { if self.debug {
print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); eprint!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap());
} }
Ok(()) Ok(())
} }
@ -948,19 +937,19 @@ mod tests {
#[test] #[test]
fn authenticate() { fn authenticate() {
let response = b"+ YmFy\r\n\ let response = b"+ YmFy\r\n\
a1 OK Logged in\r\n".to_vec(); a1 OK Logged in\r\n"
.to_vec();
let command = "a1 AUTHENTICATE PLAIN\r\n\ let command = "a1 AUTHENTICATE PLAIN\r\n\
Zm9v\r\n"; Zm9v\r\n";
let mock_stream = MockStream::new(response); let mock_stream = MockStream::new(response);
let client = Client::new(mock_stream); let client = Client::new(mock_stream);
enum Authenticate { Auth }; enum Authenticate {
Auth,
};
impl Authenticator for Authenticate { impl Authenticator for Authenticate {
type Response = Vec<u8>; type Response = Vec<u8>;
fn process(&self, challenge: &[u8]) -> Self::Response { fn process(&self, challenge: &[u8]) -> Self::Response {
assert!( assert!(challenge == b"bar", "Invalid authenticate challenge");
challenge == b"bar",
"Invalid authenticate challenge"
);
b"foo".to_vec() b"foo".to_vec()
} }
} }
@ -1102,11 +1091,11 @@ mod tests {
.to_vec(); .to_vec();
let expected_mailbox = Mailbox { let expected_mailbox = Mailbox {
flags: vec![ flags: vec![
"\\Answered".to_string(), Flag::Answered,
"\\Flagged".to_string(), Flag::Flagged,
"\\Deleted".to_string(), Flag::Deleted,
"\\Seen".to_string(), Flag::Seen,
"\\Draft".to_string(), Flag::Draft,
], ],
exists: 1, exists: 1,
recent: 1, recent: 1,
@ -1141,22 +1130,22 @@ mod tests {
.to_vec(); .to_vec();
let expected_mailbox = Mailbox { let expected_mailbox = Mailbox {
flags: vec![ flags: vec![
"\\Answered".to_string(), Flag::Answered,
"\\Flagged".to_string(), Flag::Flagged,
"\\Deleted".to_string(), Flag::Deleted,
"\\Seen".to_string(), Flag::Seen,
"\\Draft".to_string(), Flag::Draft,
], ],
exists: 1, exists: 1,
recent: 1, recent: 1,
unseen: Some(1), unseen: Some(1),
permanent_flags: vec![ permanent_flags: vec![
"\\*".to_string(), Flag::MayCreate,
"\\Answered".to_string(), Flag::Answered,
"\\Flagged".to_string(), Flag::Flagged,
"\\Deleted".to_string(), Flag::Deleted,
"\\Draft".to_string(), Flag::Draft,
"\\Seen".to_string(), Flag::Seen,
], ],
uid_next: Some(2), uid_next: Some(2),
uid_validity: Some(1257842737), uid_validity: Some(1257842737),
@ -1197,7 +1186,7 @@ mod tests {
let mock_stream = MockStream::new(response); let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream); let mut session = mock_session!(mock_stream);
let ids = session.uid_search("Unseen").unwrap(); let ids = session.uid_search("Unseen").unwrap();
let ids: HashSet<u32> = ids.iter().cloned().collect(); let ids: HashSet<Uid> = ids.iter().cloned().collect();
assert!( assert!(
session.stream.get_ref().written_buf == b"a1 UID SEARCH Unseen\r\n".to_vec(), session.stream.get_ref().written_buf == b"a1 UID SEARCH Unseen\r\n".to_vec(),
"Invalid search command" "Invalid search command"

View file

@ -1,3 +1,5 @@
//! IMAP error types.
use std::error::Error as StdError; use std::error::Error as StdError;
use std::fmt; use std::fmt;
use std::io::Error as IoError; use std::io::Error as IoError;
@ -5,11 +7,13 @@ use std::net::TcpStream;
use std::result; use std::result;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use base64::DecodeError;
use bufstream::IntoInnerError as BufError; use bufstream::IntoInnerError as BufError;
use imap_proto::Response; use imap_proto::Response;
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
use native_tls::HandshakeError as TlsHandshakeError; use native_tls::HandshakeError as TlsHandshakeError;
/// A convenience wrapper around `Result` for `imap::Error`.
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
/// A set of errors that can occur in the IMAP client /// A set of errors that can occur in the IMAP client
@ -27,11 +31,12 @@ pub enum Error {
NoResponse(String), NoResponse(String),
/// The connection was terminated unexpectedly. /// The connection was terminated unexpectedly.
ConnectionLost, ConnectionLost,
// Error parsing a server response. /// Error parsing a server response.
Parse(ParseError), Parse(ParseError),
// Error validating input data /// Command inputs were not valid [IMAP
/// strings](https://tools.ietf.org/html/rfc3501#section-4.3).
Validate(ValidateError), Validate(ValidateError),
// Error appending a mail /// Error appending an e-mail.
Append, Append,
} }
@ -41,6 +46,12 @@ impl From<IoError> for Error {
} }
} }
impl From<ParseError> for Error {
fn from(err: ParseError) -> Error {
Error::Parse(err)
}
}
impl<T> From<BufError<T>> for Error { impl<T> From<BufError<T>> for Error {
fn from(err: BufError<T>) -> Error { fn from(err: BufError<T>) -> Error {
Error::Io(err.into()) Error::Io(err.into())
@ -106,14 +117,16 @@ impl StdError for Error {
} }
} }
/// An error occured while trying to parse a server response.
#[derive(Debug)] #[derive(Debug)]
pub enum ParseError { pub enum ParseError {
// Indicates an error parsing the status response. Such as OK, NO, and BAD. /// Indicates an error parsing the status response. Such as OK, NO, and BAD.
Invalid(Vec<u8>), Invalid(Vec<u8>),
// An unexpected response was encountered. /// An unexpected response was encountered.
Unexpected(String), Unexpected(String),
// Authentication errors. /// The client could not find or decode the server's authentication challenge.
Authentication(String), Authentication(String, Option<DecodeError>),
/// The client receive data that was not UTF-8 encoded.
DataNotUtf8(FromUtf8Error), DataNotUtf8(FromUtf8Error),
} }
@ -130,19 +143,21 @@ impl StdError for ParseError {
match *self { match *self {
ParseError::Invalid(_) => "Unable to parse status response", ParseError::Invalid(_) => "Unable to parse status response",
ParseError::Unexpected(_) => "Encountered unexpected parsed response", ParseError::Unexpected(_) => "Encountered unexpected parsed response",
ParseError::Authentication(_) => "Unable to parse authentication response", ParseError::Authentication(_, _) => "Unable to parse authentication response",
ParseError::DataNotUtf8(_) => "Unable to parse data as UTF-8 text", ParseError::DataNotUtf8(_) => "Unable to parse data as UTF-8 text",
} }
} }
fn cause(&self) -> Option<&StdError> { fn cause(&self) -> Option<&StdError> {
match *self { match *self {
ParseError::Authentication(_, Some(ref e)) => Some(e),
_ => None, _ => None,
} }
} }
} }
// Invalid character found. Expand as needed /// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in an input
/// string.
#[derive(Debug)] #[derive(Debug)]
pub struct ValidateError(pub char); pub struct ValidateError(pub char);

162
src/extensions/idle.rs Normal file
View file

@ -0,0 +1,162 @@
//! Adds support for the IMAP IDLE command specificed in [RFC
//! 2177](https://tools.ietf.org/html/rfc2177).
use crate::client::Session;
use crate::error::{Error, Result};
use native_tls::TlsStream;
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::time::Duration;
/// `Handle` allows a client to block waiting for changes to the remote mailbox.
///
/// The handle blocks using the IMAP IDLE command specificed in [RFC
/// 2177](https://tools.ietf.org/html/rfc2177).
///
/// As long a the handle is active, the mailbox cannot be otherwise accessed.
#[derive(Debug)]
pub struct Handle<'a, T: Read + Write + 'a> {
session: &'a mut Session<T>,
keepalive: Duration,
done: bool,
}
/// Must be implemented for a transport in order for a `Session` using that transport to support
/// operations with timeouts.
///
/// Examples of where this is useful is for `Handle::wait_keepalive` and
/// `Handle::wait_timeout`.
pub trait SetReadTimeout {
/// Set the timeout for subsequent reads to the given one.
///
/// If `timeout` is `None`, the read timeout should be removed.
///
/// See also `std::net::TcpStream::set_read_timeout`.
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()>;
}
impl<'a, T: Read + Write + 'a> Handle<'a, T> {
pub(crate) fn new(session: &'a mut Session<T>) -> Result<Self> {
let mut h = Handle {
session,
keepalive: Duration::from_secs(29 * 60),
done: false,
};
h.init()?;
Ok(h)
}
fn init(&mut self) -> Result<()> {
// https://tools.ietf.org/html/rfc2177
//
// The IDLE command takes no arguments.
self.session.run_command("IDLE")?;
// A tagged response will be sent either
//
// a) if there's an error, or
// b) *after* we send DONE
let mut v = Vec::new();
self.session.readline(&mut v)?;
if v.starts_with(b"+") {
self.done = false;
return Ok(());
}
self.session.read_response_onto(&mut v)?;
// We should *only* get a continuation on an error (i.e., it gives BAD or NO).
unreachable!();
}
fn terminate(&mut self) -> Result<()> {
if !self.done {
self.done = true;
self.session.write_line(b"DONE")?;
self.session.read_response().map(|_| ())
} else {
Ok(())
}
}
/// Internal helper that doesn't consume self.
///
/// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`.
fn wait_inner(&mut self) -> Result<()> {
let mut v = Vec::new();
match self.session.readline(&mut v).map(|_| ()) {
Err(Error::Io(ref e))
if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock =>
{
// we need to refresh the IDLE connection
self.terminate()?;
self.init()?;
self.wait_inner()
}
r => r,
}
}
/// Block until the selected mailbox changes.
pub fn wait(mut self) -> Result<()> {
self.wait_inner()
}
}
impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
/// Set the keep-alive interval to use when `wait_keepalive` is called.
///
/// The interval defaults to 29 minutes as dictated by RFC 2177.
pub fn set_keepalive(&mut self, interval: Duration) {
self.keepalive = interval;
}
/// Block until the selected mailbox changes.
///
/// This method differs from `Handle::wait` in that it will periodically refresh the IDLE
/// connection, to prevent the server from timing out our connection. The keepalive interval is
/// set to 29 minutes by default, as dictated by RFC 2177, but can be changed using
/// `set_keepalive`.
///
/// This is the recommended method to use for waiting.
pub fn wait_keepalive(self) -> Result<()> {
// The server MAY consider a client inactive if it has an IDLE command
// running, and if such a server has an inactivity timeout it MAY log
// the client off implicitly at the end of its timeout period. Because
// of that, clients using IDLE are advised to terminate the IDLE and
// re-issue it at least every 29 minutes to avoid being logged off.
// This still allows a client to receive immediate mailbox updates even
// though it need only "poll" at half hour intervals.
let keepalive = self.keepalive;
self.wait_timeout(keepalive)
}
/// Block until the selected mailbox changes, or until the given amount of time has expired.
pub fn wait_timeout(mut self, timeout: Duration) -> Result<()> {
self.session
.stream
.get_mut()
.set_read_timeout(Some(timeout))?;
let res = self.wait_inner();
self.session.stream.get_mut().set_read_timeout(None).is_ok();
res
}
}
impl<'a, T: Read + Write + 'a> Drop for Handle<'a, T> {
fn drop(&mut self) {
// we don't want to panic here if we can't terminate the Idle
self.terminate().is_ok();
}
}
impl<'a> SetReadTimeout for TcpStream {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
TcpStream::set_read_timeout(self, timeout).map_err(Error::Io)
}
}
impl<'a> SetReadTimeout for TlsStream<TcpStream> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
self.get_ref().set_read_timeout(timeout).map_err(Error::Io)
}
}

2
src/extensions/mod.rs Normal file
View file

@ -0,0 +1,2 @@
//! Implementations of various IMAP extensions.
pub mod idle;

View file

@ -1,61 +1,60 @@
//! IMAP client bindings for Rust. //! This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC
//! 3501](https://tools.ietf.org/html/rfc3501)). After authenticating with the server, IMAP lets
//! you list, fetch, and search for e-mails, as well as monitor mailboxes for changes.
//! //!
//! # Usage //! To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can
//! then use [`Client::login`] or [`Client::authenticate`] to perform username/password or
//! challenge/response authentication respectively. This in turn gives you an authenticated
//! [`Session`], which lets you access the mailboxes at the server.
//! //!
//! Here is a basic example of using the client. //! Below is a basic client example. See the `examples/` directory for more.
//! See the `examples/` directory for more examples.
//! //!
//! ```no_run //! ```no_run
//! extern crate imap; //! extern crate imap;
//! extern crate native_tls; //! extern crate native_tls;
//! //!
//! // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. //! fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
//! // See: https://support.google.com/accounts/answer/6010255?hl=en //! let domain = "imap.example.com";
//! // Look at the `examples/gmail_oauth2.rs` for how to connect to gmail securely. //! let tls = native_tls::TlsConnector::builder().build().unwrap();
//! fn main() {
//! let domain = "imap.gmail.com";
//! let port = 993;
//! let socket_addr = (domain, port);
//! let ssl_connector = native_tls::TlsConnector::builder().build().unwrap();
//! let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap();
//! //!
//! let mut imap_session = match client.login("username", "password") { //! // we pass in the domain twice to check that the server's TLS
//! Ok(c) => c, //! // certificate is valid for the domain we're connecting to.
//! Err((e, _unauth_client)) => { //! let client = imap::connect((domain, 993), domain, &tls).unwrap();
//! eprintln!("failed to login: {}", e); //!
//! return; //! // the client we have here is unauthenticated.
//! } //! // to do anything useful with the e-mails, we need to log in
//! let mut imap_session = client
//! .login("me@example.com", "password")
//! .map_err(|e| e.0)?;
//!
//! // we want to fetch the first email in the INBOX mailbox
//! imap_session.select("INBOX")?;
//!
//! // fetch message number 1 in this mailbox, along with its RFC822 field.
//! // RFC 822 dictates the format of the body of e-mails
//! let messages = imap_session.fetch("1", "RFC822")?;
//! let message = if let Some(m) = messages.iter().next() {
//! m
//! } else {
//! return Ok(None);
//! }; //! };
//! //!
//! match imap_session.capabilities() { //! // extract the message's body
//! Ok(capabilities) => { //! let body = message.rfc822().expect("message did not have a body!");
//! for capability in capabilities.iter() { //! let body = std::str::from_utf8(body)
//! println!("{}", capability); //! .expect("message was not valid utf-8")
//! } //! .to_string();
//! }
//! Err(e) => println!("Error parsing capabilities: {}", e),
//! };
//! //!
//! match imap_session.select("INBOX") { //! // be nice to the server and log out
//! Ok(mailbox) => { //! imap_session.logout()?;
//! println!("{}", mailbox);
//! }
//! Err(e) => println!("Error selecting INBOX: {}", e),
//! };
//! //!
//! match imap_session.fetch("2", "body[text]") { //! Ok(Some(body))
//! Ok(messages) => {
//! for message in messages.iter() {
//! print!("{:?}", message);
//! }
//! }
//! Err(e) => println!("Error Fetching email 2: {}", e),
//! };
//!
//! imap_session.logout().unwrap();
//! } //! }
//! ``` //! ```
#![deny(missing_docs)]
extern crate base64;
extern crate bufstream; extern crate bufstream;
extern crate imap_proto; extern crate imap_proto;
extern crate native_tls; extern crate native_tls;
@ -63,13 +62,18 @@ extern crate nom;
extern crate regex; extern crate regex;
mod parse; mod parse;
mod types;
pub mod authenticator; pub mod types;
pub mod client;
mod authenticator;
pub use authenticator::Authenticator;
mod client;
pub use client::*;
pub mod error; pub mod error;
pub use types::*; pub mod extensions;
#[cfg(test)] #[cfg(test)]
mod mock_stream; mod mock_stream;

View file

@ -14,7 +14,7 @@ pub fn parse_authenticate_response(line: String) -> Result<String> {
return Ok(String::from(data)); return Ok(String::from(data));
} }
Err(Error::Parse(ParseError::Authentication(line))) Err(Error::Parse(ParseError::Authentication(line, None)))
} }
enum MapOrNot<T> { enum MapOrNot<T> {
@ -24,9 +24,13 @@ enum MapOrNot<T> {
Ignore, Ignore,
} }
unsafe fn parse_many<T, F>(lines: Vec<u8>, mut map: F, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> ZeroCopyResult<Vec<T>> unsafe fn parse_many<T, F>(
lines: Vec<u8>,
mut map: F,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> ZeroCopyResult<Vec<T>>
where where
F: FnMut(Response<'static>) -> MapOrNot<T>, F: FnMut(Response<'static>) -> Result<MapOrNot<T>>,
{ {
let f = |mut lines: &'static [u8]| { let f = |mut lines: &'static [u8]| {
let mut things = Vec::new(); let mut things = Vec::new();
@ -39,26 +43,27 @@ where
Ok((rest, resp)) => { Ok((rest, resp)) => {
lines = rest; lines = rest;
match map(resp) { match map(resp)? {
MapOrNot::Map(t) => things.push(t), MapOrNot::Map(t) => things.push(t),
MapOrNot::Not(resp) => { MapOrNot::Not(resp) => {
// check if this is simply a unilateral server response // check if this is simply a unilateral server response
// (see Section 7 of RFC 3501): // (see Section 7 of RFC 3501):
match resp { match resp {
Response::MailboxData(MailboxDatum::Recent(n)) => { Response::MailboxData(MailboxDatum::Recent(n)) => {
unsolicited.send(UnsolicitedResponse::Recent(n)) unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
.unwrap();
} }
Response::MailboxData(MailboxDatum::Exists(n)) => { Response::MailboxData(MailboxDatum::Exists(n)) => {
unsolicited.send(UnsolicitedResponse::Exists(n)) unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap();
.unwrap();
} }
Response::Expunge(id) => { Response::Expunge(id) => {
unsolicited.send(UnsolicitedResponse::Expunge(id)) unsolicited.send(UnsolicitedResponse::Expunge(id)).unwrap();
.unwrap();
} }
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)) unsolicited
.send(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
.unwrap(); .unwrap();
} }
Response::Fetch(..) => { Response::Fetch(..) => {
@ -66,7 +71,7 @@ where
} }
resp => break Err(resp.into()), resp => break Err(resp.into()),
} }
}, }
MapOrNot::Ignore => continue, MapOrNot::Ignore => continue,
} }
} }
@ -80,7 +85,10 @@ where
ZeroCopy::new(lines, f) ZeroCopy::new(lines, f)
} }
pub fn parse_names(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> ZeroCopyResult<Vec<Name>> { pub fn parse_names(
lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> ZeroCopyResult<Vec<Name>> {
use imap_proto::MailboxDatum; use imap_proto::MailboxDatum;
let f = |resp| match resp { let f = |resp| match resp {
// https://github.com/djc/imap-proto/issues/4 // https://github.com/djc/imap-proto/issues/4
@ -93,18 +101,21 @@ pub fn parse_names(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedRes
flags, flags,
delimiter, delimiter,
name, name,
}) => MapOrNot::Map(Name { }) => Ok(MapOrNot::Map(Name {
attributes: flags, attributes: flags.into_iter().map(NameAttribute::from).collect(),
delimiter, delimiter,
name, name,
}), })),
resp => MapOrNot::Not(resp), resp => Ok(MapOrNot::Not(resp)),
}; };
unsafe { parse_many(lines, f, unsolicited) } unsafe { parse_many(lines, f, unsolicited) }
} }
pub fn parse_fetches(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> ZeroCopyResult<Vec<Fetch>> { pub fn parse_fetches(
lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> ZeroCopyResult<Vec<Fetch>> {
let f = |resp| match resp { let f = |resp| match resp {
Response::Fetch(num, attrs) => { Response::Fetch(num, attrs) => {
let mut fetch = Fetch { let mut fetch = Fetch {
@ -120,7 +131,7 @@ pub fn parse_fetches(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedR
use imap_proto::AttributeValue; use imap_proto::AttributeValue;
match attr { match attr {
AttributeValue::Flags(flags) => { AttributeValue::Flags(flags) => {
fetch.flags.extend(flags); fetch.flags.extend(flags.into_iter().map(Flag::from));
} }
AttributeValue::Uid(uid) => fetch.uid = Some(uid), AttributeValue::Uid(uid) => fetch.uid = Some(uid),
AttributeValue::Rfc822(rfc) => fetch.rfc822 = rfc, AttributeValue::Rfc822(rfc) => fetch.rfc822 = rfc,
@ -130,15 +141,18 @@ pub fn parse_fetches(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedR
} }
} }
MapOrNot::Map(fetch) Ok(MapOrNot::Map(fetch))
} }
resp => MapOrNot::Not(resp), resp => Ok(MapOrNot::Not(resp)),
}; };
unsafe { parse_many(lines, f, unsolicited) } unsafe { parse_many(lines, f, unsolicited) }
} }
pub fn parse_capabilities(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> ZeroCopyResult<Capabilities> { pub fn parse_capabilities(
lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> ZeroCopyResult<Capabilities> {
let f = |mut lines| { let f = |mut lines| {
let mut caps = HashSet::new(); let mut caps = HashSet::new();
loop { loop {
@ -151,7 +165,12 @@ pub fn parse_capabilities(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<Unsolic
lines = rest; lines = rest;
match data { match data {
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); unsolicited
.send(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
.unwrap();
} }
Response::MailboxData(MailboxDatum::Recent(n)) => { Response::MailboxData(MailboxDatum::Recent(n)) => {
unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
@ -181,7 +200,10 @@ pub fn parse_capabilities(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<Unsolic
unsafe { ZeroCopy::new(lines, f) } unsafe { ZeroCopy::new(lines, f) }
} }
pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> Result<Mailbox> { pub fn parse_mailbox(
mut lines: &[u8],
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Mailbox> {
let mut mailbox = Mailbox::default(); let mut mailbox = Mailbox::default();
loop { loop {
@ -209,7 +231,7 @@ pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender<Unsolicite
Some(ResponseCode::PermanentFlags(flags)) => { Some(ResponseCode::PermanentFlags(flags)) => {
mailbox mailbox
.permanent_flags .permanent_flags
.extend(flags.into_iter().map(|s| s.to_string())); .extend(flags.into_iter().map(String::from).map(Flag::from));
} }
_ => {} _ => {}
} }
@ -220,7 +242,12 @@ pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender<Unsolicite
use imap_proto::MailboxDatum; use imap_proto::MailboxDatum;
match m { match m {
MailboxDatum::Status { mailbox, status } => { MailboxDatum::Status { mailbox, status } => {
unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); unsolicited
.send(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
.unwrap();
} }
MailboxDatum::Exists(e) => { MailboxDatum::Exists(e) => {
mailbox.exists = e; mailbox.exists = e;
@ -231,7 +258,7 @@ pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender<Unsolicite
MailboxDatum::Flags(flags) => { MailboxDatum::Flags(flags) => {
mailbox mailbox
.flags .flags
.extend(flags.into_iter().map(|s| s.to_string())); .extend(flags.into_iter().map(String::from).map(Flag::from));
} }
MailboxDatum::SubList { .. } | MailboxDatum::List { .. } => {} MailboxDatum::SubList { .. } | MailboxDatum::List { .. } => {}
} }
@ -254,7 +281,10 @@ pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender<Unsolicite
} }
} }
pub fn parse_ids(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>) -> Result<HashSet<u32>> { pub fn parse_ids(
lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<HashSet<u32>> {
let mut lines = &lines[..]; let mut lines = &lines[..];
let mut ids = HashSet::new(); let mut ids = HashSet::new();
loop { loop {
@ -271,7 +301,12 @@ pub fn parse_ids(lines: Vec<u8>, unsolicited: &mut mpsc::Sender<UnsolicitedRespo
lines = rest; lines = rest;
match data { match data {
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); unsolicited
.send(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
.unwrap();
} }
Response::MailboxData(MailboxDatum::Recent(n)) => { Response::MailboxData(MailboxDatum::Recent(n)) => {
unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
@ -328,7 +363,10 @@ mod tests {
let names = parse_names(lines.to_vec(), &mut send).unwrap(); let names = parse_names(lines.to_vec(), &mut send).unwrap();
assert!(recv.try_recv().is_err()); assert!(recv.try_recv().is_err());
assert_eq!(names.len(), 1); assert_eq!(names.len(), 1);
assert_eq!(names[0].attributes(), &["\\HasNoChildren"]); assert_eq!(
names[0].attributes(),
&[NameAttribute::from("\\HasNoChildren")]
);
assert_eq!(names[0].delimiter(), "."); assert_eq!(names[0].delimiter(), ".");
assert_eq!(names[0].name(), "INBOX"); assert_eq!(names[0].name(), "INBOX");
} }
@ -352,11 +390,11 @@ mod tests {
assert!(recv.try_recv().is_err()); assert!(recv.try_recv().is_err());
assert_eq!(fetches.len(), 2); assert_eq!(fetches.len(), 2);
assert_eq!(fetches[0].message, 24); assert_eq!(fetches[0].message, 24);
assert_eq!(fetches[0].flags(), &["\\Seen"]); assert_eq!(fetches[0].flags(), &[Flag::Seen]);
assert_eq!(fetches[0].uid, Some(4827943)); assert_eq!(fetches[0].uid, Some(4827943));
assert_eq!(fetches[0].rfc822(), None); assert_eq!(fetches[0].rfc822(), None);
assert_eq!(fetches[1].message, 25); assert_eq!(fetches[1].message, 25);
assert_eq!(fetches[1].flags(), &["\\Seen"]); assert_eq!(fetches[1].flags(), &[Flag::Seen]);
assert_eq!(fetches[1].uid, None); assert_eq!(fetches[1].uid, None);
assert_eq!(fetches[1].rfc822(), None); assert_eq!(fetches[1].rfc822(), None);
} }
@ -386,10 +424,12 @@ mod tests {
assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Expunge(4)); assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Expunge(4));
assert_eq!(names.len(), 1); assert_eq!(names.len(), 1);
assert_eq!(names[0].attributes(), &["\\HasNoChildren"]); assert_eq!(
names[0].attributes(),
&[NameAttribute::from("\\HasNoChildren")]
);
assert_eq!(names[0].delimiter(), "."); assert_eq!(names[0].delimiter(), ".");
assert_eq!(names[0].name(), "INBOX"); assert_eq!(names[0].name(), "INBOX");
} }
#[test] #[test]
@ -409,8 +449,18 @@ mod tests {
assert!(capabilities.has(e)); assert!(capabilities.has(e));
} }
assert_eq!(recv.try_recv().unwrap(), assert_eq!(
UnsolicitedResponse::Status("dev.github".to_string(), vec![Messages(10), UidNext(11), UidValidity(1408806928), Unseen(0)])); recv.try_recv().unwrap(),
UnsolicitedResponse::Status {
mailbox: "dev.github".to_string(),
attributes: vec![
Messages(10),
UidNext(11),
UidValidity(1408806928),
Unseen(0)
]
}
);
assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Exists(4)); assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Exists(4));
} }
@ -428,10 +478,19 @@ mod tests {
assert_eq!(ids, [23, 42, 4711].iter().cloned().collect()); assert_eq!(ids, [23, 42, 4711].iter().cloned().collect());
assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Recent(1)); assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Recent(1));
assert_eq!(recv.try_recv().unwrap(), assert_eq!(
UnsolicitedResponse::Status("INBOX".to_string(), vec![Messages(10), UidNext(11), UidValidity(1408806928), Unseen(0)])); recv.try_recv().unwrap(),
UnsolicitedResponse::Status {
mailbox: "INBOX".to_string(),
attributes: vec![
Messages(10),
UidNext(11),
UidValidity(1408806928),
Unseen(0)
]
}
);
} }
#[test] #[test]
fn parse_ids_test() { fn parse_ids_test() {

View file

@ -1,12 +1,45 @@
// Note that none of these fields are *actually* 'static. use std::borrow::Borrow;
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
use std::collections::hash_set::Iter; use std::collections::hash_set::Iter;
use std::collections::HashSet; use std::collections::HashSet;
pub struct Capabilities(pub(crate) HashSet<&'static str>);
use std::borrow::Borrow;
use std::hash::Hash; use std::hash::Hash;
/// From [section 7.2.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.2.1).
///
/// A list of capabilities that the server supports.
/// The capability list MUST include the atom "IMAP4rev1".
///
/// In addition, client and server implementations MUST implement the `STARTTLS`, `LOGINDISABLED`,
/// and `AUTH=PLAIN` (described in [IMAP-TLS](https://tools.ietf.org/html/rfc2595)) capabilities.
/// See the [Security Considerations section of the
/// RFC](https://tools.ietf.org/html/rfc3501#section-11) for important information.
///
/// A capability name which begins with `AUTH=` indicates that the server supports that particular
/// authentication mechanism.
///
/// The `LOGINDISABLED` capability indicates that the `LOGIN` command is disabled, and that the
/// server will respond with a [`Error::No`] response to any attempt to use the `LOGIN` command
/// even if the user name and password are valid. An IMAP client MUST NOT issue the `LOGIN`
/// command if the server advertises the `LOGINDISABLED` capability.
///
/// Other capability names indicate that the server supports an extension, revision, or amendment
/// to the IMAP4rev1 protocol. Server responses MUST conform to this document until the client
/// issues a command that uses the associated capability.
///
/// Capability names MUST either begin with `X` or be standard or standards-track IMAP4rev1
/// extensions, revisions, or amendments registered with IANA. A server MUST NOT offer
/// unregistered or non-standard capability names, unless such names are prefixed with
/// an `X`.
///
/// Client implementations SHOULD NOT require any capability name other than `IMAP4rev1`, and MUST
/// ignore any unknown capability names.
pub struct Capabilities(
// Note that this field isn't *actually* 'static.
// Rather, it is tied to the lifetime of the `ZeroCopy` that contains this `Name`.
pub(crate) HashSet<&'static str>,
);
impl Capabilities { impl Capabilities {
/// Check if the server has the given capability.
pub fn has<S: ?Sized>(&self, s: &S) -> bool pub fn has<S: ?Sized>(&self, s: &S) -> bool
where where
for<'a> &'a str: Borrow<S>, for<'a> &'a str: Borrow<S>,
@ -15,14 +48,17 @@ impl Capabilities {
self.0.contains(s) self.0.contains(s)
} }
/// Iterate over all the server's capabilities
pub fn iter(&self) -> Iter<&str> { pub fn iter(&self) -> Iter<&str> {
self.0.iter() self.0.iter()
} }
/// Returns how many capabilities the server has.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.0.len()
} }
/// Returns true if the server purports to have no capabilities.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_empty()
} }

View file

@ -1,28 +1,48 @@
// Note that none of these fields are *actually* 'static. use super::{Flag, Seq, Uid};
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
/// An IMAP [`FETCH` response](https://tools.ietf.org/html/rfc3501#section-7.4.2) that contains
/// data about a particular message. This response occurs as the result of a `FETCH` or `STORE`
/// command, as well as by unilateral server decision (e.g., flag updates).
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct Fetch { pub struct Fetch {
pub message: u32, /// The ordinal number of this message in its containing mailbox.
pub(crate) flags: Vec<&'static str>, pub message: Seq,
pub uid: Option<u32>,
/// A number expressing the unique identifier of the message.
pub uid: Option<Uid>,
// Note that none of these fields are *actually* 'static. Rather, they are tied to the lifetime
// of the `ZeroCopy` that contains this `Name`. That's also why they can't be public -- we can
// only return them with a lifetime tied to self.
pub(crate) flags: Vec<Flag<'static>>,
pub(crate) rfc822_header: Option<&'static [u8]>, pub(crate) rfc822_header: Option<&'static [u8]>,
pub(crate) rfc822: Option<&'static [u8]>, pub(crate) rfc822: Option<&'static [u8]>,
pub(crate) body: Option<&'static [u8]>, pub(crate) body: Option<&'static [u8]>,
} }
impl Fetch { impl Fetch {
pub fn flags(&self) -> &[&str] { /// A list of flags that are set for this message.
pub fn flags(&self) -> &[Flag] {
&self.flags[..] &self.flags[..]
} }
/// The bytes that make up the RFC822 header of this message, if `RFC822.HEADER` was included
/// in the flags argument to `FETCH`.
pub fn rfc822_header(&self) -> Option<&[u8]> { pub fn rfc822_header(&self) -> Option<&[u8]> {
self.rfc822_header self.rfc822_header
} }
/// The entire body of this message, if `RFC822` was included in the flags argument to `FETCH`.
/// The bytes SHOULD be interpreted by the client according to the content transfer encoding,
/// body type, and subtype.
pub fn rfc822(&self) -> Option<&[u8]> { pub fn rfc822(&self) -> Option<&[u8]> {
self.rfc822 self.rfc822
} }
/// An [MIME-IMB](https://tools.ietf.org/html/rfc2045) representation of this message, included
/// if `BODY` was included in the flags argument to `FETCH`. See also the documentation for
/// `BODYSTRUCTURE` in the documentation for [`FETCH
/// responses`](https://tools.ietf.org/html/rfc3501#section-7.4.2).
pub fn body(&self) -> Option<&[u8]> { pub fn body(&self) -> Option<&[u8]> {
self.body self.body
} }

View file

@ -1,13 +1,39 @@
use super::{Flag, Uid};
use std::fmt; use std::fmt;
/// Meta-information about an IMAP mailbox, as returned by
/// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends.
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Mailbox { pub struct Mailbox {
pub flags: Vec<String>, /// Defined flags in the mailbox. See the description of the [FLAGS
/// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail.
pub flags: Vec<Flag<'static>>,
/// The number of messages in the mailbox. See the description of the [EXISTS
/// response](https://tools.ietf.org/html/rfc3501#section-7.3.1) for more detail.
pub exists: u32, pub exists: u32,
/// The number of messages with the \Recent flag set. See the description of the [RECENT
/// response](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more detail.
pub recent: u32, pub recent: u32,
/// The message sequence number of the first unseen message in the mailbox. If this is
/// missing, the client can not make any assumptions about the first unseen message in the
/// mailbox, and needs to issue a `SEARCH` command if it wants to find it.
pub unseen: Option<u32>, pub unseen: Option<u32>,
pub permanent_flags: Vec<String>,
pub uid_next: Option<u32>, /// A list of message flags that the client can change permanently. If this is missing, the
/// client should assume that all flags can be changed permanently. If the client attempts to
/// STORE a flag that is not in this list list, the server will either ignore the change or
/// store the state change for the remainder of the current session only.
pub permanent_flags: Vec<Flag<'static>>,
/// The next unique identifier value. If this is missing, the client can not make any
/// assumptions about the next unique identifier value.
pub uid_next: Option<Uid>,
/// The unique identifier validity value. See [`Uid`] for more details. If this is missing,
/// the server does not support unique identifiers.
pub uid_validity: Option<u32>, pub uid_validity: Option<u32>,
} }

View file

@ -1,3 +1,194 @@
//! This module contains types used throughout the IMAP protocol.
use std::borrow::Cow;
/// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1).
///
/// A 32-bit value assigned to each message, which when used with the unique identifier validity
/// value (see below) forms a 64-bit value that MUST NOT refer to any other message in the mailbox
/// or any subsequent mailbox with the same name forever. Unique identifiers are assigned in a
/// strictly ascending fashion in the mailbox; as each message is added to the mailbox it is
/// assigned a higher UID than the message(s) which were added previously. Unlike message sequence
/// numbers, unique identifiers are not necessarily contiguous.
///
/// The unique identifier of a message MUST NOT change during the session, and SHOULD NOT change
/// between sessions. Any change of unique identifiers between sessions MUST be detectable using
/// the `UIDVALIDITY` mechanism discussed below. Persistent unique identifiers are required for a
/// client to resynchronize its state from a previous session with the server (e.g., disconnected
/// or offline access clients); this is discussed further in
/// [`IMAP-DISC`](https://tools.ietf.org/html/rfc3501#ref-IMAP-DISC).
///
/// Associated with every mailbox are two values which aid in unique identifier handling: the next
/// unique identifier value and the unique identifier validity value.
///
/// The next unique identifier value is the predicted value that will be assigned to a new message
/// in the mailbox. Unless the unique identifier validity also changes (see below), the next
/// unique identifier value MUST have the following two characteristics. First, the next unique
/// identifier value MUST NOT change unless new messages are added to the mailbox; and second, the
/// next unique identifier value MUST change whenever new messages are added to the mailbox, even
/// if those new messages are subsequently expunged.
///
/// > Note: The next unique identifier value is intended to provide a means for a client to
/// > determine whether any messages have been delivered to the mailbox since the previous time it
/// > checked this value. It is not intended to provide any guarantee that any message will have
/// > this unique identifier. A client can only assume, at the time that it obtains the next
/// > unique identifier value, that messages arriving after that time will have a UID greater than
/// > or equal to that value.
///
/// The unique identifier validity value is sent in a `UIDVALIDITY` response code in an `OK`
/// untagged response at mailbox selection time. If unique identifiers from an earlier session fail
/// to persist in this session, the unique identifier validity value MUST be greater than the one
/// used in the earlier session.
///
/// > Note: Ideally, unique identifiers SHOULD persist at all
/// > times. Although this specification recognizes that failure
/// > to persist can be unavoidable in certain server
/// > environments, it STRONGLY ENCOURAGES message store
/// > implementation techniques that avoid this problem. For
/// > example:
/// >
/// > 1. Unique identifiers MUST be strictly ascending in the
/// > mailbox at all times. If the physical message store is
/// > re-ordered by a non-IMAP agent, this requires that the
/// > unique identifiers in the mailbox be regenerated, since
/// > the former unique identifiers are no longer strictly
/// > ascending as a result of the re-ordering.
/// > 2. If the message store has no mechanism to store unique
/// > identifiers, it must regenerate unique identifiers at
/// > each session, and each session must have a unique
/// > `UIDVALIDITY` value.
/// > 3. If the mailbox is deleted and a new mailbox with the
/// > same name is created at a later date, the server must
/// > either keep track of unique identifiers from the
/// > previous instance of the mailbox, or it must assign a
/// > new `UIDVALIDITY` value to the new instance of the
/// > mailbox. A good `UIDVALIDITY` value to use in this case
/// > is a 32-bit representation of the creation date/time of
/// > the mailbox. It is alright to use a constant such as
/// > 1, but only if it guaranteed that unique identifiers
/// > will never be reused, even in the case of a mailbox
/// > being deleted (or renamed) and a new mailbox by the
/// > same name created at some future time.
/// > 4. The combination of mailbox name, `UIDVALIDITY`, and `UID`
/// > must refer to a single immutable message on that server
/// > forever. In particular, the internal date, [RFC 2822](https://tools.ietf.org/html/rfc2822)
/// > size, envelope, body structure, and message texts
/// > (RFC822, RFC822.HEADER, RFC822.TEXT, and all BODY[...]
/// > fetch data items) must never change. This does not
/// > include message numbers, nor does it include attributes
/// > that can be set by a `STORE` command (e.g., `FLAGS`).
pub type Uid = u32;
/// From section [2.3.1.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.2).
///
/// A relative position from 1 to the number of messages in the mailbox.
/// This position MUST be ordered by ascending unique identifier. As
/// each new message is added, it is assigned a message sequence number
/// that is 1 higher than the number of messages in the mailbox before
/// that new message was added.
///
/// Message sequence numbers can be reassigned during the session. For
/// example, when a message is permanently removed (expunged) from the
/// mailbox, the message sequence number for all subsequent messages is
/// decremented. The number of messages in the mailbox is also
/// decremented. Similarly, a new message can be assigned a message
/// sequence number that was once held by some other message prior to an
/// expunge.
///
/// In addition to accessing messages by relative position in the
/// mailbox, message sequence numbers can be used in mathematical
/// calculations. For example, if an untagged "11 EXISTS" is received,
/// and previously an untagged "8 EXISTS" was received, three new
/// messages have arrived with message sequence numbers of 9, 10, and 11.
/// Another example, if message 287 in a 523 message mailbox has UID
/// 12345, there are exactly 286 messages which have lesser UIDs and 236
/// messages which have greater UIDs.
pub type Seq = u32;
/// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in
/// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags
/// begin with `\` in the IMAP protocol. Certain system flags (`\Deleted` and `\Seen`) have
/// special semantics described elsewhere.
///
/// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which
/// the client can add or remove from the message flags permanently; that is, concurrent and
/// subsequent sessions will see any change in permanent flags. Changes to session flags are valid
/// only in that session.
///
/// > Note: The `\Recent` system flag is a special case of a session flag. `\Recent` can not be
/// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum Flag<'a> {
/// Message has been read
Seen,
/// Message has been answered
Answered,
/// Message is "flagged" for urgent/special attention
Flagged,
/// Message is "deleted" for removal by later EXPUNGE
Deleted,
/// Message has not completed composition (marked as a draft).
Draft,
/// Message is "recently" arrived in this mailbox. This session is the first session to have
/// been notified about this message; if the session is read-write, subsequent sessions will
/// not see `\Recent` set for this message. This flag can not be altered by the client.
///
/// If it is not possible to determine whether or not this session is the first session to be
/// notified about a message, then that message SHOULD be considered recent.
///
/// If multiple connections have the same mailbox selected simultaneously, it is undefined
/// which of these connections will see newly-arrived messages with `\Recent` set and which
/// will see it without `\Recent` set.
Recent,
/// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that
/// it is possible to create new keywords by attempting to store those flags in the mailbox.
MayCreate,
/// A non-standard user- or server-defined flag.
Custom(Cow<'a, str>),
}
impl Flag<'static> {
fn system(s: &str) -> Option<Self> {
match s {
"\\Seen" => Some(Flag::Seen),
"\\Answered" => Some(Flag::Answered),
"\\Flagged" => Some(Flag::Flagged),
"\\Deleted" => Some(Flag::Deleted),
"\\Draft" => Some(Flag::Draft),
"\\Recent" => Some(Flag::Recent),
"\\*" => Some(Flag::MayCreate),
_ => None,
}
}
}
impl<'a> From<String> for Flag<'a> {
fn from(s: String) -> Self {
if let Some(f) = Flag::system(&s) {
f
} else {
Flag::Custom(Cow::Owned(s))
}
}
}
impl<'a> From<&'a str> for Flag<'a> {
fn from(s: &'a str) -> Self {
if let Some(f) = Flag::system(s) {
f
} else {
Flag::Custom(Cow::Borrowed(s))
}
}
}
mod mailbox; mod mailbox;
pub use self::mailbox::Mailbox; pub use self::mailbox::Mailbox;
@ -5,12 +196,11 @@ mod fetch;
pub use self::fetch::Fetch; pub use self::fetch::Fetch;
mod name; mod name;
pub use self::name::Name; pub use self::name::{Name, NameAttribute};
mod capabilities; mod capabilities;
pub use self::capabilities::Capabilities; pub use self::capabilities::Capabilities;
/// re-exported from imap_proto; /// re-exported from imap_proto;
pub use imap_proto::StatusAttribute; pub use imap_proto::StatusAttribute;
@ -22,13 +212,62 @@ pub use imap_proto::StatusAttribute;
/// so the user must take care when interpreting these. /// so the user must take care when interpreting these.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum UnsolicitedResponse { pub enum UnsolicitedResponse {
Status(String, Vec<StatusAttribute>), /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
Status {
/// The mailbox that this status response is for.
mailbox: String,
/// The attributes of this mailbox.
attributes: Vec<StatusAttribute>,
},
/// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
/// indicating the number of messages with the `\Recent` flag set. This response occurs if the
/// size of the mailbox changes (e.g., new messages arrive).
///
/// > Note: It is not guaranteed that the message sequence
/// > numbers of recent messages will be a contiguous range of
/// > the highest n messages in the mailbox (where n is the
/// > value reported by the `RECENT` response). Examples of
/// > situations in which this is not the case are: multiple
/// > clients having the same mailbox open (the first session
/// > to be notified will see it as recent, others will
/// > probably see it as non-recent), and when the mailbox is
/// > re-ordered by a non-IMAP agent.
/// >
/// > The only reliable way to identify recent messages is to
/// > look at message flags to see which have the `\Recent` flag
/// > set, or to do a `SEARCH RECENT`.
Recent(u32), Recent(u32),
/// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
/// reports the number of messages in the mailbox. This response occurs if the size of the
/// mailbox changes (e.g., new messages arrive).
Exists(u32), Exists(u32),
/// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
/// reports that the specified message sequence number has been permanently removed from the
/// mailbox. The message sequence number for each successive message in the mailbox is
/// immediately decremented by 1, and this decrement is reflected in message sequence numbers
/// in subsequent responses (including other untagged `EXPUNGE` responses).
///
/// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
/// necessary to send an `EXISTS` response with the new value.
///
/// As a result of the immediate decrement rule, message sequence numbers that appear in a set
/// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
/// from lower numbers to higher numbers, or from higher numbers to lower numbers. For
/// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
/// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
/// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
/// sequence numbers 9, 8, 7, 6, and 5.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Expunge(u32), Expunge(u32),
} }
/// This type wraps an input stream and a type that was constructed by parsing that input stream,
/// which allows the parsed type to refer to data in the underlying stream instead of copying it.
///
/// Any references given out by a `ZeroCopy` should never be used after the `ZeroCopy` is dropped.
pub struct ZeroCopy<D> { pub struct ZeroCopy<D> {
_owned: Box<[u8]>, _owned: Box<[u8]>,
derived: D, derived: D,
@ -46,7 +285,7 @@ impl<D> ZeroCopy<D> {
/// `&self`. /// `&self`.
/// ///
/// It is *not* safe for the error type `E` to borrow from the passed reference. /// It is *not* safe for the error type `E` to borrow from the passed reference.
pub unsafe fn new<F, E>(owned: Vec<u8>, derive: F) -> Result<Self, E> pub(crate) unsafe fn new<F, E>(owned: Vec<u8>, derive: F) -> Result<Self, E>
where where
F: FnOnce(&'static [u8]) -> Result<D, E>, F: FnOnce(&'static [u8]) -> Result<D, E>,
{ {
@ -68,7 +307,7 @@ impl<D> ZeroCopy<D> {
} }
use super::error::Error; use super::error::Error;
pub type ZeroCopyResult<T> = Result<ZeroCopy<T>, Error>; pub(crate) type ZeroCopyResult<T> = Result<ZeroCopy<T>, Error>;
use std::ops::Deref; use std::ops::Deref;
impl<D> Deref for ZeroCopy<D> { impl<D> Deref for ZeroCopy<D> {

View file

@ -1,21 +1,90 @@
// Note that none of these fields are *actually* 'static. use std::borrow::Cow;
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
/// A name that matches a `LIST` or `LSUB` command.
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct Name { pub struct Name {
pub(crate) attributes: Vec<&'static str>, // Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
pub(crate) attributes: Vec<NameAttribute<'static>>,
pub(crate) delimiter: &'static str, pub(crate) delimiter: &'static str,
pub(crate) name: &'static str, pub(crate) name: &'static str,
} }
/// An attribute set for an IMAP name.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum NameAttribute<'a> {
/// It is not possible for any child levels of hierarchy to exist
/// under this name; no child levels exist now and none can be
/// created in the future.
NoInferiors,
/// It is not possible to use this name as a selectable mailbox.
NoSelect,
/// The mailbox has been marked "interesting" by the server; the
/// mailbox probably contains messages that have been added since
/// the last time the mailbox was selected.
Marked,
/// The mailbox does not contain any additional messages since the
/// last time the mailbox was selected.
Unmarked,
/// A non-standard user- or server-defined name attribute.
Custom(Cow<'a, str>),
}
impl NameAttribute<'static> {
fn system(s: &str) -> Option<Self> {
match s {
"\\Noinferiors" => Some(NameAttribute::NoInferiors),
"\\Noselect" => Some(NameAttribute::NoSelect),
"\\Marked" => Some(NameAttribute::Marked),
"\\Unmarked" => Some(NameAttribute::Unmarked),
_ => None,
}
}
}
impl<'a> From<String> for NameAttribute<'a> {
fn from(s: String) -> Self {
if let Some(f) = NameAttribute::system(&s) {
f
} else {
NameAttribute::Custom(Cow::Owned(s))
}
}
}
impl<'a> From<&'a str> for NameAttribute<'a> {
fn from(s: &'a str) -> Self {
if let Some(f) = NameAttribute::system(s) {
f
} else {
NameAttribute::Custom(Cow::Borrowed(s))
}
}
}
impl Name { impl Name {
pub fn attributes(&self) -> &[&str] { /// Attributes of this name.
pub fn attributes(&self) -> &[NameAttribute] {
&self.attributes[..] &self.attributes[..]
} }
/// The hierarchy delimiter is a character used to delimit levels of hierarchy in a mailbox
/// name. A client can use it to create child mailboxes, and to search higher or lower levels
/// of naming hierarchy. All children of a top-level hierarchy node MUST use the same
/// separator character. A NIL hierarchy delimiter means that no hierarchy exists; the name is
/// a "flat" name.
pub fn delimiter(&self) -> &str { pub fn delimiter(&self) -> &str {
self.delimiter self.delimiter
} }
/// The name represents an unambiguous left-to-right hierarchy, and MUST be valid for use as a
/// reference in `LIST` and `LSUB` commands. Unless [`NameAttribute::NoSelect`] is indicated,
/// the name MUST also be valid as an argument for commands, such as `SELECT`, that accept
/// mailbox names.
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
self.name self.name
} }