Skip to content

Commit 36d0c05

Browse files
committed
Implementing capability caching
1 parent 41c5597 commit 36d0c05

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

src/client.rs

+94-2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ fn validate_sequence_set(
138138
#[derive(Debug)]
139139
pub struct Session<T: Read + Write> {
140140
conn: Connection<T>,
141+
142+
// Capabilities are almost guaranteed to chance if encryption state or authentication state
143+
// changes, so caching them in `Connection` is inappropiate.
144+
capability_cache: Option<Capabilities>,
145+
141146
pub(crate) unsolicited_responses_tx: mpsc::Sender<UnsolicitedResponse>,
142147

143148
/// Server responses that are not related to the current command. See also the note on
@@ -153,6 +158,10 @@ pub struct Session<T: Read + Write> {
153158
#[derive(Debug)]
154159
pub struct Client<T: Read + Write> {
155160
conn: Connection<T>,
161+
162+
// Capabilities are almost guaranteed to chance if encryption state or authentication state
163+
// changes, so caching them in `Connection` is inappropiate.
164+
capability_cache: Option<Capabilities>,
156165
}
157166

158167
/// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful
@@ -333,6 +342,7 @@ impl<T: Read + Write> Client<T> {
333342
debug: false,
334343
greeting_read: false,
335344
},
345+
capability_cache: None,
336346
}
337347
}
338348

@@ -345,6 +355,47 @@ impl<T: Read + Write> Client<T> {
345355
Ok(res)
346356
}
347357

358+
/// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
359+
/// listing of capabilities that the server supports. The server will include "IMAP4rev1" as
360+
/// one of the listed capabilities. See [`Capabilities`] for further details.
361+
///
362+
/// This method will always bypass the local capabilities cache and send a `CAPABILITY` command
363+
/// to the server. The [`Self::capabilities()`] method can be used when returning a cached
364+
/// response is acceptable.
365+
pub fn capabilities_refresh(&mut self) -> Result<&Capabilities> {
366+
let (mut tx, _rx) = mpsc::channel();
367+
let caps = self.run_command_and_read_response("CAPABILITY")
368+
.and_then(|lines| Capabilities::parse(lines, &mut tx))?;
369+
self.capability_cache = Some(caps);
370+
371+
self.capability_cache.as_ref()
372+
// This path will not be hit; if the cache is not populated the above calls will either
373+
// populate it or return with an early error.
374+
.ok_or_else(|| panic!("CAPABILITY call did not populate capability cache!"))
375+
}
376+
377+
/// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
378+
/// listing of capabilities that the server supports. The server will include "IMAP4rev1" as
379+
/// one of the listed capabilities. See [`Capabilities`] for further details.
380+
///
381+
/// This function will not query the server if a set of capabilities was cached, but request
382+
/// and cache capabilities from the server otherwise. The [`Self::capabilities_refresh`] method
383+
/// can be used to refresh the cache by forcing a `CAPABILITY` command to be send.
384+
pub fn capabilities_ref(&mut self) -> Result<&Capabilities> {
385+
let (mut tx, _rx) = mpsc::channel();
386+
if self.capability_cache.is_none() {
387+
let caps = self.run_command_and_read_response("CAPABILITY")
388+
.and_then(|lines| Capabilities::parse(lines, &mut tx))?;
389+
390+
self.capability_cache = Some(caps);
391+
}
392+
393+
self.capability_cache.as_ref()
394+
// This path will not be hit; if the cache is not populated the above `if` will either
395+
// populate it or return with an early error.
396+
.ok_or_else(|| panic!("CAPABILITY call did not populate capability cache!"))
397+
}
398+
348399
/// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is
349400
/// returned; on error the original `Client` instance is returned in addition to the error.
350401
/// This is because `login` takes ownership of `self`, so in order to try again (e.g. after
@@ -502,6 +553,7 @@ impl<T: Read + Write> Session<T> {
502553
let (tx, rx) = mpsc::channel();
503554
Session {
504555
conn,
556+
capability_cache: None,
505557
unsolicited_responses: rx,
506558
unsolicited_responses_tx: tx,
507559
}
@@ -771,12 +823,52 @@ impl<T: Read + Write> Session<T> {
771823
self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox.as_ref())))
772824
}
773825

826+
/// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
827+
/// listing of capabilities that the server supports. The server will include "IMAP4rev1" as
828+
/// one of the listed capabilities. See [`Capabilities`] for further details.
829+
///
830+
/// This method will always bypass the local capabilities cache and send a `CAPABILITY` command
831+
/// to the server. The [`Self::capabilities()`] method can be used when returning a cached
832+
/// response is acceptable.
833+
pub fn capabilities_refresh(&mut self) -> Result<&Capabilities> {
834+
let caps = self.run_command_and_read_response("CAPABILITY")
835+
.and_then(|lines| Capabilities::parse(lines, &mut self.unsolicited_responses_tx))?;
836+
self.capability_cache = Some(caps);
837+
838+
self.capability_cache.as_ref()
839+
// This path will not be hit; if the cache is not populated the above calls will either
840+
// populate it or return with an early error.
841+
.ok_or_else(|| panic!("CAPABILITY call did not populate capability cache!"))
842+
}
843+
844+
/// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
845+
/// listing of capabilities that the server supports. The server will include "IMAP4rev1" as
846+
/// one of the listed capabilities. See [`Capabilities`] for further details.
847+
///
848+
/// This function will not query the server if a set of capabilities was cached, but request
849+
/// and cache capabilities from the server otherwise. The [`Self::capabilities_refresh`] method
850+
/// can be used to refresh the cache by forcing a `CAPABILITY` command to be send.
851+
pub fn capabilities_ref(&mut self) -> Result<&Capabilities> {
852+
if self.capability_cache.is_none() {
853+
let caps = self.run_command_and_read_response("CAPABILITY")
854+
.and_then(|lines| Capabilities::parse(lines, &mut self.unsolicited_responses_tx))?;
855+
856+
self.capability_cache = Some(caps);
857+
}
858+
859+
self.capability_cache.as_ref()
860+
// This path will not be hit; if the cache is not populated the above `if` will either
861+
// populate it or return with an early error.
862+
.ok_or_else(|| panic!("CAPABILITY call did not populate capability cache!"))
863+
}
864+
774865
/// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
775866
/// listing of capabilities that the server supports. The server will include "IMAP4rev1" as
776867
/// one of the listed capabilities. See [`Capabilities`] for further details.
777868
pub fn capabilities(&mut self) -> Result<Capabilities> {
778-
self.run_command_and_read_response("CAPABILITY")
779-
.and_then(|lines| Capabilities::parse(lines, &mut self.unsolicited_responses_tx))
869+
// TODO: This emulated the same behaviour as before, with each call issuing a command.
870+
// It may be sensible to allow hitting the cache here.
871+
self.capabilities_refresh().map(|caps| caps.clone())
780872
}
781873

782874
/// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently

src/types/capabilities.rs

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use imap_proto::{Capability, Response};
55
use ouroboros::self_referencing;
66
use std::collections::hash_set::Iter;
77
use std::collections::HashSet;
8+
use std::fmt;
89
use std::sync::mpsc;
910

1011
const IMAP4REV1_CAPABILITY: &str = "IMAP4rev1";
@@ -43,6 +44,17 @@ pub struct Capabilities {
4344
pub(crate) capabilities: HashSet<Capability<'this>>,
4445
}
4546

47+
impl Clone for Capabilities {
48+
fn clone(&self) -> Self {
49+
// Give _rx a name so it's not immediately dropped. Otherwise any unsolicited responses
50+
// that would be send there will return a SendError instead of the parsed response simply
51+
// being dropped later.
52+
let (mut tx, _rx) = mpsc::channel();
53+
Self::parse(self.borrow_data().clone(), &mut tx)
54+
.expect("failed to parse capabilities from data which was already successfully parse before")
55+
}
56+
}
57+
4658
impl Capabilities {
4759
/// Parse the given input into one or more [`Capabilitity`] responses.
4860
pub(crate) fn parse(
@@ -98,3 +110,13 @@ impl Capabilities {
98110
self.borrow_capabilities().is_empty()
99111
}
100112
}
113+
114+
impl fmt::Debug for Capabilities {
115+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116+
let mut dbg = f.debug_tuple("Capabilities");
117+
for x in self.borrow_capabilities() {
118+
dbg.field(x);
119+
}
120+
dbg.finish()
121+
}
122+
}

0 commit comments

Comments
 (0)