Adopt latest imap_proto and expose error status codes

This commit is contained in:
Jon Gjengset 2021-03-06 14:53:14 -08:00
parent d543993062
commit 9b78550394
9 changed files with 169 additions and 133 deletions

View file

@ -43,3 +43,7 @@ required-features = ["default"]
[[test]] [[test]]
name = "imap_integration" name = "imap_integration"
required-features = ["default"] required-features = ["default"]
[patch.crates-io]
# https://github.com/djc/tokio-imap/pull/115
imap-proto = { git = "https://github.com/jonhoo/tokio-imap.git", branch = "moo" }

View file

@ -1,5 +1,6 @@
use bufstream::BufStream; use bufstream::BufStream;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use imap_proto::Response;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use std::collections::HashSet; use std::collections::HashSet;
@ -10,7 +11,7 @@ use std::str;
use std::sync::mpsc; use std::sync::mpsc;
use super::authenticator::Authenticator; use super::authenticator::Authenticator;
use super::error::{Error, No, ParseError, Result, ValidateError}; use super::error::{Bad, Error, No, ParseError, Result, ValidateError};
use super::extensions; use super::extensions;
use super::parse::*; use super::parse::*;
use super::types::*; use super::types::*;
@ -847,11 +848,11 @@ impl<T: Read + Write> Session<T> {
/// either does not have [`Flag::Deleted`] set or has a [`Uid`] that is not included in the /// either does not have [`Flag::Deleted`] set or has a [`Uid`] that is not included in the
/// specified sequence set, it is not affected. /// specified sequence set, it is not affected.
/// ///
/// This command is particularly useful for disconnected use clients. By using [`uid_expunge`] /// This command is particularly useful for disconnected use clients. By using `uid_expunge`
/// instead of [`expunge`] when resynchronizing with the server, the client can ensure that it /// instead of [`expunge`](Session::expunge) when resynchronizing with the server, the client
/// does not inadvertantly remove any messages that have been marked as [`Flag::Deleted`] by /// can ensure that it does not inadvertantly remove any messages that have been marked as
/// other clients between the time that the client was last connected and the time the client /// [`Flag::Deleted`] by other clients between the time that the client was last connected and
/// resynchronizes. /// the time the client resynchronizes.
/// ///
/// This command requires that the server supports [RFC /// This command requires that the server supports [RFC
/// 4315](https://tools.ietf.org/html/rfc4315) as indicated by the `UIDPLUS` capability (see /// 4315](https://tools.ietf.org/html/rfc4315) as indicated by the `UIDPLUS` capability (see
@ -1391,7 +1392,7 @@ impl<T: Read + Write> Connection<T> {
}; };
let break_with = { let break_with = {
use imap_proto::{Response, Status}; use imap_proto::Status;
let line = &data[line_start..]; let line = &data[line_start..];
match imap_proto::parser::parse_response(line) { match imap_proto::parser::parse_response(line) {
@ -1401,16 +1402,19 @@ impl<T: Read + Write> Connection<T> {
tag, tag,
status, status,
information, information,
code,
.. ..
}, },
)) => { )) => {
assert_eq!(tag.as_bytes(), match_tag.as_bytes()); assert_eq!(tag.as_bytes(), match_tag.as_bytes());
Some(match status { Some(match status {
Status::Bad | Status::No => { Status::Bad | Status::No => Err((
Err((status, information.map(ToString::to_string))) status,
} information.map(|v| v.into_owned()),
code.map(|v| v.into_owned()),
)),
Status::Ok => Ok(()), Status::Ok => Ok(()),
status => Err((status, None)), status => Err((status, None, code.map(|v| v.into_owned()))),
}) })
} }
Ok((..)) => None, Ok((..)) => None,
@ -1418,7 +1422,7 @@ impl<T: Read + Write> Connection<T> {
continue_from = Some(line_start); continue_from = Some(line_start);
None None
} }
_ => Some(Err((Status::Bye, None))), _ => Some(Err((Status::Bye, None, None))),
} }
}; };
@ -1426,16 +1430,19 @@ impl<T: Read + Write> Connection<T> {
Some(Ok(_)) => { Some(Ok(_)) => {
break Ok(line_start); break Ok(line_start);
} }
Some(Err((status, expl))) => { Some(Err((status, expl, code))) => {
use imap_proto::Status; use imap_proto::Status;
match status { match status {
Status::Bad => { Status::Bad => {
break Err(Error::Bad( break Err(Error::Bad(Bad {
expl.unwrap_or_else(|| "no explanation given".to_string()), code,
)); information: expl
.unwrap_or_else(|| "no explanation given".to_string()),
}));
} }
Status::No => { Status::No => {
break Err(Error::No(No { break Err(Error::No(No {
code,
information: expl information: expl
.unwrap_or_else(|| "no explanation given".to_string()), .unwrap_or_else(|| "no explanation given".to_string()),
})); }));
@ -1487,6 +1494,7 @@ mod tests {
use super::super::mock_stream::MockStream; use super::super::mock_stream::MockStream;
use super::*; use super::*;
use imap_proto::types::*; use imap_proto::types::*;
use std::borrow::Cow;
macro_rules! mock_session { macro_rules! mock_session {
($s:expr) => { ($s:expr) => {
@ -1499,7 +1507,8 @@ mod tests {
let response = "a0 OK Logged in.\r\n"; let response = "a0 OK Logged in.\r\n";
let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mock_stream = MockStream::new(response.as_bytes().to_vec());
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
let actual_response = client.read_response().unwrap(); let (mut actual_response, i) = client.read_response().unwrap();
actual_response.truncate(i);
assert_eq!(Vec::<u8>::new(), actual_response); assert_eq!(Vec::<u8>::new(), actual_response);
} }
@ -1589,7 +1598,7 @@ mod tests {
let client = Client::new(mock_stream); let client = Client::new(mock_stream);
enum Authenticate { enum Authenticate {
Auth, Auth,
}; }
impl Authenticator for Authenticate { impl Authenticator for Authenticate {
type Response = Vec<u8>; type Response = Vec<u8>;
fn process(&self, challenge: &[u8]) -> Self::Response { fn process(&self, challenge: &[u8]) -> Self::Response {
@ -1846,9 +1855,9 @@ mod tests {
.to_vec(); .to_vec();
let expected_capabilities = vec![ let expected_capabilities = vec![
Capability::Imap4rev1, Capability::Imap4rev1,
Capability::Atom("STARTTLS"), Capability::Atom(Cow::Borrowed("STARTTLS")),
Capability::Auth("GSSAPI"), Capability::Auth(Cow::Borrowed("GSSAPI")),
Capability::Atom("LOGINDISABLED"), Capability::Atom(Cow::Borrowed("LOGINDISABLED")),
]; ];
let mock_stream = MockStream::new(response); let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream); let mut session = mock_session!(mock_stream);

View file

@ -10,7 +10,7 @@ use std::str::Utf8Error;
use base64::DecodeError; use base64::DecodeError;
use bufstream::IntoInnerError as BufError; use bufstream::IntoInnerError as BufError;
use imap_proto::Response; use imap_proto::{types::ResponseCode, Response};
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
@ -19,11 +19,36 @@ use native_tls::HandshakeError as TlsHandshakeError;
/// A convenience wrapper around `Result` for `imap::Error`. /// A convenience wrapper around `Result` for `imap::Error`.
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
/// A `NO` response from the server, which may contain additional metadata about the error. /// A BAD response from the server, which indicates an error message from the server.
#[derive(Debug)]
#[non_exhaustive]
pub struct Bad {
/// Human-redable message included with the Bad response.
pub information: String,
/// A more specific error status code included with the Bad response.
pub code: Option<ResponseCode<'static>>,
}
impl fmt::Display for Bad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.information)
}
}
/// A NO response from the server, which indicates an operational error message from the server.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub struct No { pub struct No {
/// Human-redable message included with the NO response.
pub information: String, pub information: String,
/// A more specific error status code included with the NO response.
pub code: Option<ResponseCode<'static>>,
}
impl fmt::Display for No {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.information)
}
} }
/// A set of errors that can occur in the IMAP client /// A set of errors that can occur in the IMAP client
@ -39,7 +64,7 @@ pub enum Error {
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
Tls(TlsError), Tls(TlsError),
/// A BAD response from the IMAP server. /// A BAD response from the IMAP server.
Bad(String), Bad(Bad),
/// A NO response from the IMAP server. /// A NO response from the IMAP server.
No(No), No(No),
/// The connection was terminated unexpectedly. /// The connection was terminated unexpectedly.

View file

@ -3,8 +3,9 @@
//! //!
//! Mailboxes or the server as a whole may have zero or more annotations associated with them. An //! Mailboxes or the server as a whole may have zero or more annotations associated with them. An
//! annotation contains a uniquely named entry, which has a value. Annotations can be added to //! annotation contains a uniquely named entry, which has a value. Annotations can be added to
//! mailboxes when a mailbox name is provided as the first argument to [`set_metadata`], or to the //! mailboxes when a mailbox name is provided as the first argument to
//! server as a whole when the first argument is `None`. //! [`set_metadata`](Session::set_metadata), or to the server as a whole when the first argument is
//! `None`.
//! //!
//! For example, a general comment being added to a mailbox may have an entry name of "/comment" //! For example, a general comment being added to a mailbox may have an entry name of "/comment"
//! and a value of "Really useful mailbox". //! and a value of "Really useful mailbox".
@ -13,10 +14,14 @@ use crate::client::*;
use crate::error::{Error, ParseError, Result}; use crate::error::{Error, ParseError, Result};
use crate::parse::handle_unilateral; use crate::parse::handle_unilateral;
use crate::types::*; use crate::types::*;
use imap_proto::types::{MailboxDatum, Metadata, Response}; use imap_proto::types::{MailboxDatum, Metadata, Response, ResponseCode};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::sync::mpsc; use std::sync::mpsc;
// for intra-doc links
#[allow(unused_imports)]
use crate::error::No;
trait CmdListItemFormat { trait CmdListItemFormat {
fn format_as_cmd_list_item(&self) -> String; fn format_as_cmd_list_item(&self) -> String;
} }
@ -162,7 +167,7 @@ impl<T: Read + Write> Session<T> {
entries: &[impl AsRef<str>], entries: &[impl AsRef<str>],
depth: MetadataDepth, depth: MetadataDepth,
maxsize: Option<usize>, maxsize: Option<usize>,
) -> Result<(Vec<Metadata>, Option<usize>)> { ) -> Result<(Vec<Metadata>, Option<u64>)> {
let v: Vec<String> = entries let v: Vec<String> = entries
.iter() .iter()
.map(|e| validate_str(e.as_ref()).unwrap()) .map(|e| validate_str(e.as_ref()).unwrap())
@ -195,8 +200,8 @@ impl<T: Read + Write> Session<T> {
{ {
match code { match code {
None => None, None => None,
// TODO: https://github.com/djc/tokio-imap/issues/113 Some(ResponseCode::MetadataLongEntries(v)) => Some(v),
Some(_) => {} Some(_) => None,
} }
} else { } else {
unreachable!("already parsed as Done by Client::run"); unreachable!("already parsed as Done by Client::run");
@ -213,22 +218,18 @@ impl<T: Read + Write> Session<T> {
/// provided, on the specified existing mailboxes or on the server (if the mailbox argument is /// provided, on the specified existing mailboxes or on the server (if the mailbox argument is
/// `None`). Clients can use `None` for the value of entries it wants to remove. /// `None`). Clients can use `None` for the value of entries it wants to remove.
/// ///
/// If the server is unable to set an annotation because the size of its /// If the server is unable to set an annotation because the size of its value is too large,
/// value is too large, this command will fail with a [`Error::No`]. /// this command will fail with a [`Error::No`] and its [status code](No::code) will be
// TODO: https://github.com/djc/tokio-imap/issues/113 /// [`ResponseCode::MetadataMaxSize`] where the contained value is the maximum octet count that
// with a "[METADATA MAXSIZE NNN]" response code when NNN is the maximum octet count that it is /// the server is willing to accept.
// willing to accept.
/// ///
/// If the server is unable to set a new annotation because the maximum /// If the server is unable to set a new annotation because the maximum number of allowed
/// number of allowed annotations has already been reached, this command will also fail with an /// annotations has already been reached, this command will fail with an [`Error::No`] and its
/// [`Error::No`]. /// [status code](No::code) will be [`ResponseCode::MetadataTooMany`].
// TODO: https://github.com/djc/tokio-imap/issues/113
// with a "[METADATA TOOMANY]" response code.
/// ///
/// If the server is unable to set a new annotation because it does not support private /// If the server is unable to set a new annotation because it does not support private
/// annotations on one of the specified mailboxes, you guess it, you'll get an [`Error::No`]. /// annotations on one of the specified mailboxes, you guess it, you'll get an [`Error::No`] with
// TODO: https://github.com/djc/tokio-imap/issues/113 /// a [status code](No::code) of [`ResponseCode::MetadataNoPrivate`].
// with a "[METADATA NOPRIVATE]" response code.
/// ///
/// When any one annotation fails to be set and [`Error::No`] is returned, the server will not /// When any one annotation fails to be set and [`Error::No`] is returned, the server will not
/// change the values for other annotations specified. /// change the values for other annotations specified.
@ -257,16 +258,16 @@ mod tests {
let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mock_stream = MockStream::new(response.as_bytes().to_vec());
let client = Client::new(mock_stream); let client = Client::new(mock_stream);
let mut session = client.login("testuser", "pass").unwrap(); let mut session = client.login("testuser", "pass").unwrap();
let r = get_metadata( let r = session.get_metadata(
&mut session, None,
"",
&["/shared/vendor/vendor.coi", "/shared/comment"], &["/shared/vendor/vendor.coi", "/shared/comment"],
MetadataDepth::Infinity, MetadataDepth::Infinity,
Option::None, Option::None,
); );
match r { match r {
Ok(v) => { Ok((v, missed)) => {
assert_eq!(missed, None);
assert_eq!(v.len(), 3); assert_eq!(v.len(), 3);
assert_eq!(v[0].entry, "/shared/vendor/vendor.coi/a"); assert_eq!(v[0].entry, "/shared/vendor/vendor.coi/a");
assert_eq!(v[0].value.as_ref().expect("None is not expected"), "AAA"); assert_eq!(v[0].value.as_ref().expect("None is not expected"), "AAA");

View file

@ -108,7 +108,9 @@ pub fn parse_fetches(
use imap_proto::AttributeValue; use imap_proto::AttributeValue;
match attr { match attr {
AttributeValue::Flags(flags) => { AttributeValue::Flags(flags) => {
fetch.flags.extend(flags.iter().cloned().map(Flag::from)); fetch
.flags
.extend(flags.iter().map(|f| Flag::from(f.to_string())));
} }
AttributeValue::Uid(uid) => fetch.uid = Some(*uid), AttributeValue::Uid(uid) => fetch.uid = Some(*uid),
AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz),
@ -399,14 +401,15 @@ pub(crate) fn handle_unilateral<'a>(
mod tests { mod tests {
use super::*; use super::*;
use imap_proto::types::*; use imap_proto::types::*;
use std::borrow::Cow;
#[test] #[test]
fn parse_capability_test() { fn parse_capability_test() {
let expected_capabilities = vec![ let expected_capabilities = vec![
Capability::Imap4rev1, Capability::Imap4rev1,
Capability::Atom("STARTTLS"), Capability::Atom(Cow::Borrowed("STARTTLS")),
Capability::Auth("GSSAPI"), Capability::Auth(Cow::Borrowed("GSSAPI")),
Capability::Atom("LOGINDISABLED"), Capability::Atom(Cow::Borrowed("LOGINDISABLED")),
]; ];
let lines = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; let lines = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n";
let (mut send, recv) = mpsc::channel(); let (mut send, recv) = mpsc::channel();
@ -422,7 +425,10 @@ mod tests {
#[test] #[test]
fn parse_capability_case_insensitive_test() { fn parse_capability_case_insensitive_test() {
// Test that "IMAP4REV1" (instead of "IMAP4rev1") is accepted // Test that "IMAP4REV1" (instead of "IMAP4rev1") is accepted
let expected_capabilities = vec![Capability::Imap4rev1, Capability::Atom("STARTTLS")]; let expected_capabilities = vec![
Capability::Imap4rev1,
Capability::Atom(Cow::Borrowed("STARTTLS")),
];
let lines = b"* CAPABILITY IMAP4REV1 STARTTLS\r\n"; let lines = b"* CAPABILITY IMAP4REV1 STARTTLS\r\n";
let (mut send, recv) = mpsc::channel(); let (mut send, recv) = mpsc::channel();
let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap();
@ -525,9 +531,9 @@ mod tests {
fn parse_capabilities_w_unilateral() { fn parse_capabilities_w_unilateral() {
let expected_capabilities = vec![ let expected_capabilities = vec![
Capability::Imap4rev1, Capability::Imap4rev1,
Capability::Atom("STARTTLS"), Capability::Atom(Cow::Borrowed("STARTTLS")),
Capability::Auth("GSSAPI"), Capability::Auth(Cow::Borrowed("GSSAPI")),
Capability::Atom("LOGINDISABLED"), Capability::Atom(Cow::Borrowed("LOGINDISABLED")),
]; ];
let lines = b"\ let lines = b"\
* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\

View file

@ -51,10 +51,10 @@ impl Capabilities {
if s.len() > AUTH_CAPABILITY_PREFIX.len() { if s.len() > AUTH_CAPABILITY_PREFIX.len() {
let (pre, val) = s.split_at(AUTH_CAPABILITY_PREFIX.len()); let (pre, val) = s.split_at(AUTH_CAPABILITY_PREFIX.len());
if pre.eq_ignore_ascii_case(AUTH_CAPABILITY_PREFIX) { if pre.eq_ignore_ascii_case(AUTH_CAPABILITY_PREFIX) {
return self.has(&Capability::Auth(val)); return self.has(&Capability::Auth(val.into()));
} }
} }
self.has(&Capability::Atom(s)) self.has(&Capability::Atom(s.into()))
} }
/// Iterate over all the server's capabilities /// Iterate over all the server's capabilities

View file

@ -40,36 +40,30 @@ impl Fetch {
/// The bytes that make up the header of this message, if `BODY[HEADER]`, `BODY.PEEK[HEADER]`, /// The bytes that make up the header of this message, if `BODY[HEADER]`, `BODY.PEEK[HEADER]`,
/// or `RFC822.HEADER` was included in the `query` argument to `FETCH`. /// or `RFC822.HEADER` was included in the `query` argument to `FETCH`.
pub fn header(&self) -> Option<&[u8]> { pub fn header(&self) -> Option<&[u8]> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::BodySection {
.filter_map(|av| match av { section: Some(SectionPath::Full(MessageSection::Header)),
AttributeValue::BodySection { data: Some(hdr),
section: Some(SectionPath::Full(MessageSection::Header)), ..
data: Some(hdr), }
.. | AttributeValue::Rfc822Header(Some(hdr)) => Some(&**hdr),
} _ => None,
| AttributeValue::Rfc822Header(Some(hdr)) => Some(*hdr), })
_ => None,
})
.next()
} }
/// The bytes that make up this message, included if `BODY[]` or `RFC822` was included in the /// The bytes that make up this message, included if `BODY[]` or `RFC822` was included in the
/// `query` argument to `FETCH`. The bytes SHOULD be interpreted by the client according to the /// `query` argument to `FETCH`. The bytes SHOULD be interpreted by the client according to the
/// content transfer encoding, body type, and subtype. /// content transfer encoding, body type, and subtype.
pub fn body(&self) -> Option<&[u8]> { pub fn body(&self) -> Option<&[u8]> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::BodySection {
.filter_map(|av| match av { section: None,
AttributeValue::BodySection { data: Some(body),
section: None, ..
data: Some(body), }
.. | AttributeValue::Rfc822(Some(body)) => Some(&**body),
} _ => None,
| AttributeValue::Rfc822(Some(body)) => Some(*body), })
_ => None,
})
.next()
} }
/// The bytes that make up the text of this message, included if `BODY[TEXT]`, `RFC822.TEXT`, /// The bytes that make up the text of this message, included if `BODY[TEXT]`, `RFC822.TEXT`,
@ -77,18 +71,15 @@ impl Fetch {
/// interpreted by the client according to the content transfer encoding, body type, and /// interpreted by the client according to the content transfer encoding, body type, and
/// subtype. /// subtype.
pub fn text(&self) -> Option<&[u8]> { pub fn text(&self) -> Option<&[u8]> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::BodySection {
.filter_map(|av| match av { section: Some(SectionPath::Full(MessageSection::Text)),
AttributeValue::BodySection { data: Some(body),
section: Some(SectionPath::Full(MessageSection::Text)), ..
data: Some(body), }
.. | AttributeValue::Rfc822Text(Some(body)) => Some(&**body),
} _ => None,
| AttributeValue::Rfc822Text(Some(body)) => Some(*body), })
_ => None,
})
.next()
} }
/// The envelope of this message, if `ENVELOPE` was included in the `query` argument to /// The envelope of this message, if `ENVELOPE` was included in the `query` argument to
@ -99,13 +90,10 @@ impl Fetch {
/// The full description of the format of the envelope is given in [RFC 3501 section /// The full description of the format of the envelope is given in [RFC 3501 section
/// 7.4.2](https://tools.ietf.org/html/rfc3501#section-7.4.2). /// 7.4.2](https://tools.ietf.org/html/rfc3501#section-7.4.2).
pub fn envelope(&self) -> Option<&Envelope<'_>> { pub fn envelope(&self) -> Option<&Envelope<'_>> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::Envelope(env) => Some(&**env),
.filter_map(|av| match av { _ => None,
AttributeValue::Envelope(env) => Some(&**env), })
_ => None,
})
.next()
} }
/// Extract the bytes that makes up the given `BOD[<section>]` of a `FETCH` response. /// Extract the bytes that makes up the given `BOD[<section>]` of a `FETCH` response.
@ -113,17 +101,14 @@ impl Fetch {
/// See [section 7.4.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.4.2) for /// See [section 7.4.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.4.2) for
/// details. /// details.
pub fn section(&self, path: &SectionPath) -> Option<&[u8]> { pub fn section(&self, path: &SectionPath) -> Option<&[u8]> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::BodySection {
.filter_map(|av| match av { section: Some(sp),
AttributeValue::BodySection { data: Some(data),
section: Some(sp), ..
data: Some(data), } if sp == path => Some(&**data),
.. _ => None,
} if sp == path => Some(*data), })
_ => None,
})
.next()
} }
/// Extract the `INTERNALDATE` of a `FETCH` response /// Extract the `INTERNALDATE` of a `FETCH` response
@ -133,11 +118,10 @@ impl Fetch {
pub fn internal_date(&self) -> Option<DateTime<FixedOffset>> { pub fn internal_date(&self) -> Option<DateTime<FixedOffset>> {
self.fetch self.fetch
.iter() .iter()
.filter_map(|av| match av { .find_map(|av| match av {
AttributeValue::InternalDate(date_time) => Some(*date_time), AttributeValue::InternalDate(date_time) => Some(&**date_time),
_ => None, _ => None,
}) })
.next()
.and_then( .and_then(
|date_time| match DateTime::parse_from_str(date_time, DATE_TIME_FORMAT) { |date_time| match DateTime::parse_from_str(date_time, DATE_TIME_FORMAT) {
Ok(date_time) => Some(date_time), Ok(date_time) => Some(date_time),
@ -151,12 +135,9 @@ impl Fetch {
/// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for /// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for
/// details. /// details.
pub fn bodystructure<'a>(&self) -> Option<&BodyStructure<'a>> { pub fn bodystructure<'a>(&self) -> Option<&BodyStructure<'a>> {
self.fetch self.fetch.iter().find_map(|av| match av {
.iter() AttributeValue::BodyStructure(bs) => Some(bs),
.filter_map(|av| match av { _ => None,
AttributeValue::BodyStructure(bs) => Some(bs), })
_ => None,
})
.next()
} }
} }

View file

@ -6,8 +6,8 @@ pub struct Name {
// Note that none of these fields are *actually* 'static. // Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`. // Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
pub(crate) attributes: Vec<NameAttribute<'static>>, pub(crate) attributes: Vec<NameAttribute<'static>>,
pub(crate) delimiter: Option<&'static str>, pub(crate) delimiter: Option<Cow<'static, str>>,
pub(crate) name: &'static str, pub(crate) name: Cow<'static, str>,
} }
/// An attribute set for an IMAP name. /// An attribute set for an IMAP name.
@ -56,6 +56,16 @@ impl<'a> From<String> for NameAttribute<'a> {
} }
} }
impl<'a> From<Cow<'a, str>> for NameAttribute<'a> {
fn from(s: Cow<'a, str>) -> Self {
if let Some(f) = NameAttribute::system(&*s) {
f
} else {
NameAttribute::Custom(s)
}
}
}
impl<'a> From<&'a str> for NameAttribute<'a> { impl<'a> From<&'a str> for NameAttribute<'a> {
fn from(s: &'a str) -> Self { fn from(s: &'a str) -> Self {
if let Some(f) = NameAttribute::system(s) { if let Some(f) = NameAttribute::system(s) {
@ -77,7 +87,7 @@ impl Name {
/// of naming hierarchy. All children of a top-level hierarchy node use the same /// of naming hierarchy. All children of a top-level hierarchy node use the same
/// separator character. `None` means that no hierarchy exists; the name is a "flat" name. /// separator character. `None` means that no hierarchy exists; the name is a "flat" name.
pub fn delimiter(&self) -> Option<&str> { pub fn delimiter(&self) -> Option<&str> {
self.delimiter self.delimiter.as_deref()
} }
/// The name represents an unambiguous left-to-right hierarchy, and are valid for use as a /// The name represents an unambiguous left-to-right hierarchy, and are valid for use as a
@ -85,6 +95,6 @@ impl Name {
/// the name is also valid as an argument for commands, such as `SELECT`, that accept mailbox /// the name is also valid as an argument for commands, such as `SELECT`, that accept mailbox
/// names. /// names.
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
self.name &*self.name
} }
} }

View file

@ -141,17 +141,17 @@ fn inbox() {
assert_ne!(fetch.uid, None); assert_ne!(fetch.uid, None);
assert_eq!(fetch.size, Some(138)); assert_eq!(fetch.size, Some(138));
let e = fetch.envelope().unwrap(); let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My first e-mail"[..])); assert_eq!(e.subject, Some(b"My first e-mail"[..].into()));
assert_ne!(e.from, None); assert_ne!(e.from, None);
assert_eq!(e.from.as_ref().unwrap().len(), 1); assert_eq!(e.from.as_ref().unwrap().len(), 1);
let from = &e.from.as_ref().unwrap()[0]; let from = &e.from.as_ref().unwrap()[0];
assert_eq!(from.mailbox, Some(&b"sender"[..])); assert_eq!(from.mailbox, Some(b"sender"[..].into()));
assert_eq!(from.host, Some(&b"localhost"[..])); assert_eq!(from.host, Some(b"localhost"[..].into()));
assert_ne!(e.to, None); assert_ne!(e.to, None);
assert_eq!(e.to.as_ref().unwrap().len(), 1); assert_eq!(e.to.as_ref().unwrap().len(), 1);
let to = &e.to.as_ref().unwrap()[0]; let to = &e.to.as_ref().unwrap()[0];
assert_eq!(to.mailbox, Some(&b"inbox"[..])); assert_eq!(to.mailbox, Some(b"inbox"[..].into()));
assert_eq!(to.host, Some(&b"localhost"[..])); assert_eq!(to.host, Some(b"localhost"[..].into()));
let date_opt = fetch.internal_date(); let date_opt = fetch.internal_date();
assert!(date_opt.is_some()); assert!(date_opt.is_some());
@ -209,7 +209,7 @@ fn inbox_uid() {
let fetch = &fetch[0]; let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid)); assert_eq!(fetch.uid, Some(uid));
let e = fetch.envelope().unwrap(); let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My first e-mail"[..])); assert_eq!(e.subject, Some(b"My first e-mail"[..].into()));
let date_opt = fetch.internal_date(); let date_opt = fetch.internal_date();
assert!(date_opt.is_some()); assert!(date_opt.is_some());
@ -269,7 +269,7 @@ fn append() {
let fetch = &fetch[0]; let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid)); assert_eq!(fetch.uid, Some(uid));
let e = fetch.envelope().unwrap(); let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My second e-mail"[..])); assert_eq!(e.subject, Some(b"My second e-mail"[..].into()));
// and let's delete it to clean up // and let's delete it to clean up
c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)") c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)")
@ -320,7 +320,7 @@ fn append_with_flags() {
let fetch = &fetch[0]; let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid)); assert_eq!(fetch.uid, Some(uid));
let e = fetch.envelope().unwrap(); let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My third e-mail"[..])); assert_eq!(e.subject, Some(b"My third e-mail"[..].into()));
// check the flags // check the flags
let setflags = fetch.flags(); let setflags = fetch.flags();