Avoid trying to FETCH an empty set of messages (#177)

Also, apply correct validation to FETCH arguments.
This commit is contained in:
Milo Mirate 2021-01-12 23:30:38 -05:00 committed by GitHub
parent 3386c26711
commit 9b6ff70e3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 89 additions and 20 deletions

View file

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Handle empty-set inputs to `fetch` and `uid_fetch` (#177)
### Removed ### Removed
## [2.4.0] - 2020-12-15 ## [2.4.0] - 2020-12-15

View file

@ -28,15 +28,74 @@ macro_rules! quote {
}; };
} }
trait OptionExt<E> {
fn err(self) -> std::result::Result<(), E>;
}
impl<E> OptionExt<E> for Option<E> {
fn err(self) -> std::result::Result<(), E> {
match self {
Some(e) => Err(e),
None => Ok(()),
}
}
}
/// Convert the input into what [the IMAP
/// grammar](https://tools.ietf.org/html/rfc3501#section-9)
/// calls "quoted", which is reachable from "string" et al.
/// Also ensure it doesn't contain a colliding command-delimiter (newline).
fn validate_str(value: &str) -> Result<String> { fn validate_str(value: &str) -> Result<String> {
let quoted = quote!(value); validate_str_noquote(value)?;
if quoted.find('\n').is_some() { Ok(quote!(value))
return Err(Error::Validate(ValidateError('\n')));
} }
if quoted.find('\r').is_some() {
return Err(Error::Validate(ValidateError('\r'))); /// Ensure the input doesn't contain a command-terminator (newline), but don't quote it like
/// `validate_str`.
/// This is helpful for things like the FETCH attributes, which,
/// per [the IMAP grammar](https://tools.ietf.org/html/rfc3501#section-9) may not be quoted:
///
/// > fetch = "FETCH" SP sequence-set SP ("ALL" / "FULL" / "FAST" /
/// > fetch-att / "(" fetch-att *(SP fetch-att) ")")
/// >
/// > fetch-att = "ENVELOPE" / "FLAGS" / "INTERNALDATE" /
/// > "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] /
/// > "BODY" ["STRUCTURE"] / "UID" /
/// > "BODY" section ["<" number "." nz-number ">"] /
/// > "BODY.PEEK" section ["<" number "." nz-number ">"]
///
/// Note the lack of reference to any of the string-like rules or the quote characters themselves.
fn validate_str_noquote(value: &str) -> Result<&str> {
value
.matches(|c| c == '\n' || c == '\r')
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
} }
Ok(quoted)
/// This ensures the input doesn't contain a command-terminator or any other whitespace
/// while leaving it not-quoted.
/// This is needed because, per [the formal grammer given in RFC
/// 3501](https://tools.ietf.org/html/rfc3501#section-9), a sequence set consists of the following:
///
/// > sequence-set = (seq-number / seq-range) *("," sequence-set)
/// > seq-range = seq-number ":" seq-number
/// > seq-number = nz-number / "*"
/// > nz-number = digit-nz *DIGIT
/// > digit-nz = %x31-39
///
/// Note the lack of reference to SP or any other such whitespace terminals.
/// Per this grammar, in theory we ought to be even more restrictive than "no whitespace".
fn validate_sequence_set(value: &str) -> Result<&str> {
value
.matches(|c: char| c.is_ascii_whitespace())
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
} }
/// 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
@ -543,13 +602,17 @@ impl<T: Read + Write> Session<T> {
S1: AsRef<str>, S1: AsRef<str>,
S2: AsRef<str>, S2: AsRef<str>,
{ {
if sequence_set.as_ref().is_empty() {
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
} else {
self.run_command_and_read_response(&format!( self.run_command_and_read_response(&format!(
"FETCH {} {}", "FETCH {} {}",
sequence_set.as_ref(), validate_sequence_set(sequence_set.as_ref())?,
query.as_ref() validate_str_noquote(query.as_ref())?
)) ))
.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::fetch`], except that all identifiers in `uid_set` are /// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8). /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
@ -558,13 +621,17 @@ impl<T: Read + Write> Session<T> {
S1: AsRef<str>, S1: AsRef<str>,
S2: AsRef<str>, S2: AsRef<str>,
{ {
if uid_set.as_ref().is_empty() {
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
} else {
self.run_command_and_read_response(&format!( self.run_command_and_read_response(&format!(
"UID FETCH {} {}", "UID FETCH {} {}",
uid_set.as_ref(), validate_sequence_set(uid_set.as_ref())?,
query.as_ref() validate_str_noquote(query.as_ref())?
)) ))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
} }
}
/// Noop always succeeds, and it does nothing. /// Noop always succeeds, and it does nothing.
pub fn noop(&mut self) -> Result<()> { pub fn noop(&mut self) -> Result<()> {