Merge branch 'master' into metadata-merge

This commit is contained in:
Jon Gjengset 2021-03-06 12:43:43 -05:00 committed by GitHub
commit a466e947bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 950 additions and 88 deletions

49
CHANGELOG.md Normal file
View file

@ -0,0 +1,49 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- VANISHED support in EXPUNGE responses and unsolicited responses (#172).
### Changed
- MSRV increased to 1.43 for nom6 and bitvec
- `expunge` and `uid_expunge` return `Result<Deleted>` instead of `Result<Vec<u32>>`.
### Removed
## [2.4.1] - 2021-01-12
### Changed
- Handle empty-set inputs to `fetch` and `uid_fetch` (#177)
## [2.4.0] - 2020-12-15
### Added
- `append_with_flags_and_date` (#174)
## [2.3.0] - 2020-08-23
### Added
- `append_with_flags` (#171)
## [2.2.0] - 2020-07-27
### Added
- Changelog
- STARTTLS example (#165)
- Timeout example (#168)
- Export `Result` and `Error` types (#170)
### Changed
- MSRV increased
- Better documentation of server greeting handling (#168)
[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.4.1...HEAD
[2.4.1]: https://github.com/jonhoo/rust-imap/compare/v2.4.0...v2.4.1
[2.4.0]: https://github.com/jonhoo/rust-imap/compare/v2.3.0...v2.4.0
[2.3.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.3.0
[2.2.0]: https://github.com/jonhoo/rust-imap/compare/v2.1.2...v2.2.0

View file

@ -1,26 +1,18 @@
[package] [package]
name = "imap" name = "imap"
version = "2.1.2" version = "3.0.0-alpha.1"
authors = ["Matt McCoy <mattnenterprise@yahoo.com>", authors = ["Jon Gjengset <jon@thesquareplanet.com>",
"Jon Gjengset <jon@thesquareplanet.com>"] "Matt McCoy <mattnenterprise@yahoo.com>"]
documentation = "https://docs.rs/imap/" documentation = "https://docs.rs/imap/"
repository = "https://github.com/jonhoo/rust-imap" repository = "https://github.com/jonhoo/rust-imap"
homepage = "https://github.com/jonhoo/rust-imap" homepage = "https://github.com/jonhoo/rust-imap"
description = "IMAP client for Rust" description = "IMAP client for Rust"
readme = "README.md" license = "Apache-2.0 OR MIT"
license = "Apache-2.0/MIT"
edition = "2018" edition = "2018"
keywords = ["email", "imap"] keywords = ["email", "imap"]
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
[badges]
azure-devops = { project = "jonhoo/jonhoo", pipeline = "imap", build = "11" }
codecov = { repository = "jonhoo/rust-imap", branch = "master", service = "github" }
maintenance = { status = "actively-developed" }
is-it-maintained-issue-resolution = { repository = "jonhoo/rust-imap" }
is-it-maintained-open-issues = { repository = "jonhoo/rust-imap" }
[features] [features]
tls = ["native-tls"] tls = ["native-tls"]
default = ["tls"] default = ["tls"]
@ -29,16 +21,16 @@ default = ["tls"]
native-tls = { version = "0.2.2", optional = true } native-tls = { version = "0.2.2", optional = true }
regex = "1.0" regex = "1.0"
bufstream = "0.1" bufstream = "0.1"
imap-proto = "0.10.0" imap-proto = "0.12.0"
nom = "5.0" nom = "6.0"
base64 = "0.12" base64 = "0.13"
chrono = "0.4" chrono = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
[dev-dependencies] [dev-dependencies]
lettre = "0.9" lettre = "0.9"
lettre_email = "0.9" lettre_email = "0.9"
rustls-connector = "0.11.0" rustls-connector = "0.13.0"
[[example]] [[example]]
name = "basic" name = "basic"

View file

@ -25,9 +25,6 @@ jobs:
- template: install-rust.yml@templates - template: install-rust.yml@templates
parameters: parameters:
rust: $(rust) rust: $(rust)
components:
- rustfmt
- clippy
- script: cargo check --all-targets - script: cargo check --all-targets
displayName: cargo check displayName: cargo check
- script: cargo test --examples - script: cargo test --examples
@ -36,10 +33,16 @@ jobs:
displayName: cargo test --doc displayName: cargo test --doc
- script: cargo test --lib - script: cargo test --lib
displayName: cargo test --lib displayName: cargo test --lib
- script: cargo fmt --all -- --check - script: |
set -e
rustup component add rustfmt
cargo fmt --all -- --check
displayName: cargo fmt --check displayName: cargo fmt --check
condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' )) condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' ))
- script: cargo clippy -- -D warnings - script: |
set -e
rustup component add clippy
cargo clippy -- -D warnings
displayName: cargo clippy displayName: cargo clippy
condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' )) condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' ))
# This represents the minimum Rust version supported. # This represents the minimum Rust version supported.
@ -47,12 +50,12 @@ jobs:
- job: msrv - job: msrv
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
displayName: "Minimum supported Rust version: 1.36.0" displayName: "Minimum supported Rust version: 1.43.0"
dependsOn: [] dependsOn: []
steps: steps:
- template: install-rust.yml@templates - template: install-rust.yml@templates
parameters: parameters:
rust: 1.36.0 rust: 1.43.0 # nom6 depends on bitvec (1.43+)
- script: cargo check - script: cargo check
displayName: cargo check displayName: cargo check
- job: integration - job: integration
@ -79,11 +82,11 @@ resources:
- repository: templates - repository: templates
type: github type: github
name: crate-ci/azure-pipelines name: crate-ci/azure-pipelines
ref: refs/heads/v0.3 ref: refs/heads/v0.4
endpoint: jonhoo endpoint: jonhoo
containers: containers:
- container: greenmail - container: greenmail
image: greenmail/standalone:1.5.11 image: greenmail/standalone:1.5.13
ports: ports:
- 3025:3025 - 3025:3025
- 3110:3110 - 3110:3110

View file

@ -7,3 +7,4 @@ Examples:
* basic - This is a very basic example of using the client. * basic - This is a very basic example of using the client.
* gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication. * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication.
* rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation). * rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation).
* timeout - This demonstrates how to use timeouts while connecting to an IMAP server.

63
examples/starttls.rs Normal file
View file

@ -0,0 +1,63 @@
/**
* Here's an example showing how to connect to the IMAP server with STARTTLS.
* The only difference with the `basic.rs` example is when using `imap::connect_starttls()` method
* instead of `imap::connect()` (l. 52), and so you can connect on port 143 instead of 993
* as you have to when using TLS the entire way.
*
* The following env vars are expected to be set:
* - IMAP_HOST
* - IMAP_USERNAME
* - IMAP_PASSWORD
* - IMAP_PORT (supposed to be 143)
*/
extern crate imap;
extern crate native_tls;
use native_tls::TlsConnector;
use std::env;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let imap_host = env::var("IMAP_HOST").expect("Missing or invalid env var: IMAP_HOST");
let imap_username =
env::var("IMAP_USERNAME").expect("Missing or invalid env var: IMAP_USERNAME");
let imap_password =
env::var("IMAP_PASSWORD").expect("Missing or invalid env var: IMAP_PASSWORD");
let imap_port: u16 = env::var("IMAP_PORT")
.expect("Missing or invalid env var: IMAP_PORT")
.to_string()
.parse()
.unwrap();
if let Some(_email) = fetch_inbox_top(imap_host, imap_username, imap_password, imap_port)? {
eprintln!("OK :)");
}
Ok(())
}
fn fetch_inbox_top(
host: String,
username: String,
password: String,
port: u16,
) -> Result<Option<String>, Box<dyn Error>> {
let domain: &str = host.as_str();
let tls = TlsConnector::builder().build().unwrap();
// we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::connect_starttls((domain, port), domain, &tls).unwrap();
// the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in
let mut _imap_session = client
.login(username.as_str(), password.as_str())
.map_err(|e| e.0)?;
// TODO Here you can process as you want. eg. search/fetch messages according to your needs.
// This returns `Ok(None)` for the need of the example
Ok(None)
}

92
examples/timeout.rs Normal file
View file

@ -0,0 +1,92 @@
extern crate imap;
extern crate native_tls;
use imap::Client;
use native_tls::TlsConnector;
use native_tls::TlsStream;
use std::env;
use std::error::Error;
use std::fmt;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
fn main() -> Result<(), Box<dyn Error>> {
let server = env::var("IMAP_SERVER")?;
let port = env::var("IMAP_PORT").unwrap_or_else(|_| String::from("993"));
let port = port.parse()?;
let username = env::var("IMAP_USER")?;
let password = env::var("IMAP_PASSWORD")?;
let timeout = env::var("IMAP_TIMEOUT").unwrap_or_else(|_| String::from("1"));
let timeout = timeout.parse()?;
let timeout = Duration::from_secs(timeout);
let tls = TlsConnector::builder().build()?;
let client = connect_all_timeout((server.as_str(), port), server.as_str(), &tls, timeout)?;
let mut session = client.login(&username, &password).map_err(|e| e.0)?;
// do something productive with session
session.logout()?;
Ok(())
}
// connect to an IMAP host with a `Duration` timeout; note that this accepts only a single
// `SocketAddr` while `connect_all_timeout` does resolve the DNS entry and try to connect to all;
// this is necessary due to the difference of the function signatures of `TcpStream::connect` and
// `TcpStream::connect_timeout`
fn connect_timeout<S: AsRef<str>>(
addr: &SocketAddr,
domain: S,
ssl_connector: &TlsConnector,
timeout: Duration,
) -> Result<Client<TlsStream<TcpStream>>, Box<dyn Error>> {
// the timeout is actually used with the initial TcpStream
let tcp_stream = TcpStream::connect_timeout(addr, timeout)?;
let tls_stream = TlsConnector::connect(ssl_connector, domain.as_ref(), tcp_stream)?;
let mut client = Client::new(tls_stream);
// don't forget to wait for the IMAP protocol server greeting ;)
client.read_greeting()?;
Ok(client)
}
// resolve address and try to connect to all in order; note that this function is required to fully
// mimic `imap::connect` with the usage of `ToSocketAddrs`
fn connect_all_timeout<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
ssl_connector: &TlsConnector,
timeout: Duration,
) -> Result<Client<TlsStream<TcpStream>>, Box<dyn Error>> {
let addrs = addr.to_socket_addrs()?;
for addr in addrs {
match connect_timeout(&addr, &domain, ssl_connector, timeout) {
Ok(client) => return Ok(client),
Err(error) => eprintln!("couldn't connect to {}: {}", addr, error),
}
}
Err(Box::new(TimeoutError))
}
// very simple timeout error; instead of printing the errors immediately like in
// `connect_all_timeout`, you may want to collect and return them
#[derive(Debug)]
struct TimeoutError;
impl fmt::Display for TimeoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "all addresses failed to connect")
}
}
impl Error for TimeoutError {}

View file

@ -1,8 +1,7 @@
use base64;
use bufstream::BufStream; use bufstream::BufStream;
use chrono::{DateTime, FixedOffset};
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use nom;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs}; use std::net::{TcpStream, ToSocketAddrs};
@ -27,15 +26,74 @@ macro_rules! quote {
}; };
} }
trait OptionExt<E> {
fn err(self) -> std::result::Result<(), E>;
}
impl<E> OptionExt<E> for Option<E> {
fn err(self) -> std::result::Result<(), E> {
match self {
Some(e) => Err(e),
None => Ok(()),
}
}
}
/// Convert the input into what [the IMAP
/// grammar](https://tools.ietf.org/html/rfc3501#section-9)
/// calls "quoted", which is reachable from "string" et al.
/// Also ensure it doesn't contain a colliding command-delimiter (newline).
pub(crate) fn validate_str(value: &str) -> Result<String> { pub(crate) fn validate_str(value: &str) -> Result<String> {
let quoted = quote!(value); validate_str_noquote(value)?;
if quoted.find('\n').is_some() { Ok(quote!(value))
return Err(Error::Validate(ValidateError('\n'))); }
}
if quoted.find('\r').is_some() { /// Ensure the input doesn't contain a command-terminator (newline), but don't quote it like
return Err(Error::Validate(ValidateError('\r'))); /// `validate_str`.
} /// This is helpful for things like the FETCH attributes, which,
Ok(quoted) /// per [the IMAP grammar](https://tools.ietf.org/html/rfc3501#section-9) may not be quoted:
///
/// > fetch = "FETCH" SP sequence-set SP ("ALL" / "FULL" / "FAST" /
/// > fetch-att / "(" fetch-att *(SP fetch-att) ")")
/// >
/// > fetch-att = "ENVELOPE" / "FLAGS" / "INTERNALDATE" /
/// > "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] /
/// > "BODY" ["STRUCTURE"] / "UID" /
/// > "BODY" section ["<" number "." nz-number ">"] /
/// > "BODY.PEEK" section ["<" number "." nz-number ">"]
///
/// Note the lack of reference to any of the string-like rules or the quote characters themselves.
fn validate_str_noquote(value: &str) -> Result<&str> {
value
.matches(|c| c == '\n' || c == '\r')
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
}
/// This ensures the input doesn't contain a command-terminator or any other whitespace
/// while leaving it not-quoted.
/// This is needed because, per [the formal grammer given in RFC
/// 3501](https://tools.ietf.org/html/rfc3501#section-9), a sequence set consists of the following:
///
/// > sequence-set = (seq-number / seq-range) *("," sequence-set)
/// > seq-range = seq-number ":" seq-number
/// > seq-number = nz-number / "*"
/// > nz-number = digit-nz *DIGIT
/// > digit-nz = %x31-39
///
/// Note the lack of reference to SP or any other such whitespace terminals.
/// Per this grammar, in theory we ought to be even more restrictive than "no whitespace".
fn validate_sequence_set(value: &str) -> Result<&str> {
value
.matches(|c: char| c.is_ascii_whitespace())
.next()
.and_then(|s| s.chars().next())
.map(|offender| Error::Validate(ValidateError(offender)))
.err()?;
Ok(value)
} }
/// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from /// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from
@ -83,6 +141,87 @@ pub struct Connection<T: Read + Write> {
pub greeting_read: bool, pub greeting_read: bool,
} }
/// A builder for the append command
#[must_use]
pub struct AppendCmd<'a, T: Read + Write> {
session: &'a mut Session<T>,
content: &'a [u8],
mailbox: &'a str,
flags: Vec<Flag<'a>>,
date: Option<DateTime<FixedOffset>>,
}
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<Item = Flag<'a>>) -> &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<FixedOffset>) -> &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 = self
.flags
.clone()
.into_iter()
.filter(|f| *f != Flag::Recent)
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(" ");
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().map(|_| ())
}
}
// `Deref` instances are so we can make use of the same underlying primitives in `Client` and // `Deref` instances are so we can make use of the same underlying primitives in `Client` and
// `Session` // `Session`
impl<T: Read + Write> Deref for Client<T> { impl<T: Read + Write> Deref for Client<T> {
@ -240,6 +379,29 @@ impl<T: Read + Write> Client<T> {
/// ///
/// This method primarily exists for writing tests that mock the underlying transport, but can /// This method primarily exists for writing tests that mock the underlying transport, but can
/// also be used to support IMAP over custom tunnels. /// also be used to support IMAP over custom tunnels.
///
/// **Note:** In case you do need to use `Client::new` over `imap::connect`, you will need to
/// listen for the IMAP protocol server greeting before authenticating:
///
/// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use imap::Client;
/// # use native_tls::TlsConnector;
/// # use std::io;
/// # use std::net::TcpStream;
/// # fn main() {
/// # let server = "imap.example.com";
/// # let username = "";
/// # let password = "";
/// # let tcp = TcpStream::connect((server, 993)).unwrap();
/// # 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<T> { pub fn new(stream: T) -> Client<T> {
Client { Client {
conn: Connection { conn: Connection {
@ -456,7 +618,7 @@ impl<T: Read + Write> Session<T> {
.and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx))
} }
/// Fetch retreives data associated with a set of messages in the mailbox. /// 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 /// 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 /// messages in the selected mailbox whose status has changed. See the note on [unilateral
@ -519,12 +681,16 @@ impl<T: Read + Write> Session<T> {
S1: AsRef<str>, S1: AsRef<str>,
S2: AsRef<str>, S2: AsRef<str>,
{ {
self.run_command_and_read_response(&format!( if sequence_set.as_ref().is_empty() {
"FETCH {} {}", parse_fetches(vec![], &mut self.unsolicited_responses_tx)
sequence_set.as_ref(), } else {
query.as_ref() self.run_command_and_read_response(&format!(
)) "FETCH {} {}",
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) validate_sequence_set(sequence_set.as_ref())?,
validate_str_noquote(query.as_ref())?
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
} }
/// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are /// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
@ -534,12 +700,16 @@ impl<T: Read + Write> Session<T> {
S1: AsRef<str>, S1: AsRef<str>,
S2: AsRef<str>, S2: AsRef<str>,
{ {
self.run_command_and_read_response(&format!( if uid_set.as_ref().is_empty() {
"UID FETCH {} {}", parse_fetches(vec![], &mut self.unsolicited_responses_tx)
uid_set.as_ref(), } else {
query.as_ref() self.run_command_and_read_response(&format!(
)) "UID FETCH {} {}",
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) validate_sequence_set(uid_set.as_ref())?,
validate_str_noquote(query.as_ref())?
))
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
}
} }
/// Noop always succeeds, and it does nothing. /// Noop always succeeds, and it does nothing.
@ -666,7 +836,7 @@ impl<T: Read + Write> Session<T> {
/// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently /// 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. /// 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. /// The message sequence number of each message that is removed is returned.
pub fn expunge(&mut self) -> Result<Vec<Seq>> { pub fn expunge(&mut self) -> Result<Deleted> {
self.run_command_and_read_response("EXPUNGE") self.run_command_and_read_response("EXPUNGE")
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
} }
@ -693,7 +863,7 @@ impl<T: Read + Write> Session<T> {
/// ///
/// Alternatively, the client may fall back to using just [`Session::expunge`], risking the /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the
/// unintended removal of some messages. /// unintended removal of some messages.
pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Vec<Uid>> { pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Deleted> {
self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref())) self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref()))
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
} }
@ -1054,22 +1224,15 @@ impl<T: Read + Write> Session<T> {
/// Specifically, the server will generally notify the client immediately via an untagged /// 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 /// `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. /// failing that, a `CHECK` command) after one or more `APPEND` commands.
pub fn append<S: AsRef<str>, B: AsRef<[u8]>>(&mut self, mailbox: S, content: B) -> Result<()> { ///
let content = content.as_ref(); pub fn append<'a>(&'a mut self, mailbox: &'a str, content: &'a [u8]) -> AppendCmd<'a, T> {
self.run_command(&format!( AppendCmd {
"APPEND \"{}\" {{{}}}", session: self,
mailbox.as_ref(), content,
content.len() mailbox,
))?; flags: Vec::new(),
let mut v = Vec::new(); date: None,
self.readline(&mut v)?;
if !v.starts_with(b"+") {
return Err(Error::Append);
} }
self.stream.write_all(content)?;
self.stream.write_all(b"\r\n")?;
self.stream.flush()?;
self.read_response().map(|_| ())
} }
/// The [`SEARCH` command](https://tools.ietf.org/html/rfc3501#section-6.4.4) searches the /// The [`SEARCH` command](https://tools.ietf.org/html/rfc3501#section-6.4.4) searches the
@ -1204,10 +1367,10 @@ impl<T: Read + Write> Connection<T> {
}; };
let break_with = { let break_with = {
use imap_proto::{parse_response, Response, Status}; use imap_proto::{Response, Status};
let line = &data[line_start..]; let line = &data[line_start..];
match parse_response(line) { match imap_proto::parser::parse_response(line) {
Ok(( Ok((
_, _,
Response::Done { Response::Done {
@ -1559,6 +1722,7 @@ mod tests {
permanent_flags: vec![], permanent_flags: vec![],
uid_next: Some(2), uid_next: Some(2),
uid_validity: Some(1257842737), uid_validity: Some(1257842737),
highest_mod_seq: None,
}; };
let mailbox_name = "INBOX"; let mailbox_name = "INBOX";
let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name)); let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name));
@ -1605,6 +1769,7 @@ mod tests {
], ],
uid_next: Some(2), uid_next: Some(2),
uid_validity: Some(1257842737), uid_validity: Some(1257842737),
highest_mod_seq: None,
}; };
let mailbox_name = "INBOX"; let mailbox_name = "INBOX";
let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name)); let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name));

View file

@ -21,6 +21,7 @@ pub type Result<T> = result::Result<T, Error>;
/// A set of errors that can occur in the IMAP client /// A set of errors that can occur in the IMAP client
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive]
pub enum Error { pub enum Error {
/// An `io::Error` that occurred while trying to read or write to a network stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
Io(IoError), Io(IoError),
@ -43,8 +44,6 @@ pub enum Error {
Validate(ValidateError), Validate(ValidateError),
/// Error appending an e-mail. /// Error appending an e-mail.
Append, Append,
#[doc(hidden)]
__Nonexhaustive,
} }
impl From<IoError> for Error { impl From<IoError> for Error {
@ -99,7 +98,6 @@ impl fmt::Display for Error {
Error::Bad(ref data) => write!(f, "Bad Response: {}", data), Error::Bad(ref data) => write!(f, "Bad Response: {}", data),
Error::ConnectionLost => f.write_str("Connection Lost"), Error::ConnectionLost => f.write_str("Connection Lost"),
Error::Append => f.write_str("Could not append mail to mailbox"), Error::Append => f.write_str("Could not append mail to mailbox"),
Error::__Nonexhaustive => f.write_str("Unknown"),
} }
} }
} }
@ -119,7 +117,6 @@ impl StdError for Error {
Error::No(_) => "No Response", Error::No(_) => "No Response",
Error::ConnectionLost => "Connection lost", Error::ConnectionLost => "Connection lost",
Error::Append => "Could not append mail to mailbox", Error::Append => "Could not append mail to mailbox",
Error::__Nonexhaustive => "Unknown",
} }
} }

View file

@ -87,6 +87,7 @@ mod client;
pub use crate::client::*; pub use crate::client::*;
pub mod error; pub mod error;
pub use error::{Error, Result};
pub mod extensions; pub mod extensions;

View file

@ -44,7 +44,7 @@ where
break Ok(things); break Ok(things);
} }
match imap_proto::parse_response(lines) { match imap_proto::parser::parse_response(lines) {
Ok((rest, resp)) => { Ok((rest, resp)) => {
lines = rest; lines = rest;
@ -127,13 +127,46 @@ pub fn parse_fetches(
pub fn parse_expunge( pub fn parse_expunge(
lines: Vec<u8>, lines: Vec<u8>,
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>, unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Vec<u32>> { ) -> Result<Deleted> {
let f = |resp| match resp { let mut lines: &[u8] = &lines;
Response::Expunge(id) => Ok(MapOrNot::Map(id)), let mut expunged = Vec::new();
resp => Ok(MapOrNot::Not(resp)), let mut vanished = Vec::new();
};
unsafe { parse_many(lines, f, unsolicited).map(|ids| ids.take()) } loop {
if lines.is_empty() {
break;
}
match imap_proto::parser::parse_response(lines) {
Ok((rest, Response::Expunge(seq))) => {
lines = rest;
expunged.push(seq);
}
Ok((rest, Response::Vanished { earlier: _, uids })) => {
lines = rest;
vanished.extend(uids);
}
Ok((rest, data)) => {
lines = rest;
if let Some(resp) = handle_unilateral(data, unsolicited) {
return Err(resp.into());
}
}
_ => {
return Err(Error::Parse(ParseError::Invalid(lines.to_vec())));
}
}
}
// If the server sends a VANISHED response then they must only send VANISHED
// in lieu of EXPUNGE responses for the rest of this connection, so it is
// always one or the other.
// https://tools.ietf.org/html/rfc7162#section-3.2.10
if !vanished.is_empty() {
Ok(Deleted::from_vanished(vanished))
} else {
Ok(Deleted::from_expunged(expunged))
}
} }
pub fn parse_capabilities( pub fn parse_capabilities(
@ -143,7 +176,7 @@ pub fn parse_capabilities(
let f = |mut lines| { let f = |mut lines| {
let mut caps = HashSet::new(); let mut caps = HashSet::new();
loop { loop {
match imap_proto::parse_response(lines) { match imap_proto::parser::parse_response(lines) {
Ok((rest, Response::Capabilities(c))) => { Ok((rest, Response::Capabilities(c))) => {
lines = rest; lines = rest;
caps.extend(c); caps.extend(c);
@ -179,7 +212,7 @@ pub fn parse_noop(
break Ok(()); break Ok(());
} }
match imap_proto::parse_response(lines) { match imap_proto::parser::parse_response(lines) {
Ok((rest, data)) => { Ok((rest, data)) => {
lines = rest; lines = rest;
if let Some(resp) = handle_unilateral(data, unsolicited) { if let Some(resp) = handle_unilateral(data, unsolicited) {
@ -200,7 +233,7 @@ pub fn parse_mailbox(
let mut mailbox = Mailbox::default(); let mut mailbox = Mailbox::default();
loop { loop {
match imap_proto::parse_response(lines) { match imap_proto::parser::parse_response(lines) {
Ok((rest, Response::Data { status, code, .. })) => { Ok((rest, Response::Data { status, code, .. })) => {
lines = rest; lines = rest;
@ -212,6 +245,9 @@ pub fn parse_mailbox(
use imap_proto::ResponseCode; use imap_proto::ResponseCode;
match code { match code {
Some(ResponseCode::HighestModSeq(seq)) => {
mailbox.highest_mod_seq = Some(seq);
}
Some(ResponseCode::UidValidity(uid)) => { Some(ResponseCode::UidValidity(uid)) => {
mailbox.uid_validity = Some(uid); mailbox.uid_validity = Some(uid);
} }
@ -254,7 +290,8 @@ pub fn parse_mailbox(
} }
MailboxDatum::List { .. } MailboxDatum::List { .. }
| MailboxDatum::MetadataSolicited { .. } | MailboxDatum::MetadataSolicited { .. }
| MailboxDatum::MetadataUnsolicited { .. } => {} | MailboxDatum::MetadataUnsolicited { .. }
| MailboxDatum::Search { .. } => {}
} }
} }
Ok((rest, Response::Expunge(n))) => { Ok((rest, Response::Expunge(n))) => {
@ -286,8 +323,8 @@ pub fn parse_ids(
break Ok(ids); break Ok(ids);
} }
match imap_proto::parse_response(lines) { match imap_proto::parser::parse_response(lines) {
Ok((rest, Response::IDs(c))) => { Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => {
lines = rest; lines = rest;
ids.extend(c); ids.extend(c);
} }
@ -322,8 +359,15 @@ pub(crate) fn handle_unilateral<'a>(
Response::MailboxData(MailboxDatum::Recent(n)) => { Response::MailboxData(MailboxDatum::Recent(n)) => {
unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
} }
Response::MailboxData(MailboxDatum::Flags(_)) => { Response::MailboxData(MailboxDatum::Flags(flags)) => {
// TODO: next breaking change: unsolicited
.send(UnsolicitedResponse::Flags(
flags
.into_iter()
.map(|s| Flag::from(s.to_string()))
.collect(),
))
.unwrap();
} }
Response::MailboxData(MailboxDatum::Exists(n)) => { Response::MailboxData(MailboxDatum::Exists(n)) => {
unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap(); unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap();
@ -342,6 +386,11 @@ pub(crate) fn handle_unilateral<'a>(
}) })
.unwrap(); .unwrap();
} }
Response::Vanished { earlier, uids } => {
unsolicited
.send(UnsolicitedResponse::Vanished { earlier, uids })
.unwrap();
}
res => { res => {
return Some(res); return Some(res);
} }
@ -569,4 +618,53 @@ mod tests {
let ids: HashSet<u32> = ids.iter().cloned().collect(); let ids: HashSet<u32> = ids.iter().cloned().collect();
assert_eq!(ids, HashSet::<u32>::new()); assert_eq!(ids, HashSet::<u32>::new());
} }
#[test]
fn parse_vanished_test() {
// VANISHED can appear if the user has enabled QRESYNC (RFC 7162), in response to
// SELECT/EXAMINE (QRESYNC); UID FETCH (VANISHED); or EXPUNGE commands. In the first
// two cases the VANISHED response will be a different type than expected
// and so goes into the unsolicited responses channel.
let lines = b"* VANISHED 3\r\n";
let (mut send, recv) = mpsc::channel();
let resp = parse_expunge(lines.to_vec(), &mut send).unwrap();
// Should be not empty, and have no seqs
assert!(!resp.is_empty());
assert_eq!(None, resp.seqs().next());
// Should have one UID response
let mut uids = resp.uids();
assert_eq!(Some(3), uids.next());
assert_eq!(None, uids.next());
// Should be nothing in the unsolicited responses channel
assert!(recv.try_recv().is_err());
// Test VANISHED mixed with FETCH
let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\
* 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n";
let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap();
match recv.try_recv().unwrap() {
UnsolicitedResponse::Vanished { earlier, uids } => {
assert!(earlier);
assert_eq!(uids.len(), 3);
assert_eq!(*uids[0].start(), 3);
assert_eq!(*uids[0].end(), 8);
assert_eq!(*uids[1].start(), 12);
assert_eq!(*uids[1].end(), 12);
assert_eq!(*uids[2].start(), 50);
assert_eq!(*uids[2].end(), 60);
}
what => panic!("Unexpected response in unsolicited responses: {:?}", what),
}
assert!(recv.try_recv().is_err());
assert_eq!(fetches.len(), 1);
assert_eq!(fetches[0].message, 49);
assert_eq!(fetches[0].flags(), &[Flag::Seen, Flag::Answered]);
assert_eq!(fetches[0].uid, Some(117));
assert_eq!(fetches[0].body(), None);
assert_eq!(fetches[0].header(), None);
}
} }

180
src/types/deleted.rs Normal file
View file

@ -0,0 +1,180 @@
use super::{Seq, Uid};
use std::ops::RangeInclusive;
/// An enum representing message sequence numbers or UID sequence sets returned
/// in response to a `EXPUNGE` command.
///
/// The `EXPUNGE` command may return several `EXPUNGE` responses referencing
/// message sequence numbers, or it may return a `VANISHED` response referencing
/// multiple UID values in a sequence set if the client has enabled
/// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.2.7).
///
/// `Deleted` implements some iterators to make it easy to use. If the caller
/// knows that they should be receiving an `EXPUNGE` or `VANISHED` response,
/// then they can use [`seqs()`](#method.seqs) to get an iterator over `EXPUNGE`
/// message sequence numbers, or [`uids()`](#method.uids) to get an iterator over
/// the `VANISHED` UIDs. As a convenience `Deleted` also implents `IntoIterator`
/// which just returns an iterator over whatever is contained within.
///
/// # Examples
/// ```no_run
/// # let domain = "imap.example.com";
/// # let tls = native_tls::TlsConnector::builder().build().unwrap();
/// # let client = imap::connect((domain, 993), domain, &tls).unwrap();
/// # let mut session = client.login("name", "pw").unwrap();
/// // Iterate over whatever is returned
/// if let Ok(deleted) = session.expunge() {
/// for id in &deleted {
/// // Do something with id
/// }
/// }
///
/// // Expect a VANISHED response with UIDs
/// if let Ok(deleted) = session.expunge() {
/// for uid in deleted.uids() {
/// // Do something with uid
/// }
/// }
/// ```
#[derive(Debug, Clone)]
pub enum Deleted {
/// Message sequence numbers given in an `EXPUNGE` response.
Expunged(Vec<Seq>),
/// Message UIDs given in a `VANISHED` response.
Vanished(Vec<RangeInclusive<Uid>>),
}
impl Deleted {
/// Construct a new `Deleted` value from a vector of message sequence
/// numbers returned in one or more `EXPUNGE` responses.
pub fn from_expunged(v: Vec<u32>) -> Self {
Deleted::Expunged(v)
}
/// Construct a new `Deleted` value from a sequence-set of UIDs
/// returned in a `VANISHED` response
pub fn from_vanished(v: Vec<RangeInclusive<u32>>) -> Self {
Deleted::Vanished(v)
}
/// Return an iterator over message sequence numbers from an `EXPUNGE`
/// response. If the client is expecting sequence numbers this function
/// can be used to ensure only sequence numbers returned in an `EXPUNGE`
/// response are processed.
pub fn seqs(&self) -> impl Iterator<Item = Seq> + '_ {
match self {
Deleted::Expunged(s) => s.iter(),
Deleted::Vanished(_) => [].iter(),
}
.copied()
}
/// Return an iterator over UIDs returned in a `VANISHED` response.
/// If the client is expecting UIDs this function can be used to ensure
/// only UIDs are processed.
pub fn uids(&self) -> impl Iterator<Item = Uid> + '_ {
match self {
Deleted::Expunged(_) => [].iter(),
Deleted::Vanished(s) => s.iter(),
}
.flat_map(|range| range.clone())
}
/// Return if the set is empty
pub fn is_empty(&self) -> bool {
match self {
Deleted::Expunged(v) => v.is_empty(),
Deleted::Vanished(v) => v.is_empty(),
}
}
}
impl<'a> IntoIterator for &'a Deleted {
type Item = u32;
type IntoIter = Box<dyn Iterator<Item = u32> + 'a>;
fn into_iter(self) -> Self::IntoIter {
match self {
Deleted::Expunged(_) => Box::new(self.seqs()),
Deleted::Vanished(_) => Box::new(self.uids()),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn seq() {
let seqs = Deleted::from_expunged(vec![3, 6, 9, 12]);
let mut i = seqs.into_iter();
assert_eq!(Some(3), i.next());
assert_eq!(Some(6), i.next());
assert_eq!(Some(9), i.next());
assert_eq!(Some(12), i.next());
assert_eq!(None, i.next());
let seqs = Deleted::from_expunged(vec![]);
let mut i = seqs.into_iter();
assert_eq!(None, i.next());
}
#[test]
fn seq_set() {
let uids = Deleted::from_vanished(vec![1..=1, 3..=5, 8..=9, 12..=12]);
let mut i = uids.into_iter();
assert_eq!(Some(1), i.next());
assert_eq!(Some(3), i.next());
assert_eq!(Some(4), i.next());
assert_eq!(Some(5), i.next());
assert_eq!(Some(8), i.next());
assert_eq!(Some(9), i.next());
assert_eq!(Some(12), i.next());
assert_eq!(None, i.next());
let uids = Deleted::from_vanished(vec![]);
assert_eq!(None, uids.into_iter().next());
}
#[test]
fn seqs() {
let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]);
let mut count: u32 = 0;
for seq in seqs.seqs() {
count += 3;
assert_eq!(seq, count);
}
assert_eq!(count, 12);
}
#[test]
fn uids() {
let uids: Deleted = Deleted::from_vanished(vec![1..=6]);
let mut count: u32 = 0;
for uid in uids.uids() {
count += 1;
assert_eq!(uid, count);
}
assert_eq!(count, 6);
}
#[test]
fn generic_iteration() {
let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]);
let mut count: u32 = 0;
for seq in &seqs {
count += 3;
assert_eq!(seq, count);
}
assert_eq!(count, 12);
let uids: Deleted = Deleted::from_vanished(vec![1..=6]);
let mut count: u32 = 0;
for uid in &uids {
count += 1;
assert_eq!(uid, count);
}
assert_eq!(count, 6);
}
}

View file

@ -4,6 +4,7 @@ use std::fmt;
/// Meta-information about an IMAP mailbox, as returned by /// Meta-information about an IMAP mailbox, as returned by
/// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends. /// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends.
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct Mailbox { pub struct Mailbox {
/// Defined flags in the mailbox. See the description of the [FLAGS /// Defined flags in the mailbox. See the description of the [FLAGS
/// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail. /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail.
@ -35,6 +36,10 @@ pub struct Mailbox {
/// The unique identifier validity value. See [`Uid`] for more details. If this is missing, /// The unique identifier validity value. See [`Uid`] for more details. If this is missing,
/// the server does not support unique identifiers. /// the server does not support unique identifiers.
pub uid_validity: Option<u32>, pub uid_validity: Option<u32>,
/// The highest mod sequence for this mailbox. Used with
/// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1).
pub highest_mod_seq: Option<u64>,
} }
impl Default for Mailbox { impl Default for Mailbox {
@ -47,6 +52,7 @@ impl Default for Mailbox {
permanent_flags: Vec::new(), permanent_flags: Vec::new(),
uid_next: None, uid_next: None,
uid_validity: None, uid_validity: None,
highest_mod_seq: None,
} }
} }
} }
@ -56,14 +62,15 @@ impl fmt::Display for Mailbox {
write!( write!(
f, f,
"flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\
uid_next: {:?}, uid_validity: {:?}", uid_next: {:?}, uid_validity: {:?}, highest_mod_seq: {:?}",
self.flags, self.flags,
self.exists, self.exists,
self.recent, self.recent,
self.unseen, self.unseen,
self.permanent_flags, self.permanent_flags,
self.uid_next, self.uid_next,
self.uid_validity self.uid_validity,
self.highest_mod_seq,
) )
} }
} }

View file

@ -169,6 +169,21 @@ impl Flag<'static> {
} }
} }
impl<'a> fmt::Display for Flag<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Flag::Seen => write!(f, "\\Seen"),
Flag::Answered => write!(f, "\\Answered"),
Flag::Flagged => write!(f, "\\Flagged"),
Flag::Deleted => write!(f, "\\Deleted"),
Flag::Draft => write!(f, "\\Draft"),
Flag::Recent => write!(f, "\\Recent"),
Flag::MayCreate => write!(f, "\\*"),
Flag::Custom(ref s) => write!(f, "{}", s),
}
}
}
impl<'a> From<String> for Flag<'a> { impl<'a> From<String> for Flag<'a> {
fn from(s: String) -> Self { fn from(s: String) -> Self {
if let Some(f) = Flag::system(&s) { if let Some(f) = Flag::system(&s) {
@ -201,6 +216,9 @@ pub use self::name::{Name, NameAttribute};
mod capabilities; mod capabilities;
pub use self::capabilities::Capabilities; pub use self::capabilities::Capabilities;
mod deleted;
pub use self::deleted::Deleted;
/// re-exported from imap_proto; /// re-exported from imap_proto;
pub use imap_proto::StatusAttribute; pub use imap_proto::StatusAttribute;
@ -211,6 +229,7 @@ pub use imap_proto::StatusAttribute;
/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder, /// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
/// so the user must take care when interpreting these. /// so the user must take care when interpreting these.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsolicitedResponse { pub enum UnsolicitedResponse {
/// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4). /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
Status { Status {
@ -271,6 +290,41 @@ pub enum UnsolicitedResponse {
/// List of annotations that were changed. /// List of annotations that were changed.
metadata_entries: Vec<String>, metadata_entries: Vec<String>,
}, },
/// An unsolicited [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10)
/// that reports a sequence-set of `UID`s that have been expunged from the mailbox.
///
/// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever
/// an `EXPUNGE` response can be sent. It can only be sent by the server if the client
/// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162).
///
/// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to
/// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER`
/// tag, which is used to announce removals within an already selected mailbox.
///
/// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a
/// single round trip by including the `(CHANGEDSINCE <MODSEQ> VANISHED)`
/// modifier to the `UID SEARCH` command, as described in
/// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example
/// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH`
/// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s
/// will be present as a `VANISHED` response in the `Session::unsolicited_responses`
/// channel.
Vanished {
/// Whether the `EARLIER` tag was set on the response
earlier: bool,
/// The list of `UID`s which have been removed
uids: Vec<std::ops::RangeInclusive<u32>>,
},
/// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that
/// identifies the flags (at a minimum, the system-defined flags) that are applicable in the
/// mailbox. Flags other than the system flags can also exist, depending on server
/// implementation.
///
/// See [`Flag`] for details.
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
Flags(Vec<Flag<'static>>),
} }
/// This type wraps an input stream and a type that was constructed by parsing that input stream, /// This type wraps an input stream and a type that was constructed by parsing that input stream,
@ -318,6 +372,7 @@ impl<D> ZeroCopy<D> {
/// ///
/// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned` /// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned`
/// passed to `ZeroCopy::new`). /// passed to `ZeroCopy::new`).
#[allow(dead_code)]
pub(crate) unsafe fn take(self) -> D { pub(crate) unsafe fn take(self) -> D {
self.derived self.derived
} }

View file

@ -1,8 +1,10 @@
extern crate chrono;
extern crate imap; extern crate imap;
extern crate lettre; extern crate lettre;
extern crate lettre_email; extern crate lettre_email;
extern crate native_tls; extern crate native_tls;
use chrono::{FixedOffset, TimeZone};
use lettre::Transport; use lettre::Transport;
use std::net::TcpStream; use std::net::TcpStream;
@ -231,3 +233,160 @@ fn list() {
// TODO: make a subdir // TODO: make a subdir
} }
#[test]
fn append() {
let to = "inbox-append1@localhost";
// make a message to append
let e: lettre::SendableEmail = lettre_email::Email::builder()
.from("sender@localhost")
.to(to)
.subject("My second e-mail")
.text("Hello world")
.build()
.unwrap()
.into();
// connect
let mut c = session(to);
let mbox = "INBOX";
c.select(mbox).unwrap();
//append
c.append(mbox, e.message_to_string().unwrap().as_bytes())
.finish()
.unwrap();
// now we should see the e-mail!
let inbox = c.uid_search("ALL").unwrap();
// and the one message should have the first message sequence number
assert_eq!(inbox.len(), 1);
let uid = inbox.into_iter().next().unwrap();
// fetch the e-mail
let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap();
assert_eq!(fetch.len(), 1);
let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid));
let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My second e-mail"[..]));
// and let's delete it to clean up
c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)")
.unwrap();
c.expunge().unwrap();
// the e-mail should be gone now
let inbox = c.search("ALL").unwrap();
assert_eq!(inbox.len(), 0);
}
#[test]
fn append_with_flags() {
use imap::types::Flag;
let to = "inbox-append2@localhost";
// make a message to append
let e: lettre::SendableEmail = lettre_email::Email::builder()
.from("sender@localhost")
.to(to)
.subject("My third e-mail")
.text("Hello world")
.build()
.unwrap()
.into();
// connect
let mut c = session(to);
let mbox = "INBOX";
c.select(mbox).unwrap();
//append
let flags = vec![Flag::Seen, Flag::Flagged];
c.append(mbox, e.message_to_string().unwrap().as_bytes())
.flags(flags)
.finish()
.unwrap();
// now we should see the e-mail!
let inbox = c.uid_search("ALL").unwrap();
// and the one message should have the first message sequence number
assert_eq!(inbox.len(), 1);
let uid = inbox.into_iter().next().unwrap();
// fetch the e-mail
let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap();
assert_eq!(fetch.len(), 1);
let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid));
let e = fetch.envelope().unwrap();
assert_eq!(e.subject, Some(&b"My third e-mail"[..]));
// check the flags
let setflags = fetch.flags();
assert!(setflags.contains(&Flag::Seen));
assert!(setflags.contains(&Flag::Flagged));
// and let's delete it to clean up
c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)")
.unwrap();
c.expunge().unwrap();
// the e-mail should be gone now
let inbox = c.search("ALL").unwrap();
assert_eq!(inbox.len(), 0);
}
#[test]
fn append_with_flags_and_date() {
use imap::types::Flag;
let to = "inbox-append3@localhost";
// make a message to append
let e: lettre::SendableEmail = lettre_email::Email::builder()
.from("sender@localhost")
.to(to)
.subject("My third e-mail")
.text("Hello world")
.build()
.unwrap()
.into();
// connect
let mut c = session(to);
let mbox = "INBOX";
c.select(mbox).unwrap();
// append
let date = FixedOffset::east(8 * 3600)
.ymd(2020, 12, 13)
.and_hms(13, 36, 36);
c.append(mbox, e.message_to_string().unwrap().as_bytes())
.flag(Flag::Seen)
.flag(Flag::Flagged)
.internal_date(date)
.finish()
.unwrap();
// now we should see the e-mail!
let inbox = c.uid_search("ALL").unwrap();
// and the one message should have the first message sequence number
assert_eq!(inbox.len(), 1);
let uid = inbox.into_iter().next().unwrap();
// fetch the e-mail
let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap();
assert_eq!(fetch.len(), 1);
let fetch = &fetch[0];
assert_eq!(fetch.uid, Some(uid));
assert_eq!(fetch.internal_date(), Some(date));
// and let's delete it to clean up
c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)")
.unwrap();
c.expunge().unwrap();
// the e-mail should be gone now
let inbox = c.search("ALL").unwrap();
assert_eq!(inbox.len(), 0);
}