use base64::{engine::general_purpose, Engine as _}; use bufstream::BufStream; use chrono::{DateTime, FixedOffset}; use imap_proto::Response; use std::collections::HashSet; use std::io::{Read, Write}; use std::ops::{Deref, DerefMut}; use std::str; use std::sync::mpsc; use super::authenticator::Authenticator; use super::error::{Bad, Bye, Error, No, ParseError, Result, TagMismatch, ValidateError}; use super::extensions; use super::parse::*; use super::types::*; use super::utils::*; #[cfg(doc)] use imap_proto::NameAttribute; static TAG_PREFIX: &str = "a"; const INITIAL_TAG: u32 = 0; const CR: u8 = 0x0d; const LF: u8 = 0x0a; macro_rules! quote { ($x:expr) => { format!("\"{}\"", $x.replace(r"\", r"\\").replace("\"", "\\\"")) }; } trait OptionExt { fn err(self) -> std::result::Result<(), E>; } impl OptionExt for Option { 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). /// /// The arguments `synopsis` and `arg_name` are used to construct the error message of /// [ValidateError] in case validation fails. pub(crate) fn validate_str( synopsis: impl Into, arg_name: impl Into, value: &str, ) -> Result { validate_str_noquote(synopsis, arg_name, value)?; Ok(quote!(value)) } /// 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. /// /// The arguments `synopsis` and `arg_name` are used to construct the error message of /// [ValidateError] in case validation fails. fn validate_str_noquote( synopsis: impl Into, arg_name: impl Into, value: &str, ) -> Result<&str> { value .matches(|c| c == '\n' || c == '\r') .next() .and_then(|s| s.chars().next()) .err() .map_err(|c| { Error::Validate(ValidateError { command_synopsis: synopsis.into(), argument: arg_name.into(), offending_char: c, }) })?; Ok(value) } /// 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( synopsis: impl Into, arg_name: impl Into, value: &str, ) -> Result<&str> { value .matches(|c: char| c.is_ascii_whitespace()) .next() .and_then(|s| s.chars().next()) .err() .map_err(|c| { Error::Validate(ValidateError { command_synopsis: synopsis.into(), argument: arg_name.into(), offending_char: c, }) })?; Ok(value) } /// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from /// a succesful login attempt. /// /// Note that the server *is* allowed to unilaterally send things to the client for messages in /// a selected mailbox whose status has changed. See the note on [unilateral server responses /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). Any such messages are parsed out /// and sent on `Session::unsolicited_responses`. // Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying // primitives type. #[derive(Debug)] pub struct Session { conn: Connection, pub(crate) unsolicited_responses_tx: mpsc::Sender, /// Server responses that are not related to the current command. See also the note on /// [unilateral server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). pub unsolicited_responses: mpsc::Receiver, } /// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first /// connecting. A succesfull call to [`Client::login`] or [`Client::authenticate`] will return a /// [`Session`] instance that provides the usual IMAP methods. // Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying // primitives type. #[derive(Debug)] pub struct Client { conn: Connection, } /// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful /// login) use a `Connection` internally for the TCP stream primitives. #[derive(Debug)] #[doc(hidden)] pub struct Connection { pub(crate) stream: BufStream, tag: u32, /// Enable debug mode for this connection so that all client-server interactions are printed to /// `STDERR`. pub debug: bool, /// Tracks if we have read a greeting. pub greeting_read: bool, } impl Connection { /// Manually increment the current tag. /// /// If writing a command to the server fails, [`Client`] assumes that the command did not reach /// the server, and thus that the next tag that should be sent is still the one used for the /// failed command. However, it could be the case that the command _did_ reach the server /// before failing, and thus a fresh tag needs to be issued instead. /// /// This function can be used to attempt to manually re-synchronize the client's tag tracker in /// such cases. It forcibly increments the client's tag counter such that the next command /// sent to the server will have a tag that is one greater than it otherwise would. pub fn skip_tag(&mut self) { self.tag += 1; } } /// A builder for the append command #[must_use] pub struct AppendCmd<'a, T: Read + Write> { session: &'a mut Session, content: &'a [u8], mailbox: &'a str, flags: Vec>, date: Option>, } impl<'a, T: Read + Write> AppendCmd<'a, T> { /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take /// an optional FLAGS parameter to set the flags on the new message. /// /// > If a flag parenthesized list is specified, the flags SHOULD be set /// > in the resulting message; otherwise, the flag list of the /// > resulting message is set to empty by default. In either case, the /// > Recent flag is also set. /// /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. pub fn flag(&mut self, flag: Flag<'a>) -> &mut Self { self.flags.push(flag); self } /// Set multiple flags at once. pub fn flags(&mut self, flags: impl IntoIterator>) -> &mut Self { self.flags.extend(flags); self } /// Pass a date in order to set the date that the message was originally sent. /// /// > If a date-time is specified, the internal date SHOULD be set in /// > the resulting message; otherwise, the internal date of the /// > resulting message is set to the current date and time by default. pub fn internal_date(&mut self, date: DateTime) -> &mut Self { self.date = Some(date); self } /// Finishes up the command and executes it. /// /// Note: be sure to set flags and optional date before you /// finish the command. pub fn finish(&mut self) -> Result { let flagstr = iter_join(self.flags.iter().filter(|f| **f != Flag::Recent), " "); let datestr = if let Some(date) = self.date { format!(" \"{}\"", date.format("%d-%h-%Y %T %z")) } else { "".to_string() }; self.session.run_command(&format!( "APPEND \"{}\" ({}){} {{{}}}", self.mailbox, flagstr, datestr, self.content.len() ))?; let mut v = Vec::new(); self.session.readline(&mut v)?; if !v.starts_with(b"+") { return Err(Error::Append); } self.session.stream.write_all(self.content)?; self.session.stream.write_all(b"\r\n")?; self.session.stream.flush()?; self.session .read_response() .and_then(|(lines, _)| parse_append(&lines, &mut self.session.unsolicited_responses_tx)) } } // `Deref` instances are so we can make use of the same underlying primitives in `Client` and // `Session` impl Deref for Client { type Target = Connection; fn deref(&self) -> &Connection { &self.conn } } impl DerefMut for Client { fn deref_mut(&mut self) -> &mut Connection { &mut self.conn } } impl Deref for Session { type Target = Connection; fn deref(&self) -> &Connection { &self.conn } } impl DerefMut for Session { fn deref_mut(&mut self) -> &mut Connection { &mut self.conn } } // As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error // is relatively common, it's abstacted away into a macro here. // // Note: 1) using `.map_err(|e| (e, self))` or similar here makes the closure own self, so we can't // do that. // 2) in theory we wouldn't need the second parameter, and could just use the identifier // `self` from the surrounding function, but being explicit here seems a lot cleaner. macro_rules! ok_or_unauth_client_err { ($r:expr, $self:expr) => { match $r { Ok(o) => o, Err(e) => return Err((e, $self)), } }; } impl Client { /// Creates a new client over the given stream. /// /// This method primarily exists for writing tests that mock the underlying transport, /// but can also be used to support IMAP over custom tunnels. If you do not need to do /// that, then it is simpler to use the [`ClientBuilder`](crate::ClientBuilder) to get /// a new client. /// /// For an example, see `examples/timeout.rs` which uses a custom timeout on the /// tcp stream. /// /// **Note:** In case you do need to use `Client::new` instead of the `ClientBuilder` /// you will need to listen for the IMAP protocol server greeting before authenticating: /// /// ```rust,no_run /// # use imap::Client; /// # use std::io; /// # use std::net::TcpStream; /// # {} #[cfg(feature = "native-tls")] /// # fn main() { /// # let server = "imap.example.com"; /// # let username = ""; /// # let password = ""; /// # let tcp = TcpStream::connect((server, 993)).unwrap(); /// # use native_tls::TlsConnector; /// # let ssl_connector = TlsConnector::builder().build().unwrap(); /// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap(); /// let mut client = Client::new(tls); /// client.read_greeting().unwrap(); /// let session = client.login(username, password).unwrap(); /// # } /// ``` pub fn new(stream: T) -> Client { Client { conn: Connection { stream: BufStream::new(stream), tag: INITIAL_TAG, debug: false, greeting_read: false, }, } } /// Yield the underlying connection for this Client. /// /// This consumes `self` since the Client is not much use without /// an underlying transport. pub fn into_inner(self) -> Result { let res = self.conn.stream.into_inner()?; Ok(res) } /// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a /// listing of capabilities that the server supports. The server will include "IMAP4rev1" as /// one of the listed capabilities. See [`Capabilities`] for further details. /// /// This allows reading capabilities before authentication. pub fn capabilities(&mut self) -> Result { // Create a temporary channel as we do not care about out of band responses before login let (mut tx, _rx) = mpsc::channel(); self.run_command_and_read_response("CAPABILITY") .and_then(|lines| Capabilities::parse(lines, &mut tx)) } /// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is /// returned; on error the original `Client` instance is returned in addition to the error. /// This is because `login` takes ownership of `self`, so in order to try again (e.g. after /// prompting the user for credetials), ownership of the original `Client` needs to be /// transferred back to the caller. /// /// ```rust,no_run /// # {} #[cfg(feature = "native-tls")] /// # fn main() { /// let client = imap::ClientBuilder::new("imap.example.org", 993) /// .connect().unwrap(); /// /// match client.login("user", "pass") { /// Ok(s) => { /// // you are successfully authenticated! /// }, /// Err((e, orig_client)) => { /// eprintln!("error logging in: {}", e); /// // prompt user and try again with orig_client here /// return; /// } /// } /// # } /// ``` pub fn login( mut self, username: impl AsRef, password: impl AsRef, ) -> ::std::result::Result, (Error, Client)> { let synopsis = "LOGIN"; let u = ok_or_unauth_client_err!(validate_str(synopsis, "username", username.as_ref()), self); let p = ok_or_unauth_client_err!(validate_str(synopsis, "password", password.as_ref()), self); ok_or_unauth_client_err!( self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)), self ); Ok(Session::new(self.conn)) } /// Authenticate with the server using the given custom `authenticator` to handle the server's /// challenge. /// /// ```no_run /// struct OAuth2 { /// user: String, /// access_token: String, /// } /// /// impl imap::Authenticator for OAuth2 { /// type Response = String; /// fn process(&self, _: &[u8]) -> Self::Response { /// format!( /// "user={}\x01auth=Bearer {}\x01\x01", /// self.user, self.access_token /// ) /// } /// } /// /// # {} #[cfg(feature = "native-tls")] /// fn main() { /// let auth = OAuth2 { /// user: String::from("me@example.com"), /// access_token: String::from(""), /// }; /// let client = imap::ClientBuilder::new("imap.example.com", 993).connect() /// .expect("Could not connect to server"); /// /// match client.authenticate("XOAUTH2", &auth) { /// Ok(session) => { /// // you are successfully authenticated! /// }, /// Err((e, orig_client)) => { /// eprintln!("error authenticating: {}", e); /// // prompt user and try again with orig_client here /// return; /// } /// }; /// } /// ``` pub fn authenticate( mut self, auth_type: impl AsRef, authenticator: &A, ) -> ::std::result::Result, (Error, Client)> { ok_or_unauth_client_err!( self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref())), self ); self.do_auth_handshake(authenticator) } /// This func does the handshake process once the authenticate command is made. fn do_auth_handshake( mut self, authenticator: &A, ) -> ::std::result::Result, (Error, Client)> { // TODO Clean up this code loop { let mut line = Vec::new(); // explicit match blocks neccessary to convert error to tuple and not bind self too // early (see also comment on `login`) ok_or_unauth_client_err!(self.readline(&mut line), self); // ignore server comments if line.starts_with(b"* ") { continue; } // Some servers will only send `+\r\n`. if line.starts_with(b"+ ") || &line == b"+\r\n" { let challenge = if &line == b"+\r\n" { Vec::new() } else { let line_str = ok_or_unauth_client_err!( match str::from_utf8(line.as_slice()) { Ok(line_str) => Ok(line_str), Err(e) => Err(Error::Parse(ParseError::DataNotUtf8(line, e))), }, self ); let data = ok_or_unauth_client_err!(parse_authenticate_response(line_str), self); ok_or_unauth_client_err!( general_purpose::STANDARD_NO_PAD .decode(data) .map_err(|e| Error::Parse(ParseError::Authentication( data.to_string(), Some(e) ))), self ) }; let raw_response = &authenticator.process(&challenge); let auth_response = general_purpose::STANDARD_NO_PAD.encode(raw_response); ok_or_unauth_client_err!( self.write_line(auth_response.into_bytes().as_slice()), self ); } else { ok_or_unauth_client_err!(self.read_response_onto(&mut line), self); return Ok(Session::new(self.conn)); } } } } impl Session { // not public, just to avoid duplicating the channel creation code fn new(conn: Connection) -> Self { let (tx, rx) = mpsc::channel(); Session { conn, unsolicited_responses: rx, unsolicited_responses_tx: tx, } } /// Selects a mailbox /// /// The `SELECT` command selects a mailbox so that messages in the mailbox can be accessed. /// Note that earlier versions of this protocol only required the FLAGS, EXISTS, and RECENT /// untagged data; consequently, client implementations SHOULD implement default behavior for /// missing data as discussed with the individual item. /// /// Only one mailbox can be selected at a time in a connection; simultaneous access to multiple /// mailboxes requires multiple connections. The `SELECT` command automatically deselects any /// currently selected mailbox before attempting the new selection. Consequently, if a mailbox /// is selected and a `SELECT` command that fails is attempted, no mailbox is selected. /// /// Note that the server *is* allowed to unilaterally send things to the client for messages in /// a selected mailbox whose status has changed. See the note on [unilateral server responses /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that if you use /// [`Connection::run_command_and_read_response`], you *may* see additional untagged `RECENT`, /// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the /// `unsolicited_responses` channel of the [`Session`](struct.Session.html). pub fn select(&mut self, mailbox_name: impl AsRef) -> Result { self.run(&format!( "SELECT {}", validate_str("SELECT", "mailbox", mailbox_name.as_ref())? )) .and_then(|(lines, _)| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } /// The `EXAMINE` command is identical to [`Session::select`] and returns the same output; /// however, the selected mailbox is identified as read-only. No changes to the permanent state /// of the mailbox, including per-user state, will happen in a mailbox opened with `examine`; /// in particular, messagess cannot lose [`Flag::Recent`] in an examined mailbox. pub fn examine(&mut self, mailbox_name: impl AsRef) -> Result { self.run(&format!( "EXAMINE {}", validate_str("EXAMINE", "mailbox", mailbox_name.as_ref())? )) .and_then(|(lines, _)| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } /// Fetch retrieves data associated with a set of messages in the mailbox. /// /// Note that the server *is* allowed to unilaterally include `FETCH` responses for other /// messages in the selected mailbox whose status has changed. See the note on [unilateral /// server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). /// /// `query` is a list of "data items" (space-separated in parentheses if `>1`). There are three /// "macro items" which specify commonly-used sets of data items, and can be used instead of /// data items. A macro must be used by itself, and not in conjunction with other macros or /// data items. They are: /// /// - `ALL`: equivalent to: `(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)` /// - `FAST`: equivalent to: `(FLAGS INTERNALDATE RFC822.SIZE)` /// /// The currently defined data items that can be fetched are listed [in the /// RFC](https://tools.ietf.org/html/rfc3501#section-6.4.5), but here are some common ones: /// /// - `FLAGS`: The flags that are set for this message. /// - `INTERNALDATE`: The internal date of the message. /// - `BODY[
]`: /// /// The text of a particular body section. The section specification is a set of zero or /// more part specifiers delimited by periods. A part specifier is either a part number /// (see RFC) or one of the following: `HEADER`, `HEADER.FIELDS`, `HEADER.FIELDS.NOT`, /// `MIME`, and `TEXT`. An empty section specification (i.e., `BODY[]`) refers to the /// entire message, including the header. /// /// The `HEADER`, `HEADER.FIELDS`, and `HEADER.FIELDS.NOT` part specifiers refer to the /// [RFC-2822](https://tools.ietf.org/html/rfc2822) header of the message or of an /// encapsulated [MIME-IMT](https://tools.ietf.org/html/rfc2046) /// MESSAGE/[RFC822](https://tools.ietf.org/html/rfc822) message. `HEADER.FIELDS` and /// `HEADER.FIELDS.NOT` are followed by a list of field-name (as defined in /// [RFC-2822](https://tools.ietf.org/html/rfc2822)) names, and return a subset of the /// header. The subset returned by `HEADER.FIELDS` contains only those header fields with /// a field-name that matches one of the names in the list; similarly, the subset returned /// by `HEADER.FIELDS.NOT` contains only the header fields with a non-matching field-name. /// The field-matching is case-insensitive but otherwise exact. Subsetting does not /// exclude the [RFC-2822](https://tools.ietf.org/html/rfc2822) delimiting blank line /// between the header and the body; the blank line is included in all header fetches, /// except in the case of a message which has no body and no blank line. /// /// The `MIME` part specifier refers to the [MIME-IMB](https://tools.ietf.org/html/rfc2045) /// header for this part. /// /// The `TEXT` part specifier refers to the text body of the message, /// omitting the [RFC-2822](https://tools.ietf.org/html/rfc2822) header. /// /// [`Flag::Seen`] is implicitly set when `BODY` is fetched; if this causes the flags to /// change, they will generally be included as part of the `FETCH` responses. /// - `BODY.PEEK[
]`: An alternate form of `BODY[
]` that does not implicitly /// set [`Flag::Seen`]. /// - `ENVELOPE`: The envelope structure of the message. This is computed by the server by /// parsing the [RFC-2822](https://tools.ietf.org/html/rfc2822) header into the component /// parts, defaulting various fields as necessary. /// - `RFC822`: Functionally equivalent to `BODY[]`. /// - `RFC822.HEADER`: Functionally equivalent to `BODY.PEEK[HEADER]`. /// - `RFC822.SIZE`: The [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message. /// - `UID`: The unique identifier for the message. pub fn fetch( &mut self, sequence_set: impl AsRef, query: impl AsRef, ) -> Result { if sequence_set.as_ref().is_empty() { Fetches::parse(vec![], &mut self.unsolicited_responses_tx) } else { let synopsis = "FETCH"; self.run_command_and_read_response(&format!( "FETCH {} {}", validate_sequence_set(synopsis, "seq", sequence_set.as_ref())?, validate_str_noquote(synopsis, "query", query.as_ref())? )) .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } } /// 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). pub fn uid_fetch( &mut self, uid_set: impl AsRef, query: impl AsRef, ) -> Result { if uid_set.as_ref().is_empty() { Fetches::parse(vec![], &mut self.unsolicited_responses_tx) } else { let synopsis = "UID FETCH"; self.run_command_and_read_response(&format!( "UID FETCH {} {}", validate_sequence_set(synopsis, "seq", uid_set.as_ref())?, validate_str_noquote(synopsis, "query", query.as_ref())? )) .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } } /// Noop always succeeds, and it does nothing. pub fn noop(&mut self) -> Result<()> { self.run_command_and_read_response("NOOP") .and_then(|lines| parse_noop(lines, &mut self.unsolicited_responses_tx)) } /// Logout informs the server that the client is done with the connection. pub fn logout(&mut self) -> Result<()> { // Check for OK or BYE. // According to the RFC: // https://datatracker.ietf.org/doc/html/rfc3501#section-6.1.3 // We should get an untagged BYE and a tagged OK. // Apparently some servers send a tagged BYE (imap.wp.pl #210) // instead, so we just treat it like OK since we are logging out // anyway and this avoids returning an error on logout. match self.run_command_and_check_ok("LOGOUT") { Ok(_) => Ok(()), Err(Error::Bye(_)) => Ok(()), resp => resp, } } /// The [`CREATE` command](https://tools.ietf.org/html/rfc3501#section-6.3.3) creates a mailbox /// with the given name. `Ok` is returned only if a new mailbox with that name has been /// created. It is an error to attempt to create `INBOX` or a mailbox with a name that /// refers to an extant mailbox. Any error in creation will return [`Error::No`]. /// /// If the mailbox name is suffixed with the server's hierarchy separator character (as /// returned from the server by [`Session::list`]), this is a declaration that the client /// intends to create mailbox names under this name in the hierarchy. Servers that do not /// require this declaration will ignore the declaration. In any case, the name created is /// without the trailing hierarchy delimiter. /// /// If the server's hierarchy separator character appears elsewhere in the name, the server /// will generally create any superior hierarchical names that are needed for the `CREATE` /// command to be successfully completed. In other words, an attempt to create `foo/bar/zap` /// on a server in which `/` is the hierarchy separator character will usually create `foo/` /// and `foo/bar/` if they do not already exist. /// /// If a new mailbox is created with the same name as a mailbox which was deleted, its unique /// identifiers will be greater than any unique identifiers used in the previous incarnation of /// the mailbox UNLESS the new incarnation has a different unique identifier validity value. /// See the description of the [`UID` /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail. pub fn create(&mut self, mailbox_name: impl AsRef) -> Result<()> { self.run_command_and_check_ok(&format!( "CREATE {}", validate_str("CREATE", "mailbox", mailbox_name.as_ref())? )) } /// The [`DELETE` command](https://tools.ietf.org/html/rfc3501#section-6.3.4) permanently /// removes the mailbox with the given name. `Ok` is returned only if the mailbox has been /// deleted. It is an error to attempt to delete `INBOX` or a mailbox name that does not /// exist. /// /// The `DELETE` command will not remove inferior hierarchical names. For example, if a mailbox /// `foo` has an inferior `foo.bar` (assuming `.` is the hierarchy delimiter character), /// removing `foo` will not remove `foo.bar`. It is an error to attempt to delete a name that /// has inferior hierarchical names and also has [`NameAttribute::NoSelect`]. /// /// It is permitted to delete a name that has inferior hierarchical names and does not have /// [`NameAttribute::NoSelect`]. In this case, all messages in that mailbox are removed, and /// the name will acquire [`NameAttribute::NoSelect`]. /// /// The value of the highest-used unique identifier of the deleted mailbox will be preserved so /// that a new mailbox created with the same name will not reuse the identifiers of the former /// incarnation, UNLESS the new incarnation has a different unique identifier validity value. /// See the description of the [`UID` /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail. pub fn delete(&mut self, mailbox_name: impl AsRef) -> Result<()> { self.run_command_and_check_ok(&format!( "DELETE {}", validate_str("DELETE", "mailbox", mailbox_name.as_ref())? )) } /// The [`RENAME` command](https://tools.ietf.org/html/rfc3501#section-6.3.5) changes the name /// of a mailbox. `Ok` is returned only if the mailbox has been renamed. It is an error to /// attempt to rename from a mailbox name that does not exist or to a mailbox name that already /// exists. Any error in renaming will return [`Error::No`]. /// /// If the name has inferior hierarchical names, then the inferior hierarchical names will also /// be renamed. For example, a rename of `foo` to `zap` will rename `foo/bar` (assuming `/` is /// the hierarchy delimiter character) to `zap/bar`. /// /// If the server's hierarchy separator character appears in the name, the server will /// generally create any superior hierarchical names that are needed for the `RENAME` command /// to complete successfully. In other words, an attempt to rename `foo/bar/zap` to /// `baz/rag/zowie` on a server in which `/` is the hierarchy separator character will /// generally create `baz/` and `baz/rag/` if they do not already exist. /// /// The value of the highest-used unique identifier of the old mailbox name will be preserved /// so that a new mailbox created with the same name will not reuse the identifiers of the /// former incarnation, UNLESS the new incarnation has a different unique identifier validity /// value. See the description of the [`UID` /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail. /// /// Renaming `INBOX` is permitted, and has special behavior. It moves all messages in `INBOX` /// to a new mailbox with the given name, leaving `INBOX` empty. If the server implementation /// supports inferior hierarchical names of `INBOX`, these are unaffected by a rename of /// `INBOX`. pub fn rename(&mut self, from: impl AsRef, to: impl AsRef) -> Result<()> { self.run_command_and_check_ok(&format!( "RENAME {} {}", quote!(from.as_ref()), quote!(to.as_ref()) )) } /// The [`SUBSCRIBE` command](https://tools.ietf.org/html/rfc3501#section-6.3.6) adds the /// specified mailbox name to the server's set of "active" or "subscribed" mailboxes as /// returned by [`Session::lsub`]. This command returns `Ok` only if the subscription is /// successful. /// /// The server may validate the mailbox argument to `SUBSCRIBE` to verify that it exists. /// However, it will not unilaterally remove an existing mailbox name from the subscription /// list even if a mailbox by that name no longer exists. pub fn subscribe(&mut self, mailbox: impl AsRef) -> Result<()> { self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox.as_ref()))) } /// The [`UNSUBSCRIBE` command](https://tools.ietf.org/html/rfc3501#section-6.3.7) removes the /// specified mailbox name from the server's set of "active" or "subscribed" mailboxes as /// returned by [`Session::lsub`]. This command returns `Ok` only if the unsubscription is /// successful. pub fn unsubscribe(&mut self, mailbox: impl AsRef) -> Result<()> { self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox.as_ref()))) } /// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a /// listing of capabilities that the server supports. The server will include "IMAP4rev1" as /// one of the listed capabilities. See [`Capabilities`] for further details. pub fn capabilities(&mut self) -> Result { self.run_command_and_read_response("CAPABILITY") .and_then(|lines| Capabilities::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox. /// The message sequence number of each message that is removed is returned. pub fn expunge(&mut self) -> Result { self.run_command("EXPUNGE")?; self.read_response() .and_then(|(lines, _)| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } /// The [`UID EXPUNGE` command](https://tools.ietf.org/html/rfc4315#section-2.1) permanently /// removes all messages that both have [`Flag::Deleted`] set and have a [`Uid`] that is /// included in the specified sequence set from the currently selected mailbox. If a message /// either does not have [`Flag::Deleted`] set or has a [`Uid`] that is not included in the /// specified sequence set, it is not affected. /// /// This command is particularly useful for disconnected use clients. By using `uid_expunge` /// instead of [`expunge`](Session::expunge) when resynchronizing with the server, the client /// can ensure that it does not inadvertantly remove any messages that have been marked as /// [`Flag::Deleted`] by other clients between the time that the client was last connected and /// the time the client resynchronizes. /// /// This command requires that the server supports [RFC /// 4315](https://tools.ietf.org/html/rfc4315) as indicated by the `UIDPLUS` capability (see /// [`Session::capabilities`]). If the server does not support the `UIDPLUS` capability, the /// client should fall back to using [`Session::store`] to temporarily remove [`Flag::Deleted`] /// from messages it does not want to remove, then invoking [`Session::expunge`]. Finally, the /// client should use [`Session::store`] to restore [`Flag::Deleted`] on the messages in which /// it was temporarily removed. /// /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the /// unintended removal of some messages. pub fn uid_expunge(&mut self, uid_set: impl AsRef) -> Result { self.run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))?; self.read_response() .and_then(|(lines, _)| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } /// The [`CHECK` command](https://tools.ietf.org/html/rfc3501#section-6.4.1) requests a /// checkpoint of the currently selected mailbox. A checkpoint refers to any /// implementation-dependent housekeeping associated with the mailbox (e.g., resolving the /// server's in-memory state of the mailbox with the state on its disk) that is not normally /// executed as part of each command. A checkpoint MAY take a non-instantaneous amount of real /// time to complete. If a server implementation has no such housekeeping considerations, /// [`Session::check`] is equivalent to [`Session::noop`]. /// /// There is no guarantee that an `EXISTS` untagged response will happen as a result of /// `CHECK`. [`Session::noop`] SHOULD be used for new message polling. pub fn check(&mut self) -> Result<()> { self.run_command_and_check_ok("CHECK") } /// The [`CLOSE` command](https://tools.ietf.org/html/rfc3501#section-6.4.2) permanently /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox, /// and returns to the authenticated state from the selected state. No `EXPUNGE` responses are /// sent. /// /// No messages are removed, and no error is given, if the mailbox is selected by /// [`Session::examine`] or is otherwise selected read-only. /// /// Even if a mailbox is selected, [`Session::select`], [`Session::examine`], or /// [`Session::logout`] command MAY be issued without previously invoking [`Session::close`]. /// [`Session::select`], [`Session::examine`], and [`Session::logout`] implicitly close the /// currently selected mailbox without doing an expunge. However, when many messages are /// deleted, a `CLOSE-LOGOUT` or `CLOSE-SELECT` sequence is considerably faster than an /// `EXPUNGE-LOGOUT` or `EXPUNGE-SELECT` because no `EXPUNGE` responses (which the client would /// probably ignore) are sent. pub fn close(&mut self) -> Result<()> { self.run_command_and_check_ok("CLOSE") } /// The [`STORE` command](https://tools.ietf.org/html/rfc3501#section-6.4.6) alters data /// associated with a message in the mailbox. Normally, `STORE` will return the updated value /// of the data with an untagged FETCH response. A suffix of `.SILENT` in `query` prevents the /// untagged `FETCH`, and the server assumes that the client has determined the updated value /// itself or does not care about the updated value. /// /// The currently defined data items that can be stored are: /// /// - `FLAGS `: /// /// Replace the flags for the message (other than [`Flag::Recent`]) with the argument. The /// new value of the flags is returned as if a `FETCH` of those flags was done. /// /// - `FLAGS.SILENT `: Equivalent to `FLAGS`, but without returning a new value. /// /// - `+FLAGS ` /// /// Add the argument to the flags for the message. The new value of the flags is returned /// as if a `FETCH` of those flags was done. /// - `+FLAGS.SILENT `: Equivalent to `+FLAGS`, but without returning a new value. /// /// - `-FLAGS ` /// /// Remove the argument from the flags for the message. The new value of the flags is /// returned as if a `FETCH` of those flags was done. /// /// - `-FLAGS.SILENT `: Equivalent to `-FLAGS`, but without returning a new value. /// /// In all cases, `` is a space-separated list enclosed in parentheses. /// /// # Examples /// /// Delete a message: /// /// ```rust,no_run /// # extern crate imap; /// # use imap::{self, Session}; /// # use std::net::TcpStream; /// fn delete(seq: imap::types::Seq, s: &mut Session) -> imap::error::Result<()> { /// s.store(format!("{}", seq), "+FLAGS (\\Deleted)")?; /// s.expunge()?; /// Ok(()) /// } /// ``` pub fn store( &mut self, sequence_set: impl AsRef, query: impl AsRef, ) -> Result { self.run_command_and_read_response(&format!( "STORE {} {}", sequence_set.as_ref(), query.as_ref() )) .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } /// Equivalent to [`Session::store`], except that all identifiers in `sequence_set` are /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8). pub fn uid_store( &mut self, uid_set: impl AsRef, query: impl AsRef, ) -> Result { self.run_command_and_read_response(&format!( "UID STORE {} {}", uid_set.as_ref(), query.as_ref() )) .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`COPY` command](https://tools.ietf.org/html/rfc3501#section-6.4.7) copies the /// specified message(s) to the end of the specified destination mailbox. The flags and /// internal date of the message(s) will generally be preserved, and [`Flag::Recent`] will /// generally be set, in the copy. /// /// If the `COPY` command is unsuccessful for any reason, the server restores the destination /// mailbox to its state before the `COPY` attempt. pub fn copy( &mut self, sequence_set: impl AsRef, mailbox_name: impl AsRef, ) -> Result<()> { self.run_command_and_check_ok(&format!( "COPY {} {}", sequence_set.as_ref(), mailbox_name.as_ref() )) } /// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8). pub fn uid_copy( &mut self, uid_set: impl AsRef, mailbox_name: impl AsRef, ) -> Result<()> { self.run_command_and_check_ok(&format!( "UID COPY {} {}", uid_set.as_ref(), mailbox_name.as_ref() )) } /// The [`MOVE` command](https://tools.ietf.org/html/rfc6851#section-3.1) takes two /// arguments: a sequence set and a named mailbox. Each message included in the set is moved, /// rather than copied, from the selected (source) mailbox to the named (target) mailbox. /// /// This means that a new message is created in the target mailbox with a /// new [`Uid`], the original message is removed from the source mailbox, and /// it appears to the client as a single action. This has the same /// effect for each message as this sequence: /// /// 1. COPY /// 2. STORE +FLAGS.SILENT \DELETED /// 3. EXPUNGE /// /// This command requires that the server supports [RFC /// 6851](https://tools.ietf.org/html/rfc6851) as indicated by the `MOVE` capability (see /// [`Session::capabilities`]). /// /// Although the effect of the `MOVE` is the same as the preceding steps, the semantics are not /// identical: The intermediate states produced by those steps do not occur, and the response /// codes are different. In particular, though the `COPY` and `EXPUNGE` response codes will be /// returned, response codes for a `store` will not be generated and [`Flag::Deleted`] will not /// be set for any message. /// /// Because a `MOVE` applies to a set of messages, it might fail partway through the set. /// Regardless of whether the command is successful in moving the entire set, each individual /// message will either be moved or unaffected. The server will leave each message in a state /// where it is in at least one of the source or target mailboxes (no message can be lost or /// orphaned). The server will generally not leave any message in both mailboxes (it would be /// bad for a partial failure to result in a bunch of duplicate messages). This is true even /// if the server returns with [`Error::No`]. pub fn mv( &mut self, sequence_set: impl AsRef, mailbox_name: impl AsRef, ) -> Result<()> { self.run_command_and_check_ok(&format!( "MOVE {} {}", sequence_set.as_ref(), validate_str("MOVE", "mailbox", mailbox_name.as_ref())? )) } /// Equivalent to [`Session::mv`], except that all identifiers in `sequence_set` are /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8) /// and the [semantics of `MOVE` and `UID /// MOVE`](https://tools.ietf.org/html/rfc6851#section-3.3). pub fn uid_mv( &mut self, uid_set: impl AsRef, mailbox_name: impl AsRef, ) -> Result<()> { self.run_command_and_check_ok(&format!( "UID MOVE {} {}", uid_set.as_ref(), validate_str("UID MOVE", "mailbox", mailbox_name.as_ref())? )) } /// The [`LIST` command](https://tools.ietf.org/html/rfc3501#section-6.3.8) returns a subset of /// names from the complete set of all names available to the client. It returns the name /// attributes, hierarchy delimiter, and name of each such name; see [`Name`] for more detail. /// /// If `reference_name` is `None` (or `""`), the currently selected mailbox is used. /// The returned mailbox names must match the supplied `mailbox_pattern`. A non-empty /// reference name argument is the name of a mailbox or a level of mailbox hierarchy, and /// indicates the context in which the mailbox name is interpreted. /// /// If `mailbox_pattern` is `None` (or `""`), it is a special request to return the hierarchy /// delimiter and the root name of the name given in the reference. The value returned as the /// root MAY be the empty string if the reference is non-rooted or is an empty string. In all /// cases, a hierarchy delimiter (or `NIL` if there is no hierarchy) is returned. This permits /// a client to get the hierarchy delimiter (or find out that the mailbox names are flat) even /// when no mailboxes by that name currently exist. /// /// The reference and mailbox name arguments are interpreted into a canonical form that /// represents an unambiguous left-to-right hierarchy. The returned mailbox names will be in /// the interpreted form. /// /// The character `*` is a wildcard, and matches zero or more characters at this position. The /// character `%` is similar to `*`, but it does not match a hierarchy delimiter. If the `%` /// wildcard is the last character of a mailbox name argument, matching levels of hierarchy are /// also returned. If these levels of hierarchy are not also selectable mailboxes, they are /// returned with [`NameAttribute::NoSelect`]. /// /// The special name `INBOX` is included if `INBOX` is supported by this server for this user /// and if the uppercase string `INBOX` matches the interpreted reference and mailbox name /// arguments with wildcards. The criteria for omitting `INBOX` is whether `SELECT INBOX` will /// return failure; it is not relevant whether the user's real `INBOX` resides on this or some /// other server. pub fn list( &mut self, reference_name: Option<&str>, mailbox_pattern: Option<&str>, ) -> Result { self.run_command_and_read_response(&format!( "LIST {} {}", quote!(reference_name.unwrap_or("")), mailbox_pattern.unwrap_or("\"\"") )) .and_then(|lines| Names::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`LSUB` command](https://tools.ietf.org/html/rfc3501#section-6.3.9) returns a subset of /// names from the set of names that the user has declared as being "active" or "subscribed". /// The arguments to this method the same as for [`Session::list`]. /// /// The returned [`Name`]s MAY contain different mailbox flags from response to /// [`Session::list`]. If this should happen, the flags returned by [`Session::list`] are /// considered more authoritative. /// /// A special situation occurs when invoking `lsub` with the `%` wildcard. Consider what /// happens if `foo/bar` (with a hierarchy delimiter of `/`) is subscribed but `foo` is not. A /// `%` wildcard to `lsub` must return `foo`, not `foo/bar`, and it will be flagged with /// [`NameAttribute::NoSelect`]. /// /// The server will not unilaterally remove an existing mailbox name from the subscription list /// even if a mailbox by that name no longer exists. pub fn lsub( &mut self, reference_name: Option<&str>, mailbox_pattern: Option<&str>, ) -> Result { self.run_command_and_read_response(&format!( "LSUB {} {}", quote!(reference_name.unwrap_or("")), mailbox_pattern.unwrap_or("") )) .and_then(|lines| Names::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`STATUS` command](https://tools.ietf.org/html/rfc3501#section-6.3.10) requests the /// status of the indicated mailbox. It does not change the currently selected mailbox, nor /// does it affect the state of any messages in the queried mailbox (in particular, `status` /// will not cause messages to lose [`Flag::Recent`]). /// /// `status` provides an alternative to opening a second [`Session`] and using /// [`Session::examine`] on a mailbox to query that mailbox's status without deselecting the /// current mailbox in the first `Session`. /// /// Unlike [`Session::list`], `status` is not guaranteed to be fast in its response. Under /// certain circumstances, it can be quite slow. In some implementations, the server is /// obliged to open the mailbox read-only internally to obtain certain status information. /// Also unlike [`Session::list`], `status` does not accept wildcards. /// /// > Note: `status` is intended to access the status of mailboxes other than the currently /// > selected mailbox. Because `status` can cause the mailbox to be opened internally, and /// > because this information is available by other means on the selected mailbox, `status` /// > SHOULD NOT be used on the currently selected mailbox. /// /// The STATUS command MUST NOT be used as a "check for new messages in the selected mailbox" /// operation (refer to sections [7](https://tools.ietf.org/html/rfc3501#section-7), /// [7.3.1](https://tools.ietf.org/html/rfc3501#section-7.3.1), and /// [7.3.2](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more information about the /// proper method for new message checking). /// /// The currently defined status data items that can be requested are: /// /// - `MESSAGES`: The number of messages in the mailbox. /// - `RECENT`: The number of messages with [`Flag::Recent`] set. /// - `UIDNEXT`: The next [`Uid`] of the mailbox. /// - `UIDVALIDITY`: The unique identifier validity value of the mailbox (see [`Uid`]). /// - `UNSEEN`: The number of messages which do not have [`Flag::Seen`] set. /// - `HIGHESTMODSEQ`: The highest mod sequence of the mailbox, counting modifications made to it. /// /// `data_items` is a space-separated list enclosed in parentheses. pub fn status( &mut self, mailbox_name: impl AsRef, data_items: impl AsRef, ) -> Result { let mailbox_name = mailbox_name.as_ref(); self.run_command_and_read_response(&format!( "STATUS {} {}", validate_str("STATUS", "mailbox", mailbox_name)?, data_items.as_ref() )) .and_then(|lines| { parse_status(&lines[..], mailbox_name, &mut self.unsolicited_responses_tx) }) } /// This method returns a handle that lets you use the [`IDLE` /// command](https://tools.ietf.org/html/rfc2177#section-3) to listen for changes to the /// currently selected mailbox. /// /// It's often more desirable to have the server transmit updates to the client in real time. /// This allows a user to see new mail immediately. It also helps some real-time applications /// based on IMAP, which might otherwise need to poll extremely often (such as every few /// seconds). While the spec actually does allow a server to push `EXISTS` responses /// aysynchronously, a client can't expect this behaviour and must poll. This method provides /// you with such a mechanism. /// /// `idle` may be used with any server that returns `IDLE` as one of the supported capabilities /// (see [`Session::capabilities`]). If the server does not advertise the `IDLE` capability, /// the client MUST NOT use `idle` and must instead poll for mailbox updates. In particular, /// the client MUST continue to be able to accept unsolicited untagged responses to ANY /// command, as specified in the base IMAP specification. /// /// See [`extensions::idle::Handle`] for details. pub fn idle(&mut self) -> extensions::idle::Handle<'_, T> { extensions::idle::Handle::make(self) } /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) appends /// `content` as a new message to the end of the specified destination `mailbox`. This /// argument SHOULD be in the format of an [RFC-2822](https://tools.ietf.org/html/rfc2822) /// message. /// /// > Note: There MAY be exceptions, e.g., draft messages, in which required RFC-2822 header /// > lines are omitted in the message literal argument to `append`. The full implications of /// > doing so MUST be understood and carefully weighed. /// /// If the append is unsuccessful for any reason, the mailbox is restored to its state before /// the append attempt; no partial appending will happen. /// /// If the destination `mailbox` does not exist, the server returns an error, and does not /// automatically create the mailbox. /// /// If the mailbox is currently selected, the normal new message actions will generally occur. /// Specifically, the server will generally notify the client immediately via an untagged /// `EXISTS` response. If the server does not do so, the client MAY issue a `NOOP` command (or /// failing that, a `CHECK` command) after one or more `APPEND` commands. /// pub fn append<'a>(&'a mut self, mailbox: &'a str, content: &'a [u8]) -> AppendCmd<'a, T> { AppendCmd { session: self, content, mailbox, flags: Vec::new(), date: None, } } /// The [`SEARCH` command](https://tools.ietf.org/html/rfc3501#section-6.4.4) searches the /// mailbox for messages that match the given `query`. `query` consist of one or more search /// keys separated by spaces. The response from the server contains a listing of [`Seq`]s /// corresponding to those messages that match the searching criteria. /// /// When multiple search keys are specified, the result is the intersection of all the messages /// that match those keys. Or, in other words, only messages that match *all* the keys. For /// example, the criteria /// /// ```text /// DELETED FROM "SMITH" SINCE 1-Feb-1994 /// ``` /// /// refers to all deleted messages from Smith that were placed in the mailbox since February 1, /// 1994. A search key can also be a parenthesized list of one or more search keys (e.g., for /// use with the `OR` and `NOT` keys). /// /// In all search keys that use strings, a message matches the key if the string is a substring /// of the field. The matching is case-insensitive. /// /// Below is a selection of common search keys. The full list can be found in the /// specification of the [`SEARCH command`](https://tools.ietf.org/html/rfc3501#section-6.4.4). /// /// - `NEW`: Messages that have [`Flag::Recent`] set but not [`Flag::Seen`]. This is functionally equivalent to `(RECENT UNSEEN)`. /// - `OLD`: Messages that do not have [`Flag::Recent`] set. This is functionally equivalent to `NOT RECENT` (as opposed to `NOT NEW`). /// - `RECENT`: Messages that have [`Flag::Recent`] set. /// - `ANSWERED`: Messages with [`Flag::Answered`] set. /// - `DELETED`: Messages with [`Flag::Deleted`] set. /// - `DRAFT`: Messages with [`Flag::Draft`] set. /// - `FLAGGED`: Messages with [`Flag::Flagged`] set. /// - `SEEN`: Messages that have [`Flag::Seen`] set. /// - ``: Messages with message sequence numbers corresponding to the specified message sequence number set. /// - `UID `: Messages with [`Uid`] corresponding to the specified unique identifier set. Sequence set ranges are permitted. /// /// - `SUBJECT `: Messages that contain the specified string in the envelope structure's `SUBJECT` field. /// - `BODY `: Messages that contain the specified string in the body of the message. /// - `FROM `: Messages that contain the specified string in the envelope structure's `FROM` field. /// - `TO `: Messages that contain the specified string in the envelope structure's `TO` field. /// /// - `NOT `: Messages that do not match the specified search key. /// - `OR `: Messages that match either search key. /// /// - `BEFORE `: Messages whose internal date (disregarding time and timezone) is earlier than the specified date. /// - `SINCE `: Messages whose internal date (disregarding time and timezone) is within or later than the specified date. pub fn search(&mut self, query: impl AsRef) -> Result> { self.run_command_and_read_response(&format!("SEARCH {}", query.as_ref())) .and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx)) } /// Equivalent to [`Session::search`], except that the returned identifiers /// are [`Uid`] instead of [`Seq`]. See also the [`UID` /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8). pub fn uid_search(&mut self, query: impl AsRef) -> Result> { self.run_command_and_read_response(&format!("UID SEARCH {}", query.as_ref())) .and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx)) } /// This issues the [SORT command](https://tools.ietf.org/html/rfc5256#section-3), /// which returns sorted search results. /// /// This command is like [`Session::search`], except that /// the results are also sorted according to the supplied criteria (subject to the given charset). pub fn sort( &mut self, criteria: &[extensions::sort::SortCriterion<'_>], charset: extensions::sort::SortCharset<'_>, query: impl AsRef, ) -> Result> { self.run_command_and_read_response(&format!( "SORT {} {} {}", extensions::sort::SortCriteria(criteria), charset, query.as_ref() )) .and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx)) } /// Equivalent to [`Session::sort`], except that it returns [`Uid`]s. /// /// See also [`Session::uid_search`]. pub fn uid_sort( &mut self, criteria: &[extensions::sort::SortCriterion<'_>], charset: extensions::sort::SortCharset<'_>, query: impl AsRef, ) -> Result> { self.run_command_and_read_response(&format!( "UID SORT {} {} {}", extensions::sort::SortCriteria(criteria), charset, query.as_ref() )) .and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx)) } /// The [`SETACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.1) /// /// Modifies the ACLs on the given mailbox for the specified identifier. /// Return [`Error::No`] if the logged in user does not have `a` rights on the mailbox. /// /// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`] /// will be returned pub fn set_acl( &mut self, mailbox_name: impl AsRef, identifier: impl AsRef, rights: &AclRights, modification: AclModifyMode, ) -> Result<()> { let mod_str = match modification { AclModifyMode::Replace => "", AclModifyMode::Add => "+", AclModifyMode::Remove => "-", }; self.run_command_and_check_ok(&format!( "SETACL {} {} {}{}", validate_str("SETACL", "mailbox", mailbox_name.as_ref())?, validate_str("SETACL", "identifier", identifier.as_ref())?, mod_str, rights, )) } /// The [`DELETEACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.2) /// /// Removes the ACL for the given identifier from the given mailbox. /// Return [`Error::No`] if the logged in user does not have `a` rights on the mailbox. /// /// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`] /// will be returned pub fn delete_acl( &mut self, mailbox_name: impl AsRef, identifier: impl AsRef, ) -> Result<()> { self.run_command_and_check_ok(&format!( "DELETEACL {} {}", validate_str("DELETEACL", "mailbox", mailbox_name.as_ref())?, validate_str("DELETEACL", "identifier", identifier.as_ref())?, )) } /// The [`GETACL` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.3) /// /// Returns the ACLs on the given mailbox. A set ot `ACL` responses are returned if the /// logged in user has `a` rights on the mailbox. Otherwise, will return [`Error::No`]. /// /// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`] /// will be returned pub fn get_acl(&mut self, mailbox_name: impl AsRef) -> Result { self.run_command_and_read_response(&format!( "GETACL {}", validate_str("GETACL", "mailbox", mailbox_name.as_ref())? )) .and_then(|lines| AclResponse::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`LISTRIGHTS` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.4) /// /// Returns the always granted and optionally granted rights on the given mailbox for the /// specified identifier (login). A set ot `LISTRIGHTS` responses are returned if the /// logged in user has `a` rights on the mailbox. Otherwise, will return [`Error::No`]. /// /// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`] /// will be returned pub fn list_rights( &mut self, mailbox_name: impl AsRef, identifier: impl AsRef, ) -> Result { self.run_command_and_read_response(&format!( "LISTRIGHTS {} {}", validate_str("LISTRIGHTS", "mailbox", mailbox_name.as_ref())?, validate_str("LISTRIGHTS", "identifier", identifier.as_ref())? )) .and_then(|lines| ListRightsResponse::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`MYRIGHTS` command](https://datatracker.ietf.org/doc/html/rfc4314#section-3.5) /// /// Returns the list of rights the logged in user has on the given mailbox. /// /// This method only works against a server with the ACL capability. Otherwise [`Error::Bad`] /// will be returned pub fn my_rights(&mut self, mailbox_name: impl AsRef) -> Result { self.run_command_and_read_response(&format!( "MYRIGHTS {}", validate_str("MYRIGHTS", "mailbox", mailbox_name.as_ref())?, )) .and_then(|lines| MyRightsResponse::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`SETQUOTA` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.1) /// /// Updates the resource limits for a mailbox. Any previous limits for the named quota /// are discarded. /// /// Returns the updated quota. pub fn set_quota( &mut self, quota_root: impl AsRef, limits: &[QuotaResourceLimit<'_>], ) -> Result { let limits = iter_join(limits.iter(), " "); self.run_command_and_read_response(&format!( "SETQUOTA {} ({})", validate_str("SETQUOTA", "quota_root", quota_root.as_ref())?, limits, )) .and_then(|lines| QuotaResponse::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`GETQUOTA` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.2) /// /// Returns the quota information for the specified quota root pub fn get_quota(&mut self, quota_root: impl AsRef) -> Result { self.run_command_and_read_response(&format!( "GETQUOTA {}", validate_str("GETQUOTA", "quota_root", quota_root.as_ref())? )) .and_then(|lines| QuotaResponse::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`GETQUOTAROOT` command](https://datatracker.ietf.org/doc/html/rfc2087#section-4.3) /// /// Returns the quota roots along with their quota information for the specified mailbox pub fn get_quota_root(&mut self, mailbox_name: impl AsRef) -> Result { self.run_command_and_read_response(&format!( "GETQUOTAROOT {}", validate_str("GETQUOTAROOT", "mailbox", mailbox_name.as_ref())? )) .and_then(|lines| QuotaRootResponse::parse(lines, &mut self.unsolicited_responses_tx)) } // these are only here because they are public interface, the rest is in `Connection` /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: impl AsRef) -> Result<()> { self.run_command_and_read_response(command).map(|_| ()) } /// Runs any command passed to it. pub fn run_command(&mut self, untagged_command: impl AsRef) -> Result<()> { self.conn.run_command(untagged_command.as_ref()) } /// Run a raw IMAP command and read back its response. /// /// Note that the server *is* allowed to unilaterally send things to the client for messages in /// a selected mailbox whose status has changed. See the note on [unilateral server responses /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that you *may* see /// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses! /// /// The response includes the final [`Response::Done`], which starts at the returned index. pub fn run(&mut self, untagged_command: impl AsRef) -> Result<(Vec, usize)> { self.conn.run(untagged_command.as_ref()) } /// Run a raw IMAP command and read back its response. /// /// Note that the server *is* allowed to unilaterally send things to the client for messages in /// a selected mailbox whose status has changed. See the note on [unilateral server responses /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that you *may* see /// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses! /// /// The response does not include the final [`Response::Done`]. pub fn run_command_and_read_response( &mut self, untagged_command: impl AsRef, ) -> Result> { let (mut data, ok) = self.run(untagged_command)?; data.truncate(ok); Ok(data) } } impl Connection { /// Read the greeting from the connection. Needs to be done after `connect`ing. /// /// Panics if called more than once on the same `Connection`. pub fn read_greeting(&mut self) -> Result> { assert!(!self.greeting_read, "Greeting can only be read once"); let mut v = Vec::new(); self.readline(&mut v)?; self.greeting_read = true; Ok(v) } pub(crate) fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { self.run_command_and_read_response(command).map(|_| ()) } fn run_command(&mut self, untagged_command: &str) -> Result<()> { let command = self.create_command(untagged_command); let result = self.write_line(command.into_bytes().as_slice()); if result.is_err() { // rollback tag increased in create_command() self.tag -= 1; } result } fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { let (mut data, ok) = self.run(untagged_command)?; data.truncate(ok); Ok(data) } fn run(&mut self, untagged_command: &str) -> Result<(Vec, usize)> { self.run_command(untagged_command)?; self.read_response() } pub(crate) fn read_response(&mut self) -> Result<(Vec, usize)> { let mut v = Vec::new(); let ok = self.read_response_onto(&mut v)?; Ok((v, ok)) } /// Read responses until a Response::Done is encountered. /// /// The `Done` is included in `data`, and the index of the `Done` is returned. pub(crate) fn read_response_onto(&mut self, data: &mut Vec) -> Result { let mut continue_from = None; let mut try_first = !data.is_empty(); let match_tag = format!("{}{}", TAG_PREFIX, self.tag); loop { let line_start = if try_first { try_first = false; 0 } else { let start_new = data.len(); self.readline(data)?; continue_from.take().unwrap_or(start_new) }; let break_with = { use imap_proto::Status; let line = &data[line_start..]; match imap_proto::parser::parse_response(line) { Ok(( _, Response::Done { tag, status, information, code, .. }, )) => { // check if tag matches if tag.as_bytes() != match_tag.as_bytes() { let expect = self.tag; let actual = tag .0 .trim_start_matches(TAG_PREFIX) .parse::() .map_err(|_| tag.as_bytes().to_vec()); break Err(Error::TagMismatch(TagMismatch { expect, actual })); } Some(match status { Status::Bad | Status::No | Status::Bye => Err(( status, information.map(|v| v.into_owned()), code.map(|v| v.into_owned()), )), Status::Ok => Ok(()), status => Err((status, None, code.map(|v| v.into_owned()))), }) } Ok((..)) => None, Err(nom::Err::Incomplete(..)) => { continue_from = Some(line_start); None } _ => Some(Err((Status::Bye, None, None))), } }; match break_with { Some(Ok(_)) => { break Ok(line_start); } Some(Err((status, expl, code))) => { use imap_proto::Status; match status { Status::Bad => { break Err(Error::Bad(Bad { code, information: expl .unwrap_or_else(|| "no explanation given".to_string()), })); } Status::No => { break Err(Error::No(No { code, information: expl .unwrap_or_else(|| "no explanation given".to_string()), })); } Status::Bye => { break Err(Error::Bye(Bye { code, information: expl .unwrap_or_else(|| "no explanation given".to_string()), })); } _ => break Err(Error::Parse(ParseError::Invalid(data.split_off(0)))), } } None => {} } } } pub(crate) fn readline(&mut self, into: &mut Vec) -> Result { use std::io::BufRead; let read = self.stream.read_until(LF, into)?; if read == 0 { return Err(Error::ConnectionLost); } if self.debug { // Remove CRLF let len = into.len(); let line = &into[(len - read)..(len - 2)]; eprintln!("S: {}", String::from_utf8_lossy(line)); } Ok(read) } fn create_command(&mut self, command: &str) -> String { self.tag += 1; format!("{}{} {}", TAG_PREFIX, self.tag, command) } pub(crate) fn write_line(&mut self, buf: &[u8]) -> Result<()> { self.stream.write_all(buf)?; self.stream.write_all(&[CR, LF])?; self.stream.flush()?; if self.debug { eprintln!("C: {}", String::from_utf8(buf.to_vec()).unwrap()); } Ok(()) } } #[cfg(test)] pub(crate) mod testutils { use crate::mock_stream::MockStream; use super::*; pub(crate) fn assert_validation_error_client( run_command: F, expected_synopsis: &'static str, expected_argument: &'static str, expected_char: char, ) where F: FnOnce( Client, ) -> std::result::Result, (Error, Client)>, { let response = Vec::new(); let mock_stream = MockStream::new(response); let client = Client::new(mock_stream); assert_eq!( run_command(client) .expect_err("Error expected, but got success") .0 .to_string(), Error::Validate(ValidateError { command_synopsis: expected_synopsis.to_owned(), argument: expected_argument.to_string(), offending_char: expected_char }) .to_string() ); } pub(crate) fn assert_validation_error_session( run_command: F, expected_synopsis: &'static str, expected_argument: &'static str, expected_char: char, ) where F: FnOnce(Session) -> Result, { let response = Vec::new(); let mock_stream = MockStream::new(response); let session = Session::new(Client::new(mock_stream).conn); assert_eq!( run_command(session) .map(|_| ()) .expect_err("Error expected, but got success") .to_string(), Error::Validate(ValidateError { command_synopsis: expected_synopsis.to_owned(), argument: expected_argument.to_string(), offending_char: expected_char }) .to_string() ); } pub(crate) fn assert_quota_resource( resource: &QuotaResource<'_>, name: QuotaResourceName<'_>, limit: u64, usage: u64, ) { assert_eq!(resource.name, name); assert_eq!(resource.usage, usage); assert_eq!(resource.limit, limit); } } #[cfg(test)] mod tests { use super::super::mock_stream::MockStream; use super::*; use imap_proto::types::Capability; use std::borrow::Cow; use super::testutils::*; macro_rules! mock_session { ($s:expr) => { Session::new(Client::new($s).conn) }; } #[test] fn read_response() { let response = "a0 OK Logged in.\r\n"; let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mut client = Client::new(mock_stream); let (mut actual_response, i) = client.read_response().unwrap(); actual_response.truncate(i); assert_eq!(Vec::::new(), actual_response); } #[test] fn fetch_body() { let response = "a0 OK Logged in.\r\n\ * 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\ a0 OK FETCH completed\r\n"; let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mut session = mock_session!(mock_stream); session.read_response().unwrap(); session.read_response().unwrap(); } #[test] fn read_greeting() { let greeting = "* OK Dovecot ready.\r\n"; let mock_stream = MockStream::new(greeting.as_bytes().to_vec()); let mut client = Client::new(mock_stream); client.read_greeting().unwrap(); } #[test] fn readline_delay_read() { let greeting = "* OK Dovecot ready.\r\n"; let expected_response: String = greeting.to_string(); let mock_stream = MockStream::default() .with_buf(greeting.as_bytes().to_vec()) .with_delay(); let mut client = Client::new(mock_stream); let mut v = Vec::new(); client.readline(&mut v).unwrap(); let actual_response = String::from_utf8(v).unwrap(); assert_eq!(expected_response, actual_response); } #[test] fn readline_eof() { let mock_stream = MockStream::default().with_eof(); let mut client = Client::new(mock_stream); let mut v = Vec::new(); if let Err(Error::ConnectionLost) = client.readline(&mut v) { } else { unreachable!("EOF read did not return connection lost"); } } #[test] #[should_panic] fn readline_err() { // TODO Check the error test let mock_stream = MockStream::default().with_err(); let mut client = Client::new(mock_stream); let mut v = Vec::new(); client.readline(&mut v).unwrap(); } #[test] fn create_command() { let base_command = "CHECK"; let mock_stream = MockStream::default(); let mut imap_stream = Client::new(mock_stream); let expected_command = format!("a1 {}", base_command); let command = imap_stream.create_command(&base_command); assert!( command == expected_command, "expected command doesn't equal actual command" ); let expected_command2 = format!("a2 {}", base_command); let command2 = imap_stream.create_command(&base_command); assert!( command2 == expected_command2, "expected command doesn't equal actual command" ); } #[test] fn authenticate() { let response = b"+ YmFy\r\n\ a1 OK Logged in\r\n" .to_vec(); let command = "a1 AUTHENTICATE PLAIN\r\n\ Zm9v\r\n"; let mock_stream = MockStream::new(response); let client = Client::new(mock_stream); enum Authenticate { Auth, } impl Authenticator for Authenticate { type Response = Vec; fn process(&self, challenge: &[u8]) -> Self::Response { assert!(challenge == b"bar", "Invalid authenticate challenge"); b"foo".to_vec() } } let session = client.authenticate("PLAIN", &Authenticate::Auth).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid authenticate command" ); } #[test] fn pre_login_capability() { let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ a1 OK CAPABILITY completed\r\n" .to_vec(); let expected_capabilities = vec![ Capability::Imap4rev1, Capability::Atom(Cow::Borrowed("STARTTLS")), Capability::Auth(Cow::Borrowed("GSSAPI")), Capability::Atom(Cow::Borrowed("LOGINDISABLED")), ]; let mock_stream = MockStream::new(response); let mut client = Client::new(mock_stream); let capabilities = client.capabilities().unwrap(); assert_eq!( client.stream.get_ref().written_buf, b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command" ); assert_eq!(capabilities.len(), 4); for e in expected_capabilities { assert!(capabilities.has(&e)); } } #[test] fn login() { let response = b"a1 OK Logged in\r\n".to_vec(); let username = "username"; let password = "password"; let command = format!("a1 LOGIN {} {}\r\n", quote!(username), quote!(password)); let mock_stream = MockStream::new(response); let client = Client::new(mock_stream); let session = client.login(username, password).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid login command" ); } #[test] fn login_validation_username() { let username = "username\n"; let password = "password"; assert_validation_error_client( |client| client.login(username, password), "LOGIN", "username", '\n', ); } #[test] fn login_validation_password() { let username = "username"; let password = "passw\rord"; assert_validation_error_client( |client| client.login(username, password), "LOGIN", "password", '\r', ); } #[test] fn logout() { let response = b"a1 OK Logout completed.\r\n".to_vec(); let command = format!("a1 LOGOUT\r\n"); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.logout().unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid logout command" ); } #[test] fn logout_with_untagged_bye() { let response = b"* BYE Logging out\r\na1 OK Logout completed.\r\n".to_vec(); let command = format!("a1 LOGOUT\r\n"); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.logout().unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid logout command" ); } #[test] fn logout_with_tagged_bye() { let response = b"a1 BYE IMAP4rev1 Server logging out\r\n".to_vec(); let command = format!("a1 LOGOUT\r\n"); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.logout().unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid logout command" ); } #[test] fn rename() { let response = b"a1 OK RENAME completed\r\n".to_vec(); let current_mailbox_name = "INBOX"; let new_mailbox_name = "NEWINBOX"; let command = format!( "a1 RENAME {} {}\r\n", quote!(current_mailbox_name), quote!(new_mailbox_name) ); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session .rename(current_mailbox_name, new_mailbox_name) .unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid rename command" ); } #[test] fn subscribe() { let response = b"a1 OK SUBSCRIBE completed\r\n".to_vec(); let mailbox = "INBOX"; let command = format!("a1 SUBSCRIBE {}\r\n", quote!(mailbox)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.subscribe(mailbox).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid subscribe command" ); } #[test] fn unsubscribe() { let response = b"a1 OK UNSUBSCRIBE completed\r\n".to_vec(); let mailbox = "INBOX"; let command = format!("a1 UNSUBSCRIBE {}\r\n", quote!(mailbox)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.unsubscribe(mailbox).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid unsubscribe command" ); } #[test] fn expunge() { let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.expunge().unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 EXPUNGE\r\n".to_vec(), "Invalid expunge command" ); } #[test] fn uid_expunge() { let response = b"* 2 EXPUNGE\r\n\ * 3 EXPUNGE\r\n\ * 4 EXPUNGE\r\n\ a1 OK UID EXPUNGE completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.uid_expunge("2:4").unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 UID EXPUNGE 2:4\r\n".to_vec(), "Invalid expunge command" ); } #[test] fn check() { let response = b"a1 OK CHECK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.check().unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command" ); } #[test] fn examine() { let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\ * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\ * 1 EXISTS\r\n\ * 1 RECENT\r\n\ * OK [UNSEEN 1] First unseen.\r\n\ * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\ * OK [UIDNEXT 2] Predicted next UID\r\n\ a1 OK [READ-ONLY] Select completed.\r\n" .to_vec(); let expected_mailbox = Mailbox { flags: vec![ Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Seen, Flag::Draft, ], exists: 1, recent: 1, unseen: Some(1), permanent_flags: vec![], uid_next: Some(2), uid_validity: Some(1257842737), highest_mod_seq: None, is_read_only: true, }; let mailbox_name = "INBOX"; let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let mailbox = session.examine(mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid examine command" ); assert_eq!(mailbox, expected_mailbox); } #[test] fn examine_validation() { assert_validation_error_session( |mut session| session.examine("INB\nOX"), "EXAMINE", "mailbox", '\n', ) } #[test] fn select() { let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\ * OK [PERMANENTFLAGS (\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)] \ Read-only mailbox.\r\n\ * 1 EXISTS\r\n\ * 1 RECENT\r\n\ * OK [UNSEEN 1] First unseen.\r\n\ * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\ * OK [UIDNEXT 2] Predicted next UID\r\n\ a1 OK [READ-ONLY] Select completed.\r\n" .to_vec(); let expected_mailbox = Mailbox { flags: vec![ Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Seen, Flag::Draft, ], exists: 1, recent: 1, unseen: Some(1), permanent_flags: vec![ Flag::MayCreate, Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Draft, Flag::Seen, ], uid_next: Some(2), uid_validity: Some(1257842737), highest_mod_seq: None, is_read_only: true, }; let mailbox_name = "INBOX"; let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let mailbox = session.select(mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid select command" ); assert_eq!(mailbox, expected_mailbox); } #[test] fn select_validation() { assert_validation_error_session( |mut session| session.select("INB\nOX"), "SELECT", "mailbox", '\n', ) } #[test] fn search() { let response = b"* SEARCH 1 2 3 4 5\r\n\ a1 OK Search completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let ids = session.search("Unseen").unwrap(); let ids: HashSet = ids.iter().cloned().collect(); assert!( session.stream.get_ref().written_buf == b"a1 SEARCH Unseen\r\n".to_vec(), "Invalid search command" ); assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect()); } #[test] fn uid_search() { let response = b"* SEARCH 1 2 3 4 5\r\n\ a1 OK Search completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let ids = session.uid_search("Unseen").unwrap(); let ids: HashSet = ids.iter().cloned().collect(); assert!( session.stream.get_ref().written_buf == b"a1 UID SEARCH Unseen\r\n".to_vec(), "Invalid search command" ); assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect()); } #[test] fn sort() { use extensions::sort::{SortCharset, SortCriterion}; let response = b"* SORT 1 2 3 4 5\r\n\ a1 OK Sort completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let ids = session .sort(&[SortCriterion::Arrival], SortCharset::Utf8, "ALL") .unwrap(); let ids: Vec = ids.iter().cloned().collect(); assert!( session.stream.get_ref().written_buf == b"a1 SORT (ARRIVAL) UTF-8 ALL\r\n".to_vec(), "Invalid sort command" ); assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::>()); } #[test] fn uid_sort() { use extensions::sort::{SortCharset, SortCriterion}; let response = b"* SORT 1 2 3 4 5\r\n\ a1 OK Sort completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let ids = session .uid_sort( &[SortCriterion::Reverse(&SortCriterion::Size)], SortCharset::UsAscii, "SUBJECT", ) .unwrap(); let ids: Vec = ids.iter().cloned().collect(); assert!( session.stream.get_ref().written_buf == b"a1 UID SORT (REVERSE SIZE) US-ASCII SUBJECT\r\n".to_vec(), "Invalid sort command" ); assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::>()); } #[test] fn set_acl_replace() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session .set_acl( "INBOX", "myuser", &"x".try_into().unwrap(), AclModifyMode::Replace, ) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETACL \"INBOX\" \"myuser\" x\r\n".to_vec(), "Invalid setacl command" ); } #[test] fn set_acl_add() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session .set_acl( "INBOX", "myuser", &"x".try_into().unwrap(), AclModifyMode::Add, ) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETACL \"INBOX\" \"myuser\" +x\r\n".to_vec(), "Invalid setacl command" ); } #[test] fn set_acl_remove() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session .set_acl( "INBOX", "myuser", &"x".try_into().unwrap(), AclModifyMode::Remove, ) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETACL \"INBOX\" \"myuser\" -x\r\n".to_vec(), "Invalid setacl command" ); } #[test] fn delete_acl() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.delete_acl("INBOX", "myuser").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 DELETEACL \"INBOX\" \"myuser\"\r\n".to_vec(), "Invalid deleteacl command" ); } #[test] fn get_acl() { let response = b"* ACL INBOX myuser lr\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.get_acl("INBOX").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETACL \"INBOX\"\r\n".to_vec(), "Invalid getacl command" ); assert_eq!(acl.parsed().mailbox(), "INBOX"); assert_eq!( acl.parsed().acls(), vec![AclEntry { identifier: "myuser".into(), rights: "lr".try_into().unwrap(), }] ); } #[test] fn get_acl_invalid_no_acl_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.get_acl("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETACL \"INBOX\"\r\n".to_vec(), "Invalid getacl command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn get_acl_invalid_too_many_acl_lines() { let response = b"* ACL INBOX myuser lr\r\n\ * ACL INBOX myuser lr\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.get_acl("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETACL \"INBOX\"\r\n".to_vec(), "Invalid getacl command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn get_acl_multiple_users() { let response = b"* ACL INBOX myuser lr other_user lr\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.get_acl("INBOX").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETACL \"INBOX\"\r\n".to_vec(), "Invalid getacl command" ); assert_eq!(acl.parsed().mailbox(), "INBOX"); assert_eq!( acl.parsed().acls(), vec![ AclEntry { identifier: "myuser".into(), rights: "lr".try_into().unwrap(), }, AclEntry { identifier: "other_user".into(), rights: "lr".try_into().unwrap(), }, ] ); } #[test] fn list_rights() { let response = b"* LISTRIGHTS INBOX myuser lr x k\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.list_rights("INBOX", "myuser").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(), "Invalid listrights command" ); assert_eq!(acl.parsed().mailbox(), "INBOX"); assert_eq!(acl.parsed().identifier(), "myuser"); assert_eq!(*acl.parsed().required(), "lr".try_into().unwrap()); assert_eq!(*acl.parsed().optional(), "kx".try_into().unwrap()); } #[test] fn list_rights_invalid_no_rights_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.list_rights("INBOX", "myuser"); assert_eq!( session.stream.get_ref().written_buf, b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(), "Invalid listrights command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn list_rights_invalid_too_many_rights_lines() { let response = b"* LISTRIGHTS INBOX myuser lr x k\r\n\ * LISTRIGHTS INBOX myuser lr x k\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.list_rights("INBOX", "myuser"); assert_eq!( session.stream.get_ref().written_buf, b"a1 LISTRIGHTS \"INBOX\" \"myuser\"\r\n".to_vec(), "Invalid listrights command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn my_rights() { let response = b"* MYRIGHTS INBOX lrxk\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.my_rights("INBOX").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(), "Invalid myrights command" ); assert_eq!(acl.parsed().mailbox(), "INBOX"); assert_eq!(*acl.parsed().rights(), "lrkx".try_into().unwrap()) } #[test] fn my_rights_invalid_no_rights_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.my_rights("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(), "Invalid myrights command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn my_rights_invalid_too_many_rights_lines() { let response = b"* MYRIGHTS INBOX lrxk\r\n\ * MYRIGHTS INBOX lrxk\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let acl = session.my_rights("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 MYRIGHTS \"INBOX\"\r\n".to_vec(), "Invalid myrights command" ); assert!(matches!(acl, Err(Error::Parse(_)))); } #[test] fn set_quota() { let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session .set_quota( "my_root", &[QuotaResourceLimit { name: QuotaResourceName::Storage, amount: 500, }], ) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(), "Invalid setquota command" ); let quota = quota.parsed().as_ref().unwrap(); assert_eq!(quota.root_name, "my_root"); assert_eq!(quota.resources.len(), 1); assert_quota_resource("a.resources[0], QuotaResourceName::Storage, 500, 10); } #[test] fn set_quota_via_quota_resource_limit_new() { let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session .set_quota("my_root", &[QuotaResourceLimit::new("STORAGE", 500)]) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(), "Invalid setquota command" ); let quota = quota.parsed().as_ref().unwrap(); assert_eq!(quota.root_name, "my_root"); assert_eq!(quota.resources.len(), 1); assert_quota_resource("a.resources[0], QuotaResourceName::Storage, 500, 10); } #[test] fn set_quota_no_such_quota_root() { let response = b"a1 NO no such quota root\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.set_quota( "my_root", &[QuotaResourceLimit { name: QuotaResourceName::Storage, amount: 500, }], ); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(), "Invalid setquota command" ); assert!(matches!(quota, Err(Error::No(_)))); } #[test] fn set_quota_invalid_no_quota_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session .set_quota( "my_root", &[QuotaResourceLimit { name: QuotaResourceName::Storage, amount: 500, }], ) .unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(), "Invalid setquota command" ); assert_eq!(quota.parsed(), &None); } #[test] fn set_quota_invalid_too_many_quota_lines() { let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\ * QUOTA my_root2 (STORAGE 10 500)\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.set_quota( "my_root", &[QuotaResourceLimit { name: QuotaResourceName::Storage, amount: 500, }], ); assert_eq!( session.stream.get_ref().written_buf, b"a1 SETQUOTA \"my_root\" (STORAGE 500)\r\n".to_vec(), "Invalid setquota command" ); assert!(matches!(quota, Err(Error::Parse(_)))); } #[test] fn get_quota() { let response = b"* QUOTA my_root ()\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.get_quota("my_root").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTA \"my_root\"\r\n".to_vec(), "Invalid getquota command" ); let quota = quota.parsed().as_ref().unwrap(); assert_eq!(quota.root_name, "my_root"); assert_eq!(quota.resources, vec![]); } #[test] fn get_quota_with_limits() { let response = b"* QUOTA my_root (STORAGE 10 500)\r a1 OK completed\r " .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.get_quota("my_root").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTA \"my_root\"\r\n".to_vec(), "Invalid getquota command" ); let quota = quota.parsed().as_ref().unwrap(); assert_eq!(quota.root_name, "my_root"); assert_eq!(quota.resources.len(), 1); assert_quota_resource("a.resources[0], QuotaResourceName::Storage, 500, 10); } #[test] fn get_quota_with_invalid_no_quota_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.get_quota("my_root").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTA \"my_root\"\r\n".to_vec(), "Invalid getquota command" ); assert_eq!(quota.parsed(), &None); } #[test] fn get_quota_with_invalid_too_many_quota_lines() { let response = b"* QUOTA my_root (STORAGE 10 500)\r\n\ * QUOTA my_root2 (STORAGE 10 500)\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.get_quota("my_root"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTA \"my_root\"\r\n".to_vec(), "Invalid getquota command" ); assert!(matches!(quota, Err(Error::Parse(_)))); } #[test] fn get_quota_with_no_such_quota_root() { let response = b"a1 NO no such quota root\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota = session.get_quota("my_root"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTA \"my_root\"\r\n".to_vec(), "Invalid getquota command" ); assert!(matches!(quota, Err(Error::No(_)))); } #[test] fn get_quota_root_with_no_root() { let response = b"* QUOTAROOT INBOX\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota_root = session.get_quota_root("INBOX").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(), "Invalid getquotaroot command" ); assert_eq!(quota_root.mailbox_name(), "INBOX"); assert_eq!( quota_root.quota_root_names().collect::>(), Vec::<&str>::new() ); assert_eq!(quota_root.quotas(), vec![]); } #[test] fn get_quota_root_no_such_mailbox() { let response = b"a1 NO no such mailboxd\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota_root = session.get_quota_root("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(), "Invalid getquotaroot command" ); assert!(matches!(quota_root, Err(Error::No(_)))); } #[test] fn get_quota_root_invalid_no_quota_root_lines() { let response = b"a1 OK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota_root = session.get_quota_root("INBOX"); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(), "Invalid getquotaroot command" ); assert!(matches!(quota_root, Err(Error::Parse(_)))); } #[test] fn get_quota_root_invalid_too_many_quota_root_lines() { let response = b"* QUOTAROOT INBOX\r\n\ * QUOTAROOT INBOX\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota_root = session.get_quota_root("INBOX"); println!("Resp: {:?}", quota_root); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(), "Invalid getquotaroot command" ); assert!(matches!(quota_root, Err(Error::Parse(_)))); } #[test] fn get_quota_root_with_root() { let response = b"* QUOTAROOT INBOX my_root\r\n\ * QUOTA my_root (STORAGE 10 500)\r\n\ a1 OK completed\r\n" .to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let quota_root = session.get_quota_root("INBOX").unwrap(); assert_eq!( session.stream.get_ref().written_buf, b"a1 GETQUOTAROOT \"INBOX\"\r\n".to_vec(), "Invalid getquotaroot command" ); assert_eq!(quota_root.mailbox_name(), "INBOX"); assert_eq!( quota_root.quota_root_names().collect::>(), vec!["my_root"] ); assert_eq!(quota_root.quotas().len(), 1); assert_eq!(quota_root.quotas().first().unwrap().root_name, "my_root"); assert_eq!( quota_root.quotas().first().unwrap().resources, vec![QuotaResource { name: QuotaResourceName::Storage, usage: 10, limit: 500, }] ); } #[test] fn capability() { let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ a1 OK CAPABILITY completed\r\n" .to_vec(); let expected_capabilities = vec![ Capability::Imap4rev1, Capability::Atom(Cow::Borrowed("STARTTLS")), Capability::Auth(Cow::Borrowed("GSSAPI")), Capability::Atom(Cow::Borrowed("LOGINDISABLED")), ]; let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); let capabilities = session.capabilities().unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command" ); assert_eq!(capabilities.len(), 4); for e in expected_capabilities { assert!(capabilities.has(&e)); } } #[test] fn create() { let response = b"a1 OK CREATE completed\r\n".to_vec(); let mailbox_name = "INBOX"; let command = format!("a1 CREATE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.create(mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid create command" ); } #[test] fn create_validation() { assert_validation_error_session( |mut session| session.create("INB\nOX"), "CREATE", "mailbox", '\n', ) } #[test] fn delete() { let response = b"a1 OK DELETE completed\r\n".to_vec(); let mailbox_name = "INBOX"; let command = format!("a1 DELETE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.delete(mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid delete command" ); } #[test] fn delete_validation() { assert_validation_error_session( |mut session| session.delete("INB\nOX"), "DELETE", "mailbox", '\n', ) } #[test] fn noop() { let response = b"a1 OK NOOP completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.noop().unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 NOOP\r\n".to_vec(), "Invalid noop command" ); } #[test] fn close() { let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.close().unwrap(); assert!( session.stream.get_ref().written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command" ); } #[test] fn store() { generic_store(" ", |c, set, query| c.store(set, query)); } #[test] fn uid_store() { generic_store(" UID ", |c, set, query| c.uid_store(set, query)); } fn generic_store(prefix: &str, op: F) where F: FnOnce(&mut Session, &str, &str) -> Result, { let res = "* 2 FETCH (FLAGS (\\Deleted \\Seen))\r\n\ * 3 FETCH (FLAGS (\\Deleted))\r\n\ * 4 FETCH (FLAGS (\\Deleted \\Flagged \\Seen))\r\n\ a1 OK STORE completed\r\n"; generic_with_uid(res, "STORE", "2.4", "+FLAGS (\\Deleted)", prefix, op); } #[test] fn copy() { generic_copy(" ", |c, set, query| c.copy(set, query)) } #[test] fn uid_copy() { generic_copy(" UID ", |c, set, query| c.uid_copy(set, query)) } fn generic_copy(prefix: &str, op: F) where F: FnOnce(&mut Session, &str, &str) -> Result, { generic_with_uid( "OK COPY completed\r\n", "COPY", "2:4", "MEETING", prefix, op, ); } #[test] fn mv() { let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\ * 2 EXPUNGE\r\n\ * 1 EXPUNGE\r\n\ a1 OK Move completed\r\n" .to_vec(); let mailbox_name = "MEETING"; let command = format!("a1 MOVE 1:2 {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.mv("1:2", mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid move command" ); } #[test] fn mv_validation_query() { assert_validation_error_session( |mut session| session.mv("1:2", "MEE\nTING"), "MOVE", "mailbox", '\n', ) } #[test] fn uid_mv() { let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\ * 2 EXPUNGE\r\n\ * 1 EXPUNGE\r\n\ a1 OK Move completed\r\n" .to_vec(); let mailbox_name = "MEETING"; let command = format!("a1 UID MOVE 41:42 {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); let mut session = mock_session!(mock_stream); session.uid_mv("41:42", mailbox_name).unwrap(); assert!( session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid uid move command" ); } #[test] fn uid_mv_validation_query() { assert_validation_error_session( |mut session| session.uid_mv("1:2", "MEE\nTING"), "UID MOVE", "mailbox", '\n', ) } #[test] fn fetch() { generic_fetch(" ", |c, seq, query| c.fetch(seq, query)) } #[test] fn fetch_validation_seq() { assert_validation_error_session( |mut session| session.fetch("\r1", "BODY[]"), "FETCH", "seq", '\r', ) } #[test] fn fetch_validation_query() { assert_validation_error_session( |mut session| session.fetch("1", "BODY[\n]"), "FETCH", "query", '\n', ) } #[test] fn uid_fetch() { generic_fetch(" UID ", |c, seq, query| c.uid_fetch(seq, query)) } fn generic_fetch(prefix: &str, op: F) where F: FnOnce(&mut Session, &str, &str) -> Result, { generic_with_uid("OK FETCH completed\r\n", "FETCH", "1", "BODY[]", prefix, op); } fn generic_with_uid(res: &str, cmd: &str, seq: &str, query: &str, prefix: &str, op: F) where F: FnOnce(&mut Session, &str, &str) -> Result, { let resp = format!("a1 {}\r\n", res).as_bytes().to_vec(); let line = format!("a1{}{} {} {}\r\n", prefix, cmd, seq, query); let mut session = mock_session!(MockStream::new(resp)); let _ = op(&mut session, seq, query); assert!( session.stream.get_ref().written_buf == line.as_bytes().to_vec(), "Invalid command" ); } #[test] fn uid_fetch_validation_seq() { assert_validation_error_session( |mut session| session.uid_fetch("\r1", "BODY[]"), "UID FETCH", "seq", '\r', ) } #[test] fn uid_fetch_validation_query() { assert_validation_error_session( |mut session| session.uid_fetch("1", "BODY[\n]"), "UID FETCH", "query", '\n', ) } #[test] fn status_validation_mailbox() { assert_validation_error_session( |mut session| session.status("IN\nBOX", "(MESSAGES)"), "STATUS", "mailbox", '\n', ) } #[test] fn quote_backslash() { assert_eq!("\"test\\\\text\"", quote!(r"test\text")); } #[test] fn quote_dquote() { assert_eq!("\"test\\\"text\"", quote!("test\"text")); } #[test] fn validate_random() { assert_eq!( "\"~iCQ_k;>[&\\\"sVCvUW`e<[&\"sVCvUW`e<