Merge branch 'master' into more-unilaterals
This commit is contained in:
commit
6935c51b2b
10 changed files with 558 additions and 44 deletions
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### 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
|
||||||
20
Cargo.toml
20
Cargo.toml
|
|
@ -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"]
|
||||||
|
|
@ -31,14 +23,14 @@ regex = "1.0"
|
||||||
bufstream = "0.1"
|
bufstream = "0.1"
|
||||||
imap-proto = "0.10.0"
|
imap-proto = "0.10.0"
|
||||||
nom = "5.0"
|
nom = "5.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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -52,7 +55,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- template: install-rust.yml@templates
|
- template: install-rust.yml@templates
|
||||||
parameters:
|
parameters:
|
||||||
rust: 1.40.0
|
rust: 1.40.0 # static-assertions (1.37+) and base64 (1.40+)
|
||||||
- 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
|
||||||
|
|
|
||||||
|
|
@ -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
63
examples/starttls.rs
Normal 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
92
examples/timeout.rs
Normal 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 {}
|
||||||
177
src/client.rs
177
src/client.rs
|
|
@ -1,5 +1,6 @@
|
||||||
use base64;
|
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 nom;
|
||||||
|
|
@ -27,15 +28,74 @@ macro_rules! quote {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait OptionExt<E> {
|
||||||
|
fn err(self) -> std::result::Result<(), E>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> OptionExt<E> for Option<E> {
|
||||||
|
fn err(self) -> std::result::Result<(), E> {
|
||||||
|
match self {
|
||||||
|
Some(e) => Err(e),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the input into what [the IMAP
|
||||||
|
/// grammar](https://tools.ietf.org/html/rfc3501#section-9)
|
||||||
|
/// calls "quoted", which is reachable from "string" et al.
|
||||||
|
/// Also ensure it doesn't contain a colliding command-delimiter (newline).
|
||||||
fn validate_str(value: &str) -> Result<String> {
|
fn validate_str(value: &str) -> Result<String> {
|
||||||
let quoted = quote!(value);
|
validate_str_noquote(value)?;
|
||||||
if quoted.find('\n').is_some() {
|
Ok(quote!(value))
|
||||||
return Err(Error::Validate(ValidateError('\n')));
|
}
|
||||||
}
|
|
||||||
if quoted.find('\r').is_some() {
|
/// 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
|
||||||
|
|
@ -240,6 +300,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 +539,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,13 +602,17 @@ impl<T: Read + Write> Session<T> {
|
||||||
S1: AsRef<str>,
|
S1: AsRef<str>,
|
||||||
S2: AsRef<str>,
|
S2: AsRef<str>,
|
||||||
{
|
{
|
||||||
|
if sequence_set.as_ref().is_empty() {
|
||||||
|
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
|
||||||
|
} else {
|
||||||
self.run_command_and_read_response(&format!(
|
self.run_command_and_read_response(&format!(
|
||||||
"FETCH {} {}",
|
"FETCH {} {}",
|
||||||
sequence_set.as_ref(),
|
validate_sequence_set(sequence_set.as_ref())?,
|
||||||
query.as_ref()
|
validate_str_noquote(query.as_ref())?
|
||||||
))
|
))
|
||||||
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
|
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
|
/// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
|
||||||
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
/// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
|
||||||
|
|
@ -534,13 +621,17 @@ impl<T: Read + Write> Session<T> {
|
||||||
S1: AsRef<str>,
|
S1: AsRef<str>,
|
||||||
S2: AsRef<str>,
|
S2: AsRef<str>,
|
||||||
{
|
{
|
||||||
|
if uid_set.as_ref().is_empty() {
|
||||||
|
parse_fetches(vec![], &mut self.unsolicited_responses_tx)
|
||||||
|
} else {
|
||||||
self.run_command_and_read_response(&format!(
|
self.run_command_and_read_response(&format!(
|
||||||
"UID FETCH {} {}",
|
"UID FETCH {} {}",
|
||||||
uid_set.as_ref(),
|
validate_sequence_set(uid_set.as_ref())?,
|
||||||
query.as_ref()
|
validate_str_noquote(query.as_ref())?
|
||||||
))
|
))
|
||||||
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
|
.and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Noop always succeeds, and it does nothing.
|
/// Noop always succeeds, and it does nothing.
|
||||||
pub fn noop(&mut self) -> Result<()> {
|
pub fn noop(&mut self) -> Result<()> {
|
||||||
|
|
@ -1055,10 +1146,68 @@ impl<T: Read + Write> Session<T> {
|
||||||
/// `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<()> {
|
pub fn append<S: AsRef<str>, B: AsRef<[u8]>>(&mut self, mailbox: S, content: B) -> Result<()> {
|
||||||
|
self.append_with_flags(mailbox, content, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 append_with_flags<S: AsRef<str>, B: AsRef<[u8]>>(
|
||||||
|
&mut self,
|
||||||
|
mailbox: S,
|
||||||
|
content: B,
|
||||||
|
flags: &[Flag<'_>],
|
||||||
|
) -> Result<()> {
|
||||||
|
self.append_with_flags_and_date(mailbox, content, flags, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`.
|
||||||
|
///
|
||||||
|
/// 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 append_with_flags_and_date<S: AsRef<str>, B: AsRef<[u8]>>(
|
||||||
|
&mut self,
|
||||||
|
mailbox: S,
|
||||||
|
content: B,
|
||||||
|
flags: &[Flag<'_>],
|
||||||
|
date: impl Into<Option<DateTime<FixedOffset>>>,
|
||||||
|
) -> Result<()> {
|
||||||
let content = content.as_ref();
|
let content = content.as_ref();
|
||||||
|
let flagstr = flags
|
||||||
|
.iter()
|
||||||
|
.filter(|f| **f != Flag::Recent)
|
||||||
|
.map(|f| f.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ");
|
||||||
|
let datestr = match date.into() {
|
||||||
|
Some(date) => format!(" \"{}\"", date.format("%d-%h-%Y %T %z")),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
self.run_command(&format!(
|
self.run_command(&format!(
|
||||||
"APPEND \"{}\" {{{}}}",
|
"APPEND \"{}\" ({}){} {{{}}}",
|
||||||
mailbox.as_ref(),
|
mailbox.as_ref(),
|
||||||
|
flagstr,
|
||||||
|
datestr,
|
||||||
content.len()
|
content.len()
|
||||||
))?;
|
))?;
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,153 @@ 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()).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: &[Flag] = &[Flag::Seen, Flag::Flagged];
|
||||||
|
c.append_with_flags(mbox, e.message_to_string().unwrap(), flags)
|
||||||
|
.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 flags: &[Flag] = &[Flag::Seen, Flag::Flagged];
|
||||||
|
let date = FixedOffset::east(8 * 3600)
|
||||||
|
.ymd(2020, 12, 13)
|
||||||
|
.and_hms(13, 36, 36);
|
||||||
|
c.append_with_flags_and_date(mbox, e.message_to_string().unwrap(), flags, Some(date))
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue