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]
name = "imap"
version = "0.8.1"
version = "0.9.0"
authors = ["Matt McCoy <mattnenterprise@yahoo.com>",
"Jon Gjengset <jon@thesquareplanet.com>"]
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)
[![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.
See the `examples/` directory for more examples.
Below is a basic client example. See the `examples/` directory for more.
```rust
extern crate imap;
extern crate native_tls;
// 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
// Look at the `examples/gmail_oauth2.rs` for how to connect to gmail securely.
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();
fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
let domain = "imap.example.com";
let tls = native_tls::TlsConnector::builder().build().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() {
Ok(capabilities) => {
for capability in capabilities.iter() {
println!("{}", capability);
}
}
Err(e) => println!("Error parsing capabilities: {}", e),
// 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_socket.select("INBOX") {
Ok(mailbox) => {
println!("{}", mailbox);
}
Err(e) => println!("Error selecting INBOX: {}", e),
};
// extract the message's body
let body = message.rfc822().expect("message did not have a body!");
let body = std::str::from_utf8(body)
.expect("message was not valid utf-8")
.to_string();
match imap_socket.fetch("2", "body[text]") {
Ok(messages) => {
for message in messages.iter() {
print!("{:?}", message);
}
}
Err(e) => println!("Error Fetching email 2: {}", e),
};
// be nice to the server and log out
imap_session.logout()?;
imap_socket.logout().unwrap();
Ok(Some(body))
}
```
@ -71,5 +74,5 @@ at your option.
## Contribution
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
additional terms or conditions.
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.

View file

@ -22,5 +22,5 @@ at your option.
## Contribution
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
additional terms or conditions.
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.

View file

@ -1,46 +1,47 @@
extern crate imap;
extern crate native_tls;
use native_tls::TlsConnector;
// 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
// Look at the gmail_oauth2.rs example on how to connect to a gmail server securely.
fn main() {
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;
}
};
match imap_session.capabilities() {
Ok(capabilities) => for capability in capabilities.iter() {
println!("{}", capability);
},
Err(e) => println!("Error parsing capability: {}", e),
};
match imap_session.select("INBOX") {
Ok(mailbox) => {
println!("{}", mailbox);
}
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();
// 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
// Look at the gmail_oauth2.rs example on how to connect to a gmail server securely.
fetch_inbox_top().unwrap();
}
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);
};
// extract the message's body
let body = message.rfc822().expect("message did not have a body!");
let body = std::str::from_utf8(body)
.expect("message was not valid utf-8")
.to_string();
// be nice to the server and log out
imap_session.logout()?;
Ok(Some(body))
}

View file

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

View file

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

View file

@ -1,16 +1,16 @@
extern crate base64;
use base64;
use bufstream::BufStream;
use native_tls::{TlsConnector, TlsStream};
use nom;
use std::collections::HashSet;
use std::io::{self, Read, Write};
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::ops::{Deref, DerefMut};
use std::sync::mpsc;
use std::time::Duration;
use super::authenticator::Authenticator;
use super::error::{Error, ParseError, Result, ValidateError};
use super::extensions;
use super::parse::{
parse_authenticate_response, parse_capabilities, parse_fetches, parse_ids, parse_mailbox,
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
/// a succesful login attempt.
///
/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
/// primitives type.
// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
// primitives type.
#[derive(Debug)]
pub struct Session<T: Read + Write> {
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
/// connecting. A succesfull call to [`login`](struct.Client.html#method.login) will return a
/// [`Session`](struct.Session.html) instance, providing the usual IMAP methods.
///
/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
/// primitives type.
// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
// primitives type.
#[derive(Debug)]
pub struct Client<T: Read + Write> {
conn: Connection<T>,
@ -67,9 +65,13 @@ pub struct Client<T: Read + Write> {
/// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful
/// login) use a `Connection` internally for the TCP stream primitives.
#[derive(Debug)]
#[doc(hidden)]
pub struct Connection<T: Read + Write> {
stream: BufStream<T>,
pub(crate) stream: BufStream<T>,
tag: u32,
/// Enable debug mode for this connection so that all client-server interactions are printed to
/// `STDERR`.
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
/// 2177](https://tools.ietf.org/html/rfc2177).
/// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// [`Session`]), use [`Client::login`] or [`Client::authenticate`].
///
/// As long a the handle is active, the mailbox cannot be otherwise accessed.
#[derive(Debug)]
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).
/// Consider using [`connect`] for a secured connection where possible.
/// You can upgrade an insecure client to a secure one using [`Client::secure`].
/// ```rust,no_run
/// # extern crate native_tls;
/// # extern crate imap;
@ -265,14 +119,14 @@ impl<'a> SetReadTimeout for TlsStream<TcpStream> {
/// # use native_tls::TlsConnector;
/// # fn main() {
/// // 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
/// let ssl_connector = TlsConnector::builder().build().unwrap();
/// let ssl_client = client.secure("imap.example.org", &ssl_connector);
/// let tls = TlsConnector::builder().build().unwrap();
/// 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) {
Ok(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
/// [`Session`](struct.Session.html) type, returned from a succesful call to
/// [`Client::login`](struct.Client.html#method.login).
/// ```rust,no_run
/// Connect to a server using a TLS-encrypted connection.
///
/// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// [`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 imap;
/// # 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();
/// let tls = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(("imap.example.org", 993), "imap.example.org", &tls).unwrap();
/// # }
/// ```
pub fn secure_connect<A: ToSocketAddrs>(
pub fn connect<A: ToSocketAddrs>(
addr: A,
domain: &str,
ssl_connector: &TlsConnector,
@ -321,9 +179,9 @@ pub fn secure_connect<A: ToSocketAddrs>(
}
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(
mut self,
domain: &str,
@ -354,7 +212,10 @@ macro_rules! ok_or_unauth_client_err {
}
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> {
Client {
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>(
mut self,
auth_type: &str,
@ -397,9 +345,7 @@ impl<T: Read + Write> Client<T> {
);
let challenge = ok_or_unauth_client_err!(
base64::decode(data.as_str())
.map_err(|_|
Error::Parse(ParseError::Authentication(data))
),
.map_err(|e| Error::Parse(ParseError::Authentication(data, Some(e)))),
self
);
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> {
// not public, just to avoid duplicating the channel creation code
fn new(conn: Connection<T>) -> Self {
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
///
/// 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
/// 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
@ -477,11 +392,15 @@ impl<T: Read + Write> Session<T> {
/// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the
/// `unsolicited_responses` channel of the [`Session`](struct.Session.html).
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)?))
.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> {
self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?))
.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")
}
/// 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>> {
self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query))
.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>> {
self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query))
.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<()> {
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<()> {
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.
/// The MOVE command is defined in [RFC 6851 - "Internet Message Access Protocol (IMAP)
/// - 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<()> {
self.run_command_and_check_ok(&format!(
"MOVE {} {}",
@ -610,9 +598,10 @@ impl<T: Read + Write> Session<T> {
))
}
/// Moves each message in the uid set into the destination mailbox.
/// The UID MOVE command is defined in [RFC 6851 - "Internet Message Access Protocol (IMAP)
/// - MOVE Extension"](https://tools.ietf.org/html/rfc6851#section-3).
/// 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)
/// 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<()> {
self.run_command_and_check_ok(&format!(
"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
/// mailbox changes.
pub fn idle(&mut self) -> Result<IdleHandle<T>> {
IdleHandle::new(self)
pub fn idle(&mut self) -> Result<extensions::idle::Handle<T>> {
extensions::idle::Handle::new(self)
}
/// 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
/// 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))
.and_then(|lines| parse_ids(lines, &mut self.unsolicited_responses_tx))
}
@ -738,13 +727,13 @@ impl<T: Read + Write> Connection<T> {
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();
self.read_response_onto(&mut 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 try_first = !data.is_empty();
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;
let read = self.stream.read_until(LF, into)?;
if read == 0 {
@ -827,7 +816,7 @@ impl<T: Read + Write> Connection<T> {
// Remove CRLF
let len = into.len();
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)
@ -838,12 +827,12 @@ impl<T: Read + Write> Connection<T> {
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(&[CR, LF])?;
self.stream.flush()?;
if self.debug {
print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap());
eprint!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap());
}
Ok(())
}
@ -948,19 +937,19 @@ mod tests {
#[test]
fn authenticate() {
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\
Zm9v\r\n";
let mock_stream = MockStream::new(response);
let client = Client::new(mock_stream);
enum Authenticate { Auth };
enum Authenticate {
Auth,
};
impl Authenticator for Authenticate {
type Response = Vec<u8>;
fn process(&self, challenge: &[u8]) -> Self::Response {
assert!(
challenge == b"bar",
"Invalid authenticate challenge"
);
assert!(challenge == b"bar", "Invalid authenticate challenge");
b"foo".to_vec()
}
}
@ -1102,11 +1091,11 @@ mod tests {
.to_vec();
let expected_mailbox = Mailbox {
flags: vec![
"\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Seen".to_string(),
"\\Draft".to_string(),
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Seen,
Flag::Draft,
],
exists: 1,
recent: 1,
@ -1141,22 +1130,22 @@ mod tests {
.to_vec();
let expected_mailbox = Mailbox {
flags: vec![
"\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Seen".to_string(),
"\\Draft".to_string(),
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Seen,
Flag::Draft,
],
exists: 1,
recent: 1,
unseen: Some(1),
permanent_flags: vec![
"\\*".to_string(),
"\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Draft".to_string(),
"\\Seen".to_string(),
Flag::MayCreate,
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Draft,
Flag::Seen,
],
uid_next: Some(2),
uid_validity: Some(1257842737),
@ -1197,7 +1186,7 @@ mod tests {
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let ids = session.uid_search("Unseen").unwrap();
let ids: HashSet<u32> = ids.iter().cloned().collect();
let ids: HashSet<Uid> = ids.iter().cloned().collect();
assert!(
session.stream.get_ref().written_buf == b"a1 UID SEARCH Unseen\r\n".to_vec(),
"Invalid search command"

View file

@ -1,3 +1,5 @@
//! IMAP error types.
use std::error::Error as StdError;
use std::fmt;
use std::io::Error as IoError;
@ -5,11 +7,13 @@ use std::net::TcpStream;
use std::result;
use std::string::FromUtf8Error;
use base64::DecodeError;
use bufstream::IntoInnerError as BufError;
use imap_proto::Response;
use native_tls::Error as TlsError;
use native_tls::HandshakeError as TlsHandshakeError;
/// A convenience wrapper around `Result` for `imap::Error`.
pub type Result<T> = result::Result<T, Error>;
/// A set of errors that can occur in the IMAP client
@ -27,11 +31,12 @@ pub enum Error {
NoResponse(String),
/// The connection was terminated unexpectedly.
ConnectionLost,
// Error parsing a server response.
/// Error parsing a server response.
Parse(ParseError),
// Error validating input data
/// Command inputs were not valid [IMAP
/// strings](https://tools.ietf.org/html/rfc3501#section-4.3).
Validate(ValidateError),
// Error appending a mail
/// Error appending an e-mail.
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 {
fn from(err: BufError<T>) -> Error {
Error::Io(err.into())
@ -106,14 +117,16 @@ impl StdError for Error {
}
}
/// An error occured while trying to parse a server response.
#[derive(Debug)]
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>),
// An unexpected response was encountered.
/// An unexpected response was encountered.
Unexpected(String),
// Authentication errors.
Authentication(String),
/// The client could not find or decode the server's authentication challenge.
Authentication(String, Option<DecodeError>),
/// The client receive data that was not UTF-8 encoded.
DataNotUtf8(FromUtf8Error),
}
@ -130,19 +143,21 @@ impl StdError for ParseError {
match *self {
ParseError::Invalid(_) => "Unable to parse status 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",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
ParseError::Authentication(_, Some(ref e)) => Some(e),
_ => 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)]
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.
//! See the `examples/` directory for more examples.
//! Below is a basic client example. See the `examples/` directory for more.
//!
//! ```no_run
//! extern crate imap;
//! extern crate native_tls;
//!
//! // 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
//! // Look at the `examples/gmail_oauth2.rs` for how to connect to gmail securely.
//! 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();
//! fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
//! let domain = "imap.example.com";
//! let tls = native_tls::TlsConnector::builder().build().unwrap();
//!
//! let mut imap_session = match client.login("username", "password") {
//! Ok(c) => c,
//! Err((e, _unauth_client)) => {
//! eprintln!("failed to login: {}", e);
//! return;
//! }
//! // 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() {
//! Ok(capabilities) => {
//! for capability in capabilities.iter() {
//! println!("{}", capability);
//! }
//! }
//! Err(e) => println!("Error parsing capabilities: {}", e),
//! };
//! // extract the message's body
//! let body = message.rfc822().expect("message did not have a body!");
//! let body = std::str::from_utf8(body)
//! .expect("message was not valid utf-8")
//! .to_string();
//!
//! match imap_session.select("INBOX") {
//! Ok(mailbox) => {
//! println!("{}", mailbox);
//! }
//! Err(e) => println!("Error selecting INBOX: {}", e),
//! };
//! // be nice to the server and log out
//! imap_session.logout()?;
//!
//! match imap_session.fetch("2", "body[text]") {
//! Ok(messages) => {
//! for message in messages.iter() {
//! print!("{:?}", message);
//! }
//! }
//! Err(e) => println!("Error Fetching email 2: {}", e),
//! };
//!
//! imap_session.logout().unwrap();
//! Ok(Some(body))
//! }
//! ```
#![deny(missing_docs)]
extern crate base64;
extern crate bufstream;
extern crate imap_proto;
extern crate native_tls;
@ -63,13 +62,18 @@ extern crate nom;
extern crate regex;
mod parse;
mod types;
pub mod authenticator;
pub mod client;
pub mod types;
mod authenticator;
pub use authenticator::Authenticator;
mod client;
pub use client::*;
pub mod error;
pub use types::*;
pub mod extensions;
#[cfg(test)]
mod mock_stream;

View file

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

View file

@ -1,12 +1,45 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
use std::borrow::Borrow;
use std::collections::hash_set::Iter;
use std::collections::HashSet;
pub struct Capabilities(pub(crate) HashSet<&'static str>);
use std::borrow::Borrow;
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 {
/// Check if the server has the given capability.
pub fn has<S: ?Sized>(&self, s: &S) -> bool
where
for<'a> &'a str: Borrow<S>,
@ -15,14 +48,17 @@ impl Capabilities {
self.0.contains(s)
}
/// Iterate over all the server's capabilities
pub fn iter(&self) -> Iter<&str> {
self.0.iter()
}
/// Returns how many capabilities the server has.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns true if the server purports to have no capabilities.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

View file

@ -1,28 +1,48 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
use super::{Flag, Seq, Uid};
/// 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)]
pub struct Fetch {
pub message: u32,
pub(crate) flags: Vec<&'static str>,
pub uid: Option<u32>,
/// The ordinal number of this message in its containing mailbox.
pub message: Seq,
/// 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: Option<&'static [u8]>,
pub(crate) body: Option<&'static [u8]>,
}
impl Fetch {
pub fn flags(&self) -> &[&str] {
/// A list of flags that are set for this message.
pub fn flags(&self) -> &[Flag] {
&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]> {
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]> {
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]> {
self.body
}

View file

@ -1,13 +1,39 @@
use super::{Flag, Uid};
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)]
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,
/// 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,
/// 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 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>,
}

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;
pub use self::mailbox::Mailbox;
@ -5,12 +196,11 @@ mod fetch;
pub use self::fetch::Fetch;
mod name;
pub use self::name::Name;
pub use self::name::{Name, NameAttribute};
mod capabilities;
pub use self::capabilities::Capabilities;
/// re-exported from imap_proto;
pub use imap_proto::StatusAttribute;
@ -22,13 +212,62 @@ pub use imap_proto::StatusAttribute;
/// so the user must take care when interpreting these.
#[derive(Debug, PartialEq, Eq)]
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),
/// 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),
/// 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),
}
/// 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> {
_owned: Box<[u8]>,
derived: D,
@ -46,7 +285,7 @@ impl<D> ZeroCopy<D> {
/// `&self`.
///
/// 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
F: FnOnce(&'static [u8]) -> Result<D, E>,
{
@ -68,7 +307,7 @@ impl<D> ZeroCopy<D> {
}
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;
impl<D> Deref for ZeroCopy<D> {

View file

@ -1,21 +1,90 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
use std::borrow::Cow;
/// A name that matches a `LIST` or `LSUB` command.
#[derive(Debug, Eq, PartialEq)]
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) 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 {
pub fn attributes(&self) -> &[&str] {
/// Attributes of this name.
pub fn attributes(&self) -> &[NameAttribute] {
&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 {
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 {
self.name
}