Merge pull request #186 from mordak/idle-responses

Pass IDLE responses to caller.
This commit is contained in:
Jon Gjengset 2021-04-19 22:30:12 -04:00 committed by GitHub
commit 382c025513
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 463 additions and 209 deletions

View file

@ -21,7 +21,7 @@ default = ["tls"]
native-tls = { version = "0.2.2", optional = true }
regex = "1.0"
bufstream = "0.1"
imap-proto = "0.14.0"
imap-proto = "0.14.1"
nom = { version = "6.0", default-features = false }
base64 = "0.13"
chrono = { version = "0.4", default-features = false }
@ -31,6 +31,7 @@ lazy_static = "1.4"
lettre = "0.9"
lettre_email = "0.9"
rustls-connector = "0.13.0"
structopt = "0.3"
[[example]]
name = "basic"
@ -40,6 +41,10 @@ required-features = ["default"]
name = "gmail_oauth2"
required-features = ["default"]
[[example]]
name = "idle"
required-features = ["default"]
[[test]]
name = "imap_integration"
required-features = ["default"]

84
examples/idle.rs Normal file
View file

@ -0,0 +1,84 @@
use native_tls::TlsConnector;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "idle")]
struct Opt {
// The server name to connect to
#[structopt(short, long)]
server: String,
// The port to use
#[structopt(short, long, default_value = "993")]
port: u16,
// The account username
#[structopt(short, long)]
username: String,
// The account password. In a production system passwords
// would normally be in a config or fetched at runtime from
// a password manager or user prompt and not passed on the
// command line.
#[structopt(short = "w", long)]
password: String,
// The mailbox to IDLE on
#[structopt(short, long, default_value = "INBOX")]
mailbox: String,
#[structopt(
short = "x",
long,
help = "The number of responses to receive before exiting",
default_value = "5"
)]
max_responses: usize,
}
fn main() {
let opt = Opt::from_args();
let ssl_conn = TlsConnector::builder().build().unwrap();
let client = imap::connect((opt.server.clone(), opt.port), opt.server, &ssl_conn)
.expect("Could not connect to imap server");
let mut imap = client
.login(opt.username, opt.password)
.expect("Could not authenticate");
// Turn on debug output so we can see the actual traffic coming
// from the server and how it is handled in our callback.
// This wouldn't be turned on in a production build, but is helpful
// in examples and for debugging.
imap.debug = true;
imap.select(opt.mailbox).expect("Could not select mailbox");
let idle = imap.idle().expect("Could not IDLE");
// Implement a trivial counter that causes the IDLE callback to end the IDLE
// after a fixed number of responses.
//
// A threaded client could use channels or shared data to interact with the
// rest of the program and update mailbox state, decide to exit the IDLE, etc.
let mut num_responses = 0;
let max_responses = opt.max_responses;
let idle_result = idle.wait_keepalive_while(|response| {
num_responses += 1;
println!("IDLE response #{}: {:?}", num_responses, response);
if num_responses >= max_responses {
// Stop IDLE
false
} else {
// Continue IDLE
true
}
});
match idle_result {
Ok(()) => println!("IDLE finished normally"),
Err(e) => println!("IDLE finished with error {:?}", e),
}
imap.logout().expect("Could not log out");
}

View file

@ -76,6 +76,10 @@ pub enum Error {
Validate(ValidateError),
/// Error appending an e-mail.
Append,
/// An unexpected response was received. This could be a response from a command,
/// or an unsolicited response that could not be converted into a local type in
/// [`UnsolicitedResponse`].
Unexpected(Response<'static>),
}
impl From<IoError> for Error {
@ -112,7 +116,7 @@ impl From<TlsError> for Error {
impl<'a> From<Response<'a>> for Error {
fn from(err: Response<'a>) -> Error {
Error::Parse(ParseError::Unexpected(format!("{:?}", err)))
Error::Unexpected(err.into_owned())
}
}
@ -130,6 +134,7 @@ impl fmt::Display for Error {
Error::Bad(ref data) => write!(f, "Bad Response: {}", data),
Error::ConnectionLost => f.write_str("Connection Lost"),
Error::Append => f.write_str("Could not append mail to mailbox"),
Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r),
}
}
}
@ -149,6 +154,7 @@ impl StdError for Error {
Error::No(_) => "No Response",
Error::ConnectionLost => "Connection lost",
Error::Append => "Could not append mail to mailbox",
Error::Unexpected(_) => "Unexpected Response",
}
}
@ -170,8 +176,6 @@ impl StdError for Error {
pub enum ParseError {
/// Indicates an error parsing the status response. Such as OK, NO, and BAD.
Invalid(Vec<u8>),
/// An unexpected response was encountered.
Unexpected(String),
/// The client could not find or decode the server's authentication challenge.
Authentication(String, Option<DecodeError>),
/// The client received data that was not UTF-8 encoded.
@ -182,7 +186,6 @@ impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ParseError::Invalid(_) => f.write_str("Unable to parse status response"),
ParseError::Unexpected(_) => f.write_str("Encountered unexpected parse response"),
ParseError::Authentication(_, _) => {
f.write_str("Unable to parse authentication response")
}
@ -195,7 +198,6 @@ impl StdError for ParseError {
fn description(&self) -> &str {
match *self {
ParseError::Invalid(_) => "Unable to parse status response",
ParseError::Unexpected(_) => "Encountered unexpected parsed response",
ParseError::Authentication(_, _) => "Unable to parse authentication response",
ParseError::DataNotUtf8(_, _) => "Unable to parse data as UTF-8 text",
}

View file

@ -3,6 +3,8 @@
use crate::client::Session;
use crate::error::{Error, Result};
use crate::parse::parse_idle;
use crate::types::UnsolicitedResponse;
#[cfg(feature = "tls")]
use native_tls::TlsStream;
use std::io::{self, Read, Write};
@ -13,13 +15,36 @@ use std::time::Duration;
///
/// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3)
/// specificed in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state
/// changes in some way. While idling does inform the client what changes happened on the server,
/// this implementation will currently just block until _anything_ changes, and then notify the
/// changes in some way.
///
/// Each of the `wait` functions takes a callback function which receives any responses
/// that arrive on the channel while IDLE. The callback function implements whatever
/// logic is needed to handle the IDLE response, and then returns a boolean
/// to continue idling (`true`) or stop (`false`).
/// For users that want the IDLE to exit on any change (the behavior proior to version 3.0),
/// a convenience callback function [`stop_on_any`] is provided.
///
/// ```no_run
/// # use native_tls::TlsConnector;
/// use imap::extensions::idle;
/// let ssl_conn = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(("example.com", 993), "example.com", &ssl_conn)
/// .expect("Could not connect to imap server");
/// let mut imap = client.login("user@example.com", "password")
/// .expect("Could not authenticate");
/// imap.select("INBOX")
/// .expect("Could not select mailbox");
///
/// let idle = imap.idle().expect("Could not IDLE");
///
/// // Exit on any mailbox change
/// let result = idle.wait_keepalive_while(idle::stop_on_any);
/// ```
///
/// Note that 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. [`Handle::wait_keepalive`]
/// re-issue it at least every 29 minutes to avoid being logged off. [`Handle::wait_keepalive_while`]
/// does this. This still allows a client to receive immediate mailbox updates even though it need
/// only "poll" at half hour intervals.
///
@ -40,11 +65,16 @@ pub enum WaitOutcome {
MailboxChanged,
}
/// A convenience function to always cause the IDLE handler to exit on any change.
pub fn stop_on_any(_response: UnsolicitedResponse) -> bool {
false
}
/// 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`.
/// Examples of where this is useful is for `Handle::wait_keepalive_while` and
/// `Handle::wait_timeout_while`.
pub trait SetReadTimeout {
/// Set the timeout for subsequent reads to the given one.
///
@ -99,58 +129,102 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> {
/// 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, reconnect: bool) -> Result<WaitOutcome> {
/// This is necessary so that we can keep using the inner `Session` in `wait_keepalive_while`.
fn wait_inner<F>(&mut self, reconnect: bool, mut callback: F) -> Result<WaitOutcome>
where
F: FnMut(UnsolicitedResponse) -> bool,
{
let mut v = Vec::new();
loop {
let result = match self.session.readline(&mut v).map(|_| ()) {
let result = loop {
match self.session.readline(&mut v) {
Err(Error::Io(ref e))
if e.kind() == io::ErrorKind::TimedOut
|| e.kind() == io::ErrorKind::WouldBlock =>
{
if reconnect {
self.terminate()?;
self.init()?;
return self.wait_inner(reconnect);
break Ok(WaitOutcome::TimedOut);
}
Ok(WaitOutcome::TimedOut)
}
Ok(()) => Ok(WaitOutcome::MailboxChanged),
Err(r) => Err(r),
}?;
Ok(_len) => {
// Handle Dovecot's imap_idle_notify_interval message
if v.eq_ignore_ascii_case(b"* OK Still here\r\n") {
v.clear();
} else {
break Ok(result);
continue;
}
match parse_idle(&v) {
// Something went wrong parsing.
(_rest, Some(Err(r))) => break Err(r),
// Complete response. We expect rest to be empty.
(rest, Some(Ok(response))) => {
if !callback(response) {
break Ok(WaitOutcome::MailboxChanged);
}
// Assert on partial parse in debug builds - we expect
// to always parse all or none of the input buffer.
// On release builds, we still do the right thing.
debug_assert!(
rest.is_empty(),
"Unexpected partial parse: input: {:?}, output: {:?}",
v,
rest,
);
if rest.is_empty() {
v.clear();
} else {
let used = v.len() - rest.len();
v.drain(0..used);
}
}
// Incomplete parse - do nothing and read more.
(_rest, None) => {}
}
}
Err(r) => break Err(r),
};
};
// Reconnect on timeout if needed
match (reconnect, result) {
(true, Ok(WaitOutcome::TimedOut)) => {
self.terminate()?;
self.init()?;
self.wait_inner(reconnect, callback)
}
(_, result) => result,
}
}
/// Block until the selected mailbox changes.
pub fn wait(mut self) -> Result<()> {
self.wait_inner(true).map(|_| ())
/// Block until the given callback returns `false`, or until a response
/// arrives that is not explicitly handled by [`UnsolicitedResponse`].
pub fn wait_while<F>(mut self, callback: F) -> Result<()>
where
F: FnMut(UnsolicitedResponse) -> bool,
{
self.wait_inner(true, callback).map(|_| ())
}
}
impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
/// Set the keep-alive interval to use when `wait_keepalive` is called.
/// Set the keep-alive interval to use when `wait_keepalive_while` 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.
/// Block until the given callback returns `false`, or until a response
/// arrives that is not explicitly handled by [`UnsolicitedResponse`].
///
/// This method differs from [`Handle::wait`] in that it will periodically refresh the IDLE
/// This method differs from [`Handle::wait_while`] 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
/// [`Handle::set_keepalive`].
///
/// This is the recommended method to use for waiting.
pub fn wait_keepalive(self) -> Result<()> {
pub fn wait_keepalive_while<F>(self, callback: F) -> Result<()>
where
F: FnMut(UnsolicitedResponse) -> bool,
{
// 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
@ -159,26 +233,33 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
// 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.timed_wait(keepalive, true).map(|_| ())
self.timed_wait(keepalive, true, callback).map(|_| ())
}
/// Block until the selected mailbox changes, or until the given amount of time has expired.
#[deprecated(note = "use wait_with_timeout instead")]
pub fn wait_timeout(self, timeout: Duration) -> Result<()> {
self.wait_with_timeout(timeout).map(|_| ())
/// Block until the given given amount of time has elapsed, the given callback
/// returns `false`, or until a response arrives that is not explicitly handled
/// by [`UnsolicitedResponse`].
pub fn wait_with_timeout_while<F>(self, timeout: Duration, callback: F) -> Result<WaitOutcome>
where
F: FnMut(UnsolicitedResponse) -> bool,
{
self.timed_wait(timeout, false, callback)
}
/// Block until the selected mailbox changes, or until the given amount of time has expired.
pub fn wait_with_timeout(self, timeout: Duration) -> Result<WaitOutcome> {
self.timed_wait(timeout, false)
}
fn timed_wait(mut self, timeout: Duration, reconnect: bool) -> Result<WaitOutcome> {
fn timed_wait<F>(
mut self,
timeout: Duration,
reconnect: bool,
callback: F,
) -> Result<WaitOutcome>
where
F: FnMut(UnsolicitedResponse) -> bool,
{
self.session
.stream
.get_mut()
.set_read_timeout(Some(timeout))?;
let res = self.wait_inner(reconnect);
let res = self.wait_inner(reconnect, callback);
let _ = self.session.stream.get_mut().set_read_timeout(None).is_ok();
res
}

View file

@ -2,6 +2,7 @@ use imap_proto::{MailboxDatum, Response, ResponseCode};
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::sync::mpsc;
use super::error::{Error, ParseError, Result};
@ -105,12 +106,9 @@ pub fn parse_fetches(
// set some common fields eaglery
for attr in &fetch.fetch {
use imap_proto::AttributeValue;
match attr {
AttributeValue::Flags(flags) => {
fetch
.flags
.extend(flags.iter().map(|f| Flag::from(f.to_string())));
fetch.flags.extend(Flag::from_strs(flags));
}
AttributeValue::Uid(uid) => fetch.uid = Some(*uid),
AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz),
@ -270,9 +268,7 @@ pub fn parse_mailbox(
mailbox.unseen = Some(n);
}
Some(ResponseCode::PermanentFlags(flags)) => {
mailbox
.permanent_flags
.extend(flags.into_iter().map(String::from).map(Flag::from));
mailbox.permanent_flags.extend(Flag::from_strs(flags));
}
_ => {}
}
@ -296,9 +292,7 @@ pub fn parse_mailbox(
mailbox.recent = r;
}
MailboxDatum::Flags(flags) => {
mailbox
.flags
.extend(flags.into_iter().map(String::from).map(Flag::from));
mailbox.flags.extend(Flag::from_strs(flags));
}
_ => {}
}
@ -350,6 +344,21 @@ pub fn parse_ids(
}
}
/// Parse a single unsolicited response from IDLE responses.
pub fn parse_idle(lines: &[u8]) -> (&[u8], Option<Result<UnsolicitedResponse>>) {
match imap_proto::parser::parse_response(lines) {
Ok((rest, response)) => match UnsolicitedResponse::try_from(response) {
Ok(unsolicited) => (rest, Some(Ok(unsolicited))),
Err(res) => (rest, Some(Err(res.into()))),
},
Err(nom::Err::Incomplete(_)) => (lines, None),
Err(_) => (
lines,
Some(Err(Error::Parse(ParseError::Invalid(lines.to_vec())))),
),
}
}
// Check if this is simply a unilateral server response (see Section 7 of RFC 3501).
//
// Returns `None` if the response was handled, `Some(res)` if not.
@ -357,53 +366,14 @@ pub(crate) fn try_handle_unilateral<'a>(
res: Response<'a>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Option<Response<'a>> {
match res {
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
unsolicited
.send(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
.unwrap();
}
Response::MailboxData(MailboxDatum::Recent(n)) => {
unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
}
Response::MailboxData(MailboxDatum::Flags(flags)) => {
unsolicited
.send(UnsolicitedResponse::Flags(
flags
.into_iter()
.map(|s| Flag::from(s.to_string()))
.collect(),
))
.unwrap();
}
Response::MailboxData(MailboxDatum::Exists(n)) => {
unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap();
}
Response::Expunge(n) => {
unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap();
}
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => {
unsolicited
.send(UnsolicitedResponse::Metadata {
mailbox: mailbox.to_string(),
metadata_entries: values.iter().map(|s| s.to_string()).collect(),
})
.unwrap();
}
Response::Vanished { earlier, uids } => {
unsolicited
.send(UnsolicitedResponse::Vanished { earlier, uids })
.unwrap();
}
res => {
return Some(res);
}
}
match UnsolicitedResponse::try_from(res) {
Ok(response) => {
unsolicited.send(response).ok();
None
}
Err(unhandled) => Some(unhandled),
}
}
#[cfg(test)]
mod tests {

View file

@ -168,6 +168,13 @@ impl Flag<'static> {
_ => None,
}
}
/// Helper function to transform Strings into owned Flags
pub fn from_strs<S: ToString>(
v: impl IntoIterator<Item = S>,
) -> impl Iterator<Item = Flag<'static>> {
v.into_iter().map(|s| Flag::from(s.to_string()))
}
}
impl<'a> fmt::Display for Flag<'a> {
@ -220,113 +227,8 @@ pub use self::capabilities::Capabilities;
mod deleted;
pub use self::deleted::Deleted;
/// re-exported from imap_proto;
pub use imap_proto::StatusAttribute;
/// Responses that the server sends that are not related to the current command.
/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
/// to accept any response at any time. These are the ones we've encountered in the wild.
///
/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
/// so the user must take care when interpreting these.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsolicitedResponse {
/// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
Status {
/// The mailbox that this status response is for.
mailbox: String,
/// The attributes of this mailbox.
attributes: Vec<StatusAttribute>,
},
/// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
/// indicating the number of messages with the `\Recent` flag set. This response occurs if the
/// size of the mailbox changes (e.g., new messages arrive).
///
/// > Note: It is not guaranteed that the message sequence
/// > numbers of recent messages will be a contiguous range of
/// > the highest n messages in the mailbox (where n is the
/// > value reported by the `RECENT` response). Examples of
/// > situations in which this is not the case are: multiple
/// > clients having the same mailbox open (the first session
/// > to be notified will see it as recent, others will
/// > probably see it as non-recent), and when the mailbox is
/// > re-ordered by a non-IMAP agent.
/// >
/// > The only reliable way to identify recent messages is to
/// > look at message flags to see which have the `\Recent` flag
/// > set, or to do a `SEARCH RECENT`.
Recent(u32),
/// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
/// reports the number of messages in the mailbox. This response occurs if the size of the
/// mailbox changes (e.g., new messages arrive).
Exists(u32),
/// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
/// reports that the specified message sequence number has been permanently removed from the
/// mailbox. The message sequence number for each successive message in the mailbox is
/// immediately decremented by 1, and this decrement is reflected in message sequence numbers
/// in subsequent responses (including other untagged `EXPUNGE` responses).
///
/// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
/// necessary to send an `EXISTS` response with the new value.
///
/// As a result of the immediate decrement rule, message sequence numbers that appear in a set
/// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
/// from lower numbers to higher numbers, or from higher numbers to lower numbers. For
/// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
/// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
/// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
/// sequence numbers 9, 8, 7, 6, and 5.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Expunge(Seq),
/// An unsolicited METADATA response (https://tools.ietf.org/html/rfc5464#section-4.4.2)
/// that reports a change in a server or mailbox annotation.
Metadata {
/// Mailbox name for which annotations were changed.
mailbox: String,
/// List of annotations that were changed.
metadata_entries: Vec<String>,
},
/// An unsolicited [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10)
/// that reports a sequence-set of `UID`s that have been expunged from the mailbox.
///
/// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever
/// an `EXPUNGE` response can be sent. It can only be sent by the server if the client
/// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162).
///
/// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to
/// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER`
/// tag, which is used to announce removals within an already selected mailbox.
///
/// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a
/// single round trip by including the `(CHANGEDSINCE <MODSEQ> VANISHED)`
/// modifier to the `UID SEARCH` command, as described in
/// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example
/// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH`
/// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s
/// will be present as a `VANISHED` response in the `Session::unsolicited_responses`
/// channel.
Vanished {
/// Whether the `EARLIER` tag was set on the response
earlier: bool,
/// The list of `UID`s which have been removed
uids: Vec<std::ops::RangeInclusive<u32>>,
},
/// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that
/// identifies the flags (at a minimum, the system-defined flags) that are applicable in the
/// mailbox. Flags other than the system flags can also exist, depending on server
/// implementation.
///
/// See [`Flag`] for details.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Flags(Vec<Flag<'static>>),
}
mod unsolicited_response;
pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse};
/// 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.

View file

@ -0,0 +1,210 @@
use std::convert::TryFrom;
use super::{Flag, Seq};
/// re-exported from imap_proto;
pub use imap_proto::AttributeValue;
pub use imap_proto::ResponseCode;
pub use imap_proto::StatusAttribute;
use imap_proto::{MailboxDatum, Response, Status};
/// Responses that the server sends that are not related to the current command.
/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
/// to accept any response at any time.
///
/// Not all possible responses are explicitly enumerated here because in practice only
/// some types of responses are delivered as unsolicited responses. If you encounter an
/// unsolicited response in the wild that is not handled here, please
/// [open an issue](https://github.com/jonhoo/rust-imap/issues) and let us know!
///
/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
/// so the user must take care when interpreting these.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsolicitedResponse {
/// An unsolicited `BYE` response.
///
/// The `BYE` response may have an optional `ResponseCode` that provides additional
/// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.5).
Bye {
/// Optional response code.
code: Option<ResponseCode<'static>>,
/// Information text that may be presented to the user.
information: Option<String>,
},
/// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
/// reports the number of messages in the mailbox. This response occurs if the size of the
/// mailbox changes (e.g., new messages arrive).
Exists(u32),
/// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
/// reports that the specified message sequence number has been permanently removed from the
/// mailbox. The message sequence number for each successive message in the mailbox is
/// immediately decremented by 1, and this decrement is reflected in message sequence numbers
/// in subsequent responses (including other untagged `EXPUNGE` responses).
///
/// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
/// necessary to send an `EXISTS` response with the new value.
///
/// As a result of the immediate decrement rule, message sequence numbers that appear in a set
/// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
/// from lower numbers to higher numbers, or from higher numbers to lower numbers. For
/// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
/// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
/// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
/// sequence numbers 9, 8, 7, 6, and 5.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Expunge(Seq),
/// An unsolicited `FETCH` response.
///
/// The server may unilaterally send `FETCH` responses, as described in
/// [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.4.2).
Fetch {
/// Message identifier.
id: u32,
/// Attribute values for this message.
attributes: Vec<AttributeValue<'static>>,
},
/// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that
/// identifies the flags (at a minimum, the system-defined flags) that are applicable in the
/// mailbox. Flags other than the system flags can also exist, depending on server
/// implementation.
///
/// See [`Flag`] for details.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Flags(Vec<Flag<'static>>),
/// An unsolicited METADATA response (https://tools.ietf.org/html/rfc5464#section-4.4.2)
/// that reports a change in a server or mailbox annotation.
Metadata {
/// Mailbox name for which annotations were changed.
mailbox: String,
/// List of annotations that were changed.
metadata_entries: Vec<String>,
},
/// An unsolicited `OK` response.
///
/// The `OK` response may have an optional `ResponseCode` that provides additional
/// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.1).
Ok {
/// Optional response code.
code: Option<ResponseCode<'static>>,
/// Information text that may be presented to the user.
information: Option<String>,
},
/// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
/// indicating the number of messages with the `\Recent` flag set. This response occurs if the
/// size of the mailbox changes (e.g., new messages arrive).
///
/// > Note: It is not guaranteed that the message sequence
/// > numbers of recent messages will be a contiguous range of
/// > the highest n messages in the mailbox (where n is the
/// > value reported by the `RECENT` response). Examples of
/// > situations in which this is not the case are: multiple
/// > clients having the same mailbox open (the first session
/// > to be notified will see it as recent, others will
/// > probably see it as non-recent), and when the mailbox is
/// > re-ordered by a non-IMAP agent.
/// >
/// > The only reliable way to identify recent messages is to
/// > look at message flags to see which have the `\Recent` flag
/// > set, or to do a `SEARCH RECENT`.
Recent(u32),
/// An unsolicited [`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 [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10)
/// that reports a sequence-set of `UID`s that have been expunged from the mailbox.
///
/// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever
/// an `EXPUNGE` response can be sent. It can only be sent by the server if the client
/// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162).
///
/// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to
/// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER`
/// tag, which is used to announce removals within an already selected mailbox.
///
/// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a
/// single round trip by including the `(CHANGEDSINCE <MODSEQ> VANISHED)`
/// modifier to the `UID SEARCH` command, as described in
/// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example
/// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH`
/// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s
/// will be present as a `VANISHED` response in the `Session::unsolicited_responses`
/// channel.
Vanished {
/// Whether the `EARLIER` tag was set on the response
earlier: bool,
/// The list of `UID`s which have been removed
uids: Vec<std::ops::RangeInclusive<u32>>,
},
}
/// Try to convert from a `imap_proto::Response`.
///
/// Not all `Response` variants are supported - only those which
/// are known or likely to be sent by a server as a unilateral response
/// during normal operations or during an IDLE session are implented.
///
/// If the conversion fails, the input `Reponse` is returned.
impl<'a> TryFrom<Response<'a>> for UnsolicitedResponse {
type Error = Response<'a>;
fn try_from(response: Response<'a>) -> Result<Self, Self::Error> {
match response {
Response::Data {
status: Status::Bye,
code,
information,
} => Ok(UnsolicitedResponse::Bye {
code: code.map(|c| c.into_owned()),
information: information.map(|s| s.to_string()),
}),
Response::Data {
status: Status::Ok,
code,
information,
} => Ok(UnsolicitedResponse::Ok {
code: code.map(|c| c.into_owned()),
information: information.map(|s| s.to_string()),
}),
Response::Expunge(n) => Ok(UnsolicitedResponse::Expunge(n)),
Response::Fetch(id, attributes) => Ok(UnsolicitedResponse::Fetch {
id,
attributes: attributes.into_iter().map(|a| a.into_owned()).collect(),
}),
Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)),
Response::MailboxData(MailboxDatum::Flags(flags)) => {
Ok(UnsolicitedResponse::Flags(Flag::from_strs(flags).collect()))
}
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => {
Ok(UnsolicitedResponse::Metadata {
mailbox: mailbox.to_string(),
metadata_entries: values.iter().map(|s| s.to_string()).collect(),
})
}
Response::MailboxData(MailboxDatum::Recent(n)) => Ok(UnsolicitedResponse::Recent(n)),
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
Ok(UnsolicitedResponse::Status {
mailbox: mailbox.into(),
attributes: status,
})
}
Response::Vanished { earlier, uids } => {
Ok(UnsolicitedResponse::Vanished { earlier, uids })
}
_ => Err(response),
}
}
}