Merge pull request #186 from mordak/idle-responses
Pass IDLE responses to caller.
This commit is contained in:
commit
382c025513
7 changed files with 463 additions and 209 deletions
|
|
@ -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
84
examples/idle.rs
Normal 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");
|
||||
}
|
||||
12
src/error.rs
12
src/error.rs
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
80
src/parse.rs
80
src/parse.rs
|
|
@ -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,52 +366,13 @@ 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)]
|
||||
|
|
|
|||
116
src/types/mod.rs
116
src/types/mod.rs
|
|
@ -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.
|
||||
|
|
|
|||
210
src/types/unsolicited_response.rs
Normal file
210
src/types/unsolicited_response.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue