Skip to content

Commit e5b4346

Browse files
jonhoomattnenterprise
authored andcommitted
Add IMAP IDLE support (#27)
* Add IMAP IDLE support This patch adds support for the IMAP IDLE command specified in RFC 2177. It allows clients to block while waiting for changes to a selected mailbox, without having to poll the server. This is especially useful for monitoring a mailbox for new messages. The API is currently somewhat primitive: users can call `Client::idle()` to get an IDLE "handle", which they can then call `wait()` on to block until a change is detected. The implementation also provides `wait_keepalive()` (which tries to avoid connection timeouts) and `wait_timeout()` (which allows the user to specify the maximum time to block). Further down the line, the handle should probably also implement `Iterator` to allow clients to watch new events as they happen. This *could* be implemented today, but given that most other `Client` methods just return unparsed strings at the moment, I didn't feel like that enhancement was a high priority. For now, users will have to manually query the mailbox for what changed. Fixes #26. * Avoid unnecessary as_bytes() * Make wait_keepalive interval configurable * Avoid ?, which requires Rust 1.13
1 parent 1ef94f8 commit e5b4346

File tree

1 file changed

+151
-1
lines changed

1 file changed

+151
-1
lines changed

src/client.rs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::net::{TcpStream, ToSocketAddrs};
22
use openssl::ssl::{SslContext, SslStream};
3-
use std::io::{Read, Write};
3+
use std::io::{self, Read, Write};
4+
use std::time::Duration;
45

56
use super::mailbox::Mailbox;
67
use super::authenticator::Authenticator;
@@ -19,6 +20,149 @@ pub struct Client<T> {
1920
pub debug: bool
2021
}
2122

23+
/// `IdleHandle` allows a client to block waiting for changes to the remote mailbox.
24+
///
25+
/// The handle blocks using the IMAP IDLE command specificed in [RFC
26+
/// 2177](https://tools.ietf.org/html/rfc2177).
27+
///
28+
/// As long a the handle is active, the mailbox cannot be otherwise accessed.
29+
pub struct IdleHandle<'a, T: Read + Write + 'a> {
30+
client: &'a mut Client<T>,
31+
keepalive: Duration,
32+
}
33+
34+
/// Must be implemented for a transport in order for a `Client` using that transport to support
35+
/// operations with timeouts.
36+
///
37+
/// Examples of where this is useful is for `IdleHandle::wait_keepalive` and
38+
/// `IdleHandle::wait_timeout`.
39+
pub trait SetReadTimeout {
40+
/// Set the timeout for subsequent reads to the given one.
41+
///
42+
/// If `timeout` is `None`, the read timeout should be removed.
43+
///
44+
/// See also `std::net::TcpStream::set_read_timeout`.
45+
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()>;
46+
}
47+
48+
impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> {
49+
fn new(client: &'a mut Client<T>) -> Result<Self> {
50+
let mut h = IdleHandle {
51+
client: client,
52+
keepalive: Duration::from_secs(29 * 60),
53+
};
54+
try!(h.init());
55+
Ok(h)
56+
}
57+
58+
fn init(&mut self) -> Result<()> {
59+
// https://tools.ietf.org/html/rfc2177
60+
//
61+
// The IDLE command takes no arguments.
62+
try!(self.client.run_command("IDLE"));
63+
64+
// A tagged response will be sent either
65+
//
66+
// a) if there's an error, or
67+
// b) *after* we send DONE
68+
let tag = format!("{}{} ", TAG_PREFIX, self.client.tag);
69+
let raw_data = try!(self.client.readline());
70+
let line = String::from_utf8(raw_data).unwrap();
71+
if line.starts_with(&tag) {
72+
try!(parse_response(vec![line]));
73+
// We should *only* get a continuation on an error (i.e., it gives BAD or NO).
74+
unreachable!();
75+
} else if !line.starts_with("+") {
76+
return Err(Error::BadResponse(vec![line]));
77+
}
78+
79+
Ok(())
80+
}
81+
82+
fn terminate(&mut self) -> Result<()> {
83+
try!(self.client.write_line(b"DONE"));
84+
let lines = try!(self.client.read_response());
85+
parse_response_ok(lines)
86+
}
87+
88+
/// Block until the selected mailbox changes.
89+
pub fn wait(&mut self) -> Result<()> {
90+
self.client.readline().map(|_| ())
91+
}
92+
93+
/// Cancel the IDLE handle prematurely.
94+
pub fn cancel(self) {
95+
// causes Drop to be called
96+
}
97+
}
98+
99+
impl<'a, T: SetReadTimeout + Read + Write + 'a> IdleHandle<'a, T> {
100+
/// Set the keep-alive interval to use when `wait_keepalive` is called.
101+
///
102+
/// The interval defaults to 29 minutes as dictated by RFC 2177.
103+
pub fn set_keepalive(&mut self, interval: Duration) {
104+
self.keepalive = interval;
105+
}
106+
107+
/// Block until the selected mailbox changes.
108+
///
109+
/// This method differs from `IdleHandle::wait` in that it will periodically refresh the IDLE
110+
/// connection, to prevent the server from timing out our connection. The keepalive interval is
111+
/// set to 29 minutes by default, as dictated by RFC 2177, but can be changed using
112+
/// `set_keepalive`.
113+
///
114+
/// This is the recommended method to use for waiting.
115+
pub fn wait_keepalive(&mut self) -> Result<()> {
116+
// The server MAY consider a client inactive if it has an IDLE command
117+
// running, and if such a server has an inactivity timeout it MAY log
118+
// the client off implicitly at the end of its timeout period. Because
119+
// of that, clients using IDLE are advised to terminate the IDLE and
120+
// re-issue it at least every 29 minutes to avoid being logged off.
121+
// This still allows a client to receive immediate mailbox updates even
122+
// though it need only "poll" at half hour intervals.
123+
try!(self.client.stream.set_read_timeout(Some(self.keepalive)));
124+
match self.wait() {
125+
Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut ||
126+
e.kind() == io::ErrorKind::WouldBlock => {
127+
// we need to refresh the IDLE connection
128+
try!(self.terminate());
129+
try!(self.init());
130+
self.wait_keepalive()
131+
}
132+
r => {
133+
try!(self.client.stream.set_read_timeout(None));
134+
r
135+
}
136+
}
137+
}
138+
139+
/// Block until the selected mailbox changes, or until the given amount of time has expired.
140+
pub fn wait_timeout(&mut self, timeout: Duration) -> Result<()> {
141+
try!(self.client.stream.set_read_timeout(Some(timeout)));
142+
let res = self.wait();
143+
try!(self.client.stream.set_read_timeout(None));
144+
res
145+
}
146+
}
147+
148+
impl<'a, T: Read + Write + 'a> Drop for IdleHandle<'a, T> {
149+
fn drop(&mut self) {
150+
self.terminate().expect("IDLE connection did not terminate cleanly");
151+
}
152+
}
153+
154+
impl<'a> SetReadTimeout for TcpStream {
155+
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
156+
TcpStream::set_read_timeout(self, timeout).map_err(|e| Error::Io(e))
157+
}
158+
}
159+
160+
impl<'a> SetReadTimeout for SslStream<TcpStream> {
161+
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
162+
self.get_ref().set_read_timeout(timeout).map_err(|e| Error::Io(e))
163+
}
164+
}
165+
22166
impl Client<TcpStream> {
23167
/// Creates a new client.
24168
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> {
@@ -230,6 +374,12 @@ impl<T: Read+Write> Client<T> {
230374
self.run_command_and_parse(&format!("STATUS {} {}", mailbox_name, status_data_items))
231375
}
232376

377+
/// Returns a handle that can be used to block until the state of the currently selected
378+
/// mailbox changes.
379+
pub fn idle(&mut self) -> Result<IdleHandle<T>> {
380+
IdleHandle::new(self)
381+
}
382+
233383
/// Runs a command and checks if it returns OK.
234384
pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
235385
let lines = try!(self.run_command_and_read_response(command));

0 commit comments

Comments
 (0)