A significant documentation upgrade
Fixes #77. Touches on #74. Fixes #70 through documentation (I think?) Fixes #62.
This commit is contained in:
parent
bddfab357f
commit
f83742dc3d
17 changed files with 1112 additions and 492 deletions
|
|
@ -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/"
|
||||||
|
|
|
||||||
79
README.md
79
README.md
|
|
@ -10,54 +10,57 @@
|
||||||
[](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap)
|
[](https://ci.appveyor.com/api/projects/status/github/mattnenterprise/rust-imap)
|
||||||
[](https://coveralls.io/github/mattnenterprise/rust-imap?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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
525
src/client.rs
525
src/client.rs
|
|
@ -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"
|
||||||
|
|
|
||||||
33
src/error.rs
33
src/error.rs
|
|
@ -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
162
src/extensions/idle.rs
Normal 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
2
src/extensions/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
//! Implementations of various IMAP extensions.
|
||||||
|
pub mod idle;
|
||||||
98
src/lib.rs
98
src/lib.rs
|
|
@ -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;
|
||||||
|
|
|
||||||
137
src/parse.rs
137
src/parse.rs
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
251
src/types/mod.rs
251
src/types/mod.rs
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue