up
This commit is contained in:
commit
48477b8ff2
23 changed files with 4555 additions and 0 deletions
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "imap-proto"
|
||||||
|
version = "0.14.3"
|
||||||
|
authors = ["Dirkjan Ochtman <dirkjan@ochtman.nl>"]
|
||||||
|
description = "IMAP protocol parser and data structures"
|
||||||
|
documentation = "https://docs.rs/imap-proto"
|
||||||
|
keywords = ["imap", "email"]
|
||||||
|
categories = ["email", "network-programming", "parser-implementations"]
|
||||||
|
homepage = "https://github.com/djc/tokio-imap"
|
||||||
|
repository = "https://github.com/djc/tokio-imap"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
readme = "../README.md"
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "passively-maintained" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nom = { version = "6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert_matches = "1.3"
|
31
examples/parse_response.rs
Normal file
31
examples/parse_response.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use imap_proto::Response;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
loop {
|
||||||
|
let line = {
|
||||||
|
print!("Enter IMAP4REV1 response: ");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
std::io::stdin().read_line(&mut line)?;
|
||||||
|
line
|
||||||
|
};
|
||||||
|
|
||||||
|
match Response::from_bytes(line.replace("\n", "\r\n").as_bytes()) {
|
||||||
|
Ok((remaining, command)) => {
|
||||||
|
println!("{:#?}", command);
|
||||||
|
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
println!("Remaining data in buffer: {:?}", remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("Error parsing the response. Is it correct? Exiting.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
4
fuzz/.gitignore
vendored
Normal file
4
fuzz/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
21
fuzz/Cargo.toml
Normal file
21
fuzz/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "imap-proto-fuzz"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = ["Automatically generated"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies.imap-proto]
|
||||||
|
path = ".."
|
||||||
|
[dependencies.libfuzzer-sys]
|
||||||
|
git = "https://github.com/rust-fuzz/libfuzzer-sys.git"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "utf8_parse_response"
|
||||||
|
path = "fuzz_targets/utf8_parse_response.rs"
|
||||||
|
|
||||||
|
# Prevent this from interfering with workspaces
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
8
fuzz/fuzz_targets/utf8_parse_response.rs
Normal file
8
fuzz/fuzz_targets/utf8_parse_response.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#![no_main]
|
||||||
|
#[macro_use] extern crate libfuzzer_sys;
|
||||||
|
extern crate imap_proto;
|
||||||
|
|
||||||
|
// UTF-8
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
let _ = imap_proto::Response::from_bytes(data);
|
||||||
|
});
|
387
src/builders/command.rs
Normal file
387
src/builders/command.rs
Normal file
|
@ -0,0 +1,387 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::ops::{RangeFrom, RangeInclusive};
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
use crate::types::{AttrMacro, Attribute, State};
|
||||||
|
|
||||||
|
pub struct CommandBuilder {}
|
||||||
|
|
||||||
|
impl CommandBuilder {
|
||||||
|
pub fn check() -> Command {
|
||||||
|
let args = b"CHECK".to_vec();
|
||||||
|
Command {
|
||||||
|
args,
|
||||||
|
next_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close() -> Command {
|
||||||
|
let args = b"CLOSE".to_vec();
|
||||||
|
Command {
|
||||||
|
args,
|
||||||
|
next_state: Some(State::Authenticated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn examine(mailbox: &str) -> SelectCommand<select::NoParams> {
|
||||||
|
let args = format!("EXAMINE \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
|
||||||
|
SelectCommand {
|
||||||
|
args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch() -> FetchCommand<fetch::Empty> {
|
||||||
|
FetchCommand {
|
||||||
|
args: b"FETCH ".to_vec(),
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(reference: &str, glob: &str) -> Command {
|
||||||
|
let args = format!(
|
||||||
|
"LIST \"{}\" \"{}\"",
|
||||||
|
quoted_string(reference).unwrap(),
|
||||||
|
quoted_string(glob).unwrap()
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
Command {
|
||||||
|
args,
|
||||||
|
next_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(user_name: &str, password: &str) -> Command {
|
||||||
|
let args = format!(
|
||||||
|
"LOGIN \"{}\" \"{}\"",
|
||||||
|
quoted_string(user_name).unwrap(),
|
||||||
|
quoted_string(password).unwrap()
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
Command {
|
||||||
|
args,
|
||||||
|
next_state: Some(State::Authenticated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(mailbox: &str) -> SelectCommand<select::NoParams> {
|
||||||
|
let args = format!("SELECT \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
|
||||||
|
SelectCommand {
|
||||||
|
args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uid_fetch() -> FetchCommand<fetch::Empty> {
|
||||||
|
FetchCommand {
|
||||||
|
args: b"UID FETCH ".to_vec(),
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Command {
|
||||||
|
pub args: Vec<u8>,
|
||||||
|
pub next_state: Option<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SelectCommand<T> {
|
||||||
|
args: Vec<u8>,
|
||||||
|
state: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectCommand<select::NoParams> {
|
||||||
|
// RFC 4551 CONDSTORE parameter (based on RFC 4466 `select-param`)
|
||||||
|
pub fn cond_store(mut self) -> SelectCommand<select::Params> {
|
||||||
|
self.args.extend(b" (CONDSTORE");
|
||||||
|
SelectCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SelectCommand<select::NoParams>> for Command {
|
||||||
|
fn from(cmd: SelectCommand<select::NoParams>) -> Command {
|
||||||
|
Command {
|
||||||
|
args: cmd.args,
|
||||||
|
next_state: Some(State::Selected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SelectCommand<select::Params>> for Command {
|
||||||
|
fn from(mut cmd: SelectCommand<select::Params>) -> Command {
|
||||||
|
cmd.args.push(b')');
|
||||||
|
Command {
|
||||||
|
args: cmd.args,
|
||||||
|
next_state: Some(State::Selected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod select {
|
||||||
|
pub struct NoParams;
|
||||||
|
pub struct Params;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch {
|
||||||
|
pub struct Empty;
|
||||||
|
pub struct Messages;
|
||||||
|
pub struct Attributes;
|
||||||
|
pub struct Modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FetchCommand<T> {
|
||||||
|
args: Vec<u8>,
|
||||||
|
state: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchCommand<fetch::Empty> {
|
||||||
|
pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
|
||||||
|
sequence_num(&mut self.args, num);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
|
||||||
|
sequence_range(&mut self.args, range);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
|
||||||
|
range_from(&mut self.args, range);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchCommand<fetch::Messages> {
|
||||||
|
pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
|
||||||
|
self.args.extend(b",");
|
||||||
|
sequence_num(&mut self.args, num);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
|
||||||
|
self.args.extend(b",");
|
||||||
|
sequence_range(&mut self.args, range);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
|
||||||
|
self.args.extend(b",");
|
||||||
|
range_from(&mut self.args, range);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attr_macro(mut self, named: AttrMacro) -> FetchCommand<fetch::Modifiers> {
|
||||||
|
self.args.push(b' ');
|
||||||
|
self.args.extend(
|
||||||
|
match named {
|
||||||
|
AttrMacro::All => "ALL",
|
||||||
|
AttrMacro::Fast => "FAST",
|
||||||
|
AttrMacro::Full => "FULL",
|
||||||
|
}
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
|
||||||
|
self.args.extend(b" (");
|
||||||
|
push_attr(&mut self.args, attr);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sequence_num(cmd: &mut Vec<u8>, num: u32) {
|
||||||
|
cmd.extend(num.to_string().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sequence_range(cmd: &mut Vec<u8>, range: RangeInclusive<u32>) {
|
||||||
|
cmd.extend(range.start().to_string().as_bytes());
|
||||||
|
cmd.push(b':');
|
||||||
|
cmd.extend(range.end().to_string().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from(cmd: &mut Vec<u8>, range: RangeFrom<u32>) {
|
||||||
|
cmd.extend(range.start.to_string().as_bytes());
|
||||||
|
cmd.extend(b":*");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchCommand<fetch::Attributes> {
|
||||||
|
pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
|
||||||
|
self.args.push(b' ');
|
||||||
|
push_attr(&mut self.args, attr);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
|
||||||
|
self.args.push(b')');
|
||||||
|
changed_since(&mut self.args, seq);
|
||||||
|
FetchCommand {
|
||||||
|
args: self.args,
|
||||||
|
state: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_attr(cmd: &mut Vec<u8>, attr: Attribute) {
|
||||||
|
cmd.extend(
|
||||||
|
match attr {
|
||||||
|
Attribute::Body => "BODY",
|
||||||
|
Attribute::Envelope => "ENVELOPE",
|
||||||
|
Attribute::Flags => "FLAGS",
|
||||||
|
Attribute::InternalDate => "INTERNALDATE",
|
||||||
|
Attribute::ModSeq => "MODSEQ",
|
||||||
|
Attribute::Rfc822 => "RFC822",
|
||||||
|
Attribute::Rfc822Size => "RFC822.SIZE",
|
||||||
|
Attribute::Rfc822Text => "RFC822.TEXT",
|
||||||
|
Attribute::Uid => "UID",
|
||||||
|
}
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchCommand<fetch::Attributes>> for Command {
|
||||||
|
fn from(mut cmd: FetchCommand<fetch::Attributes>) -> Command {
|
||||||
|
cmd.args.push(b')');
|
||||||
|
Command {
|
||||||
|
args: cmd.args,
|
||||||
|
next_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchCommand<fetch::Modifiers>> for Command {
|
||||||
|
fn from(cmd: FetchCommand<fetch::Modifiers>) -> Command {
|
||||||
|
Command {
|
||||||
|
args: cmd.args,
|
||||||
|
next_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchCommand<fetch::Modifiers> {
|
||||||
|
pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
|
||||||
|
changed_since(&mut self.args, seq);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed_since(cmd: &mut Vec<u8>, seq: u64) {
|
||||||
|
cmd.extend(b" (CHANGEDSINCE ");
|
||||||
|
cmd.extend(seq.to_string().as_bytes());
|
||||||
|
cmd.push(b')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an escaped string if necessary for use as a "quoted" string per
|
||||||
|
/// the IMAPv4 RFC. Return value does not include surrounding quote characters.
|
||||||
|
/// Will return Err if the argument contains illegal characters.
|
||||||
|
///
|
||||||
|
/// Relevant definitions from RFC 3501 formal syntax:
|
||||||
|
///
|
||||||
|
/// string = quoted / literal [literal elided here]
|
||||||
|
/// quoted = DQUOTE *QUOTED-CHAR DQUOTE
|
||||||
|
/// QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
|
||||||
|
/// quoted-specials = DQUOTE / "\"
|
||||||
|
/// TEXT-CHAR = <any CHAR except CR and LF>
|
||||||
|
fn quoted_string(s: &str) -> Result<Cow<str>, &'static str> {
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
let (mut start, mut new) = (0, Vec::<u8>::new());
|
||||||
|
for (i, b) in bytes.iter().enumerate() {
|
||||||
|
match *b {
|
||||||
|
b'\r' | b'\n' => {
|
||||||
|
return Err("CR and LF not allowed in quoted strings");
|
||||||
|
}
|
||||||
|
b'\\' | b'"' => {
|
||||||
|
if start < i {
|
||||||
|
new.extend(&bytes[start..i]);
|
||||||
|
}
|
||||||
|
new.push(b'\\');
|
||||||
|
new.push(*b);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if start == 0 {
|
||||||
|
Ok(Cow::Borrowed(s))
|
||||||
|
} else {
|
||||||
|
if start < bytes.len() {
|
||||||
|
new.extend(&bytes[start..]);
|
||||||
|
}
|
||||||
|
// Since the argument is a str, it must contain valid UTF-8. Since
|
||||||
|
// this function's transformation preserves the UTF-8 validity,
|
||||||
|
// unwrapping here should be okay.
|
||||||
|
Ok(Cow::Owned(String::from_utf8(new).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{quoted_string, Attribute, Command, CommandBuilder};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login() {
|
||||||
|
assert_eq!(
|
||||||
|
CommandBuilder::login("djc", "s3cr3t").args,
|
||||||
|
b"LOGIN \"djc\" \"s3cr3t\""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
CommandBuilder::login("djc", "domain\\password").args,
|
||||||
|
b"LOGIN \"djc\" \"domain\\\\password\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select() {
|
||||||
|
let cmd = Command::from(CommandBuilder::select("INBOX"));
|
||||||
|
assert_eq!(&cmd.args, br#"SELECT "INBOX""#);
|
||||||
|
let cmd = Command::from(CommandBuilder::examine("INBOX").cond_store());
|
||||||
|
assert_eq!(&cmd.args, br#"EXAMINE "INBOX" (CONDSTORE)"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch() {
|
||||||
|
let cmd: Command = CommandBuilder::fetch()
|
||||||
|
.range_from(1..)
|
||||||
|
.attr(Attribute::Uid)
|
||||||
|
.attr(Attribute::ModSeq)
|
||||||
|
.changed_since(13)
|
||||||
|
.into();
|
||||||
|
assert_eq!(cmd.args, &b"FETCH 1:* (UID MODSEQ) (CHANGEDSINCE 13)"[..]);
|
||||||
|
|
||||||
|
let cmd: Command = CommandBuilder::fetch()
|
||||||
|
.num(1)
|
||||||
|
.num(2)
|
||||||
|
.attr(Attribute::Uid)
|
||||||
|
.attr(Attribute::ModSeq)
|
||||||
|
.into();
|
||||||
|
assert_eq!(cmd.args, &b"FETCH 1,2 (UID MODSEQ)"[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quoted_string() {
|
||||||
|
assert_eq!(quoted_string("a").unwrap(), "a");
|
||||||
|
assert_eq!(quoted_string("").unwrap(), "");
|
||||||
|
assert_eq!(quoted_string("a\"b\\c").unwrap(), "a\\\"b\\\\c");
|
||||||
|
assert_eq!(quoted_string("\"foo\\").unwrap(), "\\\"foo\\\\");
|
||||||
|
assert!(quoted_string("\n").is_err());
|
||||||
|
}
|
||||||
|
}
|
1
src/builders/mod.rs
Normal file
1
src/builders/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod command;
|
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod builders;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use crate::parser::ParseResult;
|
||||||
|
pub use crate::types::*;
|
76
src/parser/bodystructure.rs
Normal file
76
src/parser/bodystructure.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::types::BodyStructure;
|
||||||
|
/// An utility parser helping to find the appropriate
|
||||||
|
/// section part from a FETCH response.
|
||||||
|
pub struct BodyStructParser<'a> {
|
||||||
|
root: &'a BodyStructure<'a>,
|
||||||
|
prefix: Vec<u32>,
|
||||||
|
iter: u32,
|
||||||
|
map: HashMap<Vec<u32>, &'a BodyStructure<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyStructParser<'a> {
|
||||||
|
/// Returns a new parser
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `root` - The root of the `BodyStructure response.
|
||||||
|
pub fn new(root: &'a BodyStructure<'a>) -> Self {
|
||||||
|
let mut parser = BodyStructParser {
|
||||||
|
root,
|
||||||
|
prefix: vec![],
|
||||||
|
iter: 1,
|
||||||
|
map: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
parser.parse(parser.root);
|
||||||
|
parser
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search particular element within the bodystructure.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `func` - The filter used to search elements within the bodystructure.
|
||||||
|
pub fn search<F>(&self, func: F) -> Option<Vec<u32>>
|
||||||
|
where
|
||||||
|
F: Fn(&'a BodyStructure<'a>) -> bool,
|
||||||
|
{
|
||||||
|
let elem: Vec<_> = self
|
||||||
|
.map
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(k, v)| {
|
||||||
|
if func(*v) {
|
||||||
|
let slice: &[u32] = k;
|
||||||
|
Some(slice)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
elem.first().map(|a| a.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reetr
|
||||||
|
fn parse(&mut self, node: &'a BodyStructure) {
|
||||||
|
match node {
|
||||||
|
BodyStructure::Multipart { bodies, .. } => {
|
||||||
|
let vec = self.prefix.clone();
|
||||||
|
self.map.insert(vec, node);
|
||||||
|
|
||||||
|
for (i, n) in bodies.iter().enumerate() {
|
||||||
|
self.iter += i as u32;
|
||||||
|
self.prefix.push(self.iter);
|
||||||
|
self.parse(n);
|
||||||
|
self.prefix.pop();
|
||||||
|
}
|
||||||
|
self.iter = 1;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let vec = self.prefix.clone();
|
||||||
|
self.map.insert(vec, node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
331
src/parser/core.rs
Normal file
331
src/parser/core.rs
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{escaped, tag, tag_no_case, take, take_while, take_while1},
|
||||||
|
character::streaming::{char, digit1, one_of},
|
||||||
|
combinator::{map, map_res},
|
||||||
|
multi::{separated_list0, separated_list1},
|
||||||
|
sequence::{delimited, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::str::{from_utf8, FromStr};
|
||||||
|
|
||||||
|
// ----- number -----
|
||||||
|
|
||||||
|
// number = 1*DIGIT
|
||||||
|
// ; Unsigned 32-bit integer
|
||||||
|
// ; (0 <= n < 4,294,967,296)
|
||||||
|
pub fn number(i: &[u8]) -> IResult<&[u8], u32> {
|
||||||
|
let (i, bytes) = digit1(i)?;
|
||||||
|
match from_utf8(bytes).ok().and_then(|s| u32::from_str(s).ok()) {
|
||||||
|
Some(v) => Ok((i, v)),
|
||||||
|
None => Err(nom::Err::Error(nom::error::make_error(
|
||||||
|
i,
|
||||||
|
nom::error::ErrorKind::ParseTo,
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// same as `number` but 64-bit
|
||||||
|
pub fn number_64(i: &[u8]) -> IResult<&[u8], u64> {
|
||||||
|
let (i, bytes) = digit1(i)?;
|
||||||
|
match from_utf8(bytes).ok().and_then(|s| u64::from_str(s).ok()) {
|
||||||
|
Some(v) => Ok((i, v)),
|
||||||
|
None => Err(nom::Err::Error(nom::error::make_error(
|
||||||
|
i,
|
||||||
|
nom::error::ErrorKind::ParseTo,
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// seq-range = seq-number ":" seq-number
|
||||||
|
// ; two seq-number values and all values between
|
||||||
|
// ; these two regardless of order.
|
||||||
|
// ; seq-number is a nz-number
|
||||||
|
pub fn sequence_range(i: &[u8]) -> IResult<&[u8], std::ops::RangeInclusive<u32>> {
|
||||||
|
map(tuple((number, tag(":"), number)), |(s, _, e)| s..=e)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sequence-set = (seq-number / seq-range) *("," sequence-set)
|
||||||
|
// ; set of seq-number values, regardless of order.
|
||||||
|
// ; Servers MAY coalesce overlaps and/or execute the
|
||||||
|
// ; sequence in any order.
|
||||||
|
pub fn sequence_set(i: &[u8]) -> IResult<&[u8], Vec<std::ops::RangeInclusive<u32>>> {
|
||||||
|
separated_list1(tag(","), alt((sequence_range, map(number, |n| n..=n))))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- string -----
|
||||||
|
|
||||||
|
// string = quoted / literal
|
||||||
|
pub fn string(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
alt((quoted, literal))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// string bytes as utf8
|
||||||
|
pub fn string_utf8(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(string, from_utf8)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoted = DQUOTE *QUOTED-CHAR DQUOTE
|
||||||
|
pub fn quoted(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
delimited(
|
||||||
|
char('"'),
|
||||||
|
escaped(
|
||||||
|
take_while1(|byte| is_text_char(byte) && !is_quoted_specials(byte)),
|
||||||
|
'\\',
|
||||||
|
one_of("\\\""),
|
||||||
|
),
|
||||||
|
char('"'),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoted bytes as utf8
|
||||||
|
pub fn quoted_utf8(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(quoted, from_utf8)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoted-specials = DQUOTE / "\"
|
||||||
|
pub fn is_quoted_specials(c: u8) -> bool {
|
||||||
|
c == b'"' || c == b'\\'
|
||||||
|
}
|
||||||
|
|
||||||
|
/// literal = "{" number "}" CRLF *CHAR8
|
||||||
|
/// ; Number represents the number of CHAR8s
|
||||||
|
pub fn literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
let mut parser = tuple((tag(b"{"), number, tag(b"}"), tag("\r\n")));
|
||||||
|
|
||||||
|
let (remaining, (_, count, _, _)) = parser(input)?;
|
||||||
|
|
||||||
|
let (remaining, data) = take(count)(remaining)?;
|
||||||
|
|
||||||
|
if !data.iter().all(|byte| is_char8(*byte)) {
|
||||||
|
// FIXME: what ErrorKind should this have?
|
||||||
|
return Err(nom::Err::Error(nom::error::Error::new(
|
||||||
|
remaining,
|
||||||
|
nom::error::ErrorKind::Verify,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((remaining, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CHAR8 = %x01-ff ; any OCTET except NUL, %x00
|
||||||
|
pub fn is_char8(i: u8) -> bool {
|
||||||
|
i != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- astring ----- atom (roughly) or string
|
||||||
|
|
||||||
|
// astring = 1*ASTRING-CHAR / string
|
||||||
|
pub fn astring(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
alt((take_while1(is_astring_char), string))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// astring bytes as utf8
|
||||||
|
pub fn astring_utf8(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(astring, from_utf8)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASTRING-CHAR = ATOM-CHAR / resp-specials
|
||||||
|
pub fn is_astring_char(c: u8) -> bool {
|
||||||
|
is_atom_char(c) || is_resp_specials(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ATOM-CHAR = <any CHAR except atom-specials>
|
||||||
|
pub fn is_atom_char(c: u8) -> bool {
|
||||||
|
is_char(c) && !is_atom_specials(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials
|
||||||
|
pub fn is_atom_specials(c: u8) -> bool {
|
||||||
|
c == b'('
|
||||||
|
|| c == b')'
|
||||||
|
|| c == b'{'
|
||||||
|
|| c == b' '
|
||||||
|
|| c < 32
|
||||||
|
|| is_list_wildcards(c)
|
||||||
|
|| is_quoted_specials(c)
|
||||||
|
|| is_resp_specials(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resp-specials = "]"
|
||||||
|
pub fn is_resp_specials(c: u8) -> bool {
|
||||||
|
c == b']'
|
||||||
|
}
|
||||||
|
|
||||||
|
// atom = 1*ATOM-CHAR
|
||||||
|
pub fn atom(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(take_while1(is_atom_char), from_utf8)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- nstring ----- nil or string
|
||||||
|
|
||||||
|
// nstring = string / nil
|
||||||
|
pub fn nstring(i: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
|
||||||
|
alt((map(nil, |_| None), map(string, Some)))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nstring bytes as utf8
|
||||||
|
pub fn nstring_utf8(i: &[u8]) -> IResult<&[u8], Option<&str>> {
|
||||||
|
alt((map(nil, |_| None), map(string_utf8, Some)))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nil = "NIL"
|
||||||
|
pub fn nil(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
tag_no_case("NIL")(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- text -----
|
||||||
|
|
||||||
|
// text = 1*TEXT-CHAR
|
||||||
|
pub fn text(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(take_while(is_text_char), from_utf8)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEXT-CHAR = <any CHAR except CR and LF>
|
||||||
|
pub fn is_text_char(c: u8) -> bool {
|
||||||
|
is_char(c) && c != b'\r' && c != b'\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHAR = %x01-7F
|
||||||
|
// ; any 7-bit US-ASCII character,
|
||||||
|
// ; excluding NUL
|
||||||
|
// From RFC5234
|
||||||
|
pub fn is_char(c: u8) -> bool {
|
||||||
|
matches!(c, 0x01..=0x7F)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- others -----
|
||||||
|
|
||||||
|
// list-wildcards = "%" / "*"
|
||||||
|
pub fn is_list_wildcards(c: u8) -> bool {
|
||||||
|
c == b'%' || c == b'*'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paren_delimited<'a, F, O, E>(f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>
|
||||||
|
where
|
||||||
|
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
|
||||||
|
E: nom::error::ParseError<&'a [u8]>,
|
||||||
|
{
|
||||||
|
delimited(char('('), f, char(')'))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parenthesized_nonempty_list<'a, F, O, E>(
|
||||||
|
f: F,
|
||||||
|
) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Vec<O>, E>
|
||||||
|
where
|
||||||
|
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
|
||||||
|
E: nom::error::ParseError<&'a [u8]>,
|
||||||
|
{
|
||||||
|
delimited(char('('), separated_list1(char(' '), f), char(')'))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parenthesized_list<'a, F, O, E>(f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Vec<O>, E>
|
||||||
|
where
|
||||||
|
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
|
||||||
|
E: nom::error::ParseError<&'a [u8]>,
|
||||||
|
{
|
||||||
|
delimited(char('('), separated_list0(char(' '), f), char(')'))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn opt_opt<'a, F, O, E>(mut f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Option<O>, E>
|
||||||
|
where
|
||||||
|
F: FnMut(&'a [u8]) -> IResult<&'a [u8], Option<O>, E>,
|
||||||
|
{
|
||||||
|
move |i: &[u8]| match f(i) {
|
||||||
|
Ok((i, o)) => Ok((i, o)),
|
||||||
|
Err(nom::Err::Error(_)) => Ok((i, None)),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quoted() {
|
||||||
|
let (rem, val) = quoted(br#""Hello"???"#).unwrap();
|
||||||
|
assert_eq!(rem, b"???");
|
||||||
|
assert_eq!(val, b"Hello");
|
||||||
|
|
||||||
|
// Allowed escapes...
|
||||||
|
assert!(quoted(br#""Hello \" "???"#).is_ok());
|
||||||
|
assert!(quoted(br#""Hello \\ "???"#).is_ok());
|
||||||
|
|
||||||
|
// Not allowed escapes...
|
||||||
|
assert!(quoted(br#""Hello \a "???"#).is_err());
|
||||||
|
assert!(quoted(br#""Hello \z "???"#).is_err());
|
||||||
|
assert!(quoted(br#""Hello \? "???"#).is_err());
|
||||||
|
|
||||||
|
let (rem, val) = quoted(br#""Hello \"World\""???"#).unwrap();
|
||||||
|
assert_eq!(rem, br#"???"#);
|
||||||
|
// Should it be this (Hello \"World\") ...
|
||||||
|
assert_eq!(val, br#"Hello \"World\""#);
|
||||||
|
// ... or this (Hello "World")?
|
||||||
|
//assert_eq!(val, br#"Hello "World""#); // fails
|
||||||
|
|
||||||
|
// Test Incomplete
|
||||||
|
assert_matches!(quoted(br#""#), Err(nom::Err::Incomplete(_)));
|
||||||
|
assert_matches!(quoted(br#""\"#), Err(nom::Err::Incomplete(_)));
|
||||||
|
assert_matches!(quoted(br#""Hello "#), Err(nom::Err::Incomplete(_)));
|
||||||
|
|
||||||
|
// Test Error
|
||||||
|
assert_matches!(quoted(br#"\"#), Err(nom::Err::Error(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_string_literal() {
|
||||||
|
match string(b"{3}\r\nXYZ") {
|
||||||
|
Ok((_, value)) => {
|
||||||
|
assert_eq!(value, b"XYZ");
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_astring() {
|
||||||
|
match astring(b"text ") {
|
||||||
|
Ok((_, value)) => {
|
||||||
|
assert_eq!(value, b"text");
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sequence_range() {
|
||||||
|
match sequence_range(b"23:28 ") {
|
||||||
|
Ok((_, value)) => {
|
||||||
|
assert_eq!(*value.start(), 23);
|
||||||
|
assert_eq!(*value.end(), 28);
|
||||||
|
assert_eq!(value.collect::<Vec<u32>>(), vec![23, 24, 25, 26, 27, 28]);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sequence_set() {
|
||||||
|
match sequence_set(b"1,2:8,10,15:30 ") {
|
||||||
|
Ok((_, value)) => {
|
||||||
|
assert_eq!(value.len(), 4);
|
||||||
|
let v = &value[0];
|
||||||
|
assert_eq!(*v.start(), 1);
|
||||||
|
assert_eq!(*v.end(), 1);
|
||||||
|
let v = &value[1];
|
||||||
|
assert_eq!(*v.start(), 2);
|
||||||
|
assert_eq!(*v.end(), 8);
|
||||||
|
let v = &value[2];
|
||||||
|
assert_eq!(*v.start(), 10);
|
||||||
|
assert_eq!(*v.end(), 10);
|
||||||
|
let v = &value[3];
|
||||||
|
assert_eq!(*v.start(), 15);
|
||||||
|
assert_eq!(*v.end(), 30);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/parser/mod.rs
Normal file
27
src/parser/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::types::Response;
|
||||||
|
use nom::{branch::alt, IResult};
|
||||||
|
|
||||||
|
pub mod core;
|
||||||
|
|
||||||
|
pub mod bodystructure;
|
||||||
|
pub mod rfc2087;
|
||||||
|
pub mod rfc3501;
|
||||||
|
pub mod rfc4315;
|
||||||
|
pub mod rfc4551;
|
||||||
|
pub mod rfc5161;
|
||||||
|
pub mod rfc5256;
|
||||||
|
pub mod rfc5464;
|
||||||
|
pub mod rfc7162;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub fn parse_response(msg: &[u8]) -> ParseResult {
|
||||||
|
alt((
|
||||||
|
rfc3501::continue_req,
|
||||||
|
rfc3501::response_data,
|
||||||
|
rfc3501::response_tagged,
|
||||||
|
))(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ParseResult<'a> = IResult<&'a [u8], Response<'a>>;
|
279
src/parser/rfc2087.rs
Normal file
279
src/parser/rfc2087.rs
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc2087
|
||||||
|
//!
|
||||||
|
//! IMAP4 QUOTA extension
|
||||||
|
//!
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
character::streaming::space1,
|
||||||
|
combinator::map,
|
||||||
|
multi::many0,
|
||||||
|
multi::separated_list0,
|
||||||
|
sequence::{delimited, preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::core::astring_utf8;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
use super::core::number_64;
|
||||||
|
|
||||||
|
/// 5.1. QUOTA Response
|
||||||
|
/// ```ignore
|
||||||
|
/// quota_response ::= "QUOTA" SP astring SP quota_list
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn quota(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
let (rest, (_, _, root_name, _, resources)) = tuple((
|
||||||
|
tag_no_case("QUOTA"),
|
||||||
|
space1,
|
||||||
|
map(astring_utf8, Cow::Borrowed),
|
||||||
|
space1,
|
||||||
|
quota_list,
|
||||||
|
))(i)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
rest,
|
||||||
|
Response::Quota(Quota {
|
||||||
|
root_name,
|
||||||
|
resources,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ```ignore
|
||||||
|
/// quota_list ::= "(" #quota_resource ")"
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn quota_list(i: &[u8]) -> IResult<&[u8], Vec<QuotaResource>> {
|
||||||
|
delimited(tag("("), separated_list0(space1, quota_resource), tag(")"))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ```ignore
|
||||||
|
/// quota_resource ::= atom SP number SP number
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn quota_resource(i: &[u8]) -> IResult<&[u8], QuotaResource> {
|
||||||
|
let (rest, (name, _, usage, _, limit)) = tuple((
|
||||||
|
quota_resource_name,
|
||||||
|
tag(" "),
|
||||||
|
number_64,
|
||||||
|
tag(" "),
|
||||||
|
number_64,
|
||||||
|
))(i)?;
|
||||||
|
|
||||||
|
Ok((rest, QuotaResource { name, usage, limit }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn quota_resource_name(i: &[u8]) -> IResult<&[u8], QuotaResourceName> {
|
||||||
|
alt((
|
||||||
|
map(tag_no_case("STORAGE"), |_| QuotaResourceName::Storage),
|
||||||
|
map(tag_no_case("MESSAGE"), |_| QuotaResourceName::Message),
|
||||||
|
map(map(astring_utf8, Cow::Borrowed), QuotaResourceName::Atom),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5.2. QUOTAROOT Response
|
||||||
|
/// ```ignore
|
||||||
|
/// quotaroot_response ::= "QUOTAROOT" SP astring *(SP astring)
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn quota_root(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
let (rest, (_, _, mailbox_name, quota_root_names)) = tuple((
|
||||||
|
tag_no_case("QUOTAROOT"),
|
||||||
|
space1,
|
||||||
|
map(astring_utf8, Cow::Borrowed),
|
||||||
|
many0(preceded(space1, map(astring_utf8, Cow::Borrowed))),
|
||||||
|
))(i)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
rest,
|
||||||
|
Response::QuotaRoot(QuotaRoot {
|
||||||
|
mailbox_name,
|
||||||
|
quota_root_names,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota() {
|
||||||
|
assert_matches!(
|
||||||
|
quota(b"QUOTA \"\" (STORAGE 10 512)"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::Quota(Quota {
|
||||||
|
root_name: Cow::Borrowed(""),
|
||||||
|
resources: vec![QuotaResource {
|
||||||
|
name: QuotaResourceName::Storage,
|
||||||
|
usage: 10,
|
||||||
|
limit: 512
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_response_data() {
|
||||||
|
assert_matches!(
|
||||||
|
crate::parser::rfc3501::response_data(b"* QUOTA \"\" (STORAGE 10 512)\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::Quota(Quota {
|
||||||
|
root_name: Cow::Borrowed(""),
|
||||||
|
resources: vec![QuotaResource {
|
||||||
|
name: QuotaResourceName::Storage,
|
||||||
|
usage: 10,
|
||||||
|
limit: 512
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_list() {
|
||||||
|
assert_matches!(
|
||||||
|
quota_list(b"(STORAGE 10 512)"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
vec![QuotaResource {
|
||||||
|
name: QuotaResourceName::Storage,
|
||||||
|
usage: 10,
|
||||||
|
limit: 512
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quota_list(b"(MESSAGE 100 512)"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
vec![QuotaResource {
|
||||||
|
name: QuotaResourceName::Message,
|
||||||
|
usage: 100,
|
||||||
|
limit: 512
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quota_list(b"(DAILY 55 200)"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
vec![QuotaResource {
|
||||||
|
name: QuotaResourceName::Atom(Cow::Borrowed("DAILY")),
|
||||||
|
usage: 55,
|
||||||
|
limit: 200
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_root_response_data() {
|
||||||
|
assert_matches!(
|
||||||
|
crate::parser::rfc3501::response_data("* QUOTAROOT INBOX \"\"\r\n".as_bytes()),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("INBOX"),
|
||||||
|
quota_root_names: vec![Cow::Borrowed("")]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminated_quota_root(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
nom::sequence::terminated(quota_root, nom::bytes::streaming::tag("\r\n"))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_root_without_root_names() {
|
||||||
|
assert_matches!(
|
||||||
|
terminated_quota_root(b"QUOTAROOT comp.mail.mime\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("comp.mail.mime"),
|
||||||
|
quota_root_names: vec![]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quota_root2() {
|
||||||
|
assert_matches!(
|
||||||
|
terminated_quota_root(b"QUOTAROOT INBOX HU\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("INBOX"),
|
||||||
|
quota_root_names: vec![Cow::Borrowed("HU")]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
terminated_quota_root(b"QUOTAROOT INBOX \"\"\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("INBOX"),
|
||||||
|
quota_root_names: vec![Cow::Borrowed("")]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\"\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("Inbox"),
|
||||||
|
quota_root_names: vec![Cow::Borrowed("#Account")]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\" \"#Mailbox\"\r\n"),
|
||||||
|
Ok((_, r)) => {
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Response::QuotaRoot(QuotaRoot{
|
||||||
|
mailbox_name: Cow::Borrowed("Inbox"),
|
||||||
|
quota_root_names: vec![Cow::Borrowed("#Account"), Cow::Borrowed("#Mailbox")]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
src/parser/rfc3501/body.rs
Normal file
72
src/parser/rfc3501/body.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
character::streaming::char,
|
||||||
|
combinator::{map, opt},
|
||||||
|
multi::many0,
|
||||||
|
sequence::{delimited, preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::{parser::core::*, types::*};
|
||||||
|
|
||||||
|
pub fn section_part(i: &[u8]) -> IResult<&[u8], Vec<u32>> {
|
||||||
|
let (i, (part, mut rest)) = tuple((number, many0(preceded(char('.'), number))))(i)?;
|
||||||
|
rest.insert(0, part);
|
||||||
|
Ok((i, rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_msgtext(i: &[u8]) -> IResult<&[u8], MessageSection> {
|
||||||
|
alt((
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
tag_no_case("HEADER.FIELDS"),
|
||||||
|
opt(tag_no_case(".NOT")),
|
||||||
|
tag(" "),
|
||||||
|
parenthesized_list(astring),
|
||||||
|
)),
|
||||||
|
|_| MessageSection::Header,
|
||||||
|
),
|
||||||
|
map(tag_no_case("HEADER"), |_| MessageSection::Header),
|
||||||
|
map(tag_no_case("TEXT"), |_| MessageSection::Text),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_text(i: &[u8]) -> IResult<&[u8], MessageSection> {
|
||||||
|
alt((
|
||||||
|
section_msgtext,
|
||||||
|
map(tag_no_case("MIME"), |_| MessageSection::Mime),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_spec(i: &[u8]) -> IResult<&[u8], SectionPath> {
|
||||||
|
alt((
|
||||||
|
map(section_msgtext, SectionPath::Full),
|
||||||
|
map(
|
||||||
|
tuple((section_part, opt(preceded(char('.'), section_text)))),
|
||||||
|
|(part, text)| SectionPath::Part(part, text),
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section(i: &[u8]) -> IResult<&[u8], Option<SectionPath>> {
|
||||||
|
delimited(char('['), opt(section_spec), char(']'))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msg_att_body_section(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
tag_no_case("BODY"),
|
||||||
|
section,
|
||||||
|
opt(delimited(char('<'), number, char('>'))),
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
)),
|
||||||
|
|(_, section, index, _, data)| AttributeValue::BodySection {
|
||||||
|
section,
|
||||||
|
index,
|
||||||
|
data: data.map(Cow::Borrowed),
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
539
src/parser/rfc3501/body_structure.rs
Normal file
539
src/parser/rfc3501/body_structure.rs
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
character::streaming::char,
|
||||||
|
combinator::{map, opt},
|
||||||
|
multi::many1,
|
||||||
|
sequence::{delimited, preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parser::{core::*, rfc3501::envelope},
|
||||||
|
types::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
// body-fields = body-fld-param SP body-fld-id SP body-fld-desc SP
|
||||||
|
// body-fld-enc SP body-fld-octets
|
||||||
|
fn body_fields(i: &[u8]) -> IResult<&[u8], BodyFields> {
|
||||||
|
let (i, (param, _, id, _, description, _, transfer_encoding, _, octets)) = tuple((
|
||||||
|
body_param,
|
||||||
|
tag(" "),
|
||||||
|
// body id seems to refer to the Message-ID or possibly Content-ID header, which
|
||||||
|
// by the definition in RFC 2822 seems to resolve to all ASCII characters (through
|
||||||
|
// a large amount of indirection which I did not have the patience to fully explore)
|
||||||
|
nstring_utf8,
|
||||||
|
tag(" "),
|
||||||
|
// Per https://tools.ietf.org/html/rfc2045#section-8, description should be all ASCII
|
||||||
|
nstring_utf8,
|
||||||
|
tag(" "),
|
||||||
|
body_encoding,
|
||||||
|
tag(" "),
|
||||||
|
number,
|
||||||
|
))(i)?;
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
BodyFields {
|
||||||
|
param,
|
||||||
|
id: id.map(Cow::Borrowed),
|
||||||
|
description: description.map(Cow::Borrowed),
|
||||||
|
transfer_encoding,
|
||||||
|
octets,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// body-ext-1part = body-fld-md5 [SP body-fld-dsp [SP body-fld-lang
|
||||||
|
// [SP body-fld-loc *(SP body-extension)]]]
|
||||||
|
// ; MUST NOT be returned on non-extensible
|
||||||
|
// ; "BODY" fetch
|
||||||
|
fn body_ext_1part(i: &[u8]) -> IResult<&[u8], BodyExt1Part> {
|
||||||
|
let (i, (md5, disposition, language, location, extension)) = tuple((
|
||||||
|
// Per RFC 1864, MD5 values are base64-encoded
|
||||||
|
opt_opt(preceded(tag(" "), nstring_utf8)),
|
||||||
|
opt_opt(preceded(tag(" "), body_disposition)),
|
||||||
|
opt_opt(preceded(tag(" "), body_lang)),
|
||||||
|
// Location appears to reference a URL, which by RFC 1738 (section 2.2) should be ASCII
|
||||||
|
opt_opt(preceded(tag(" "), nstring_utf8)),
|
||||||
|
opt(preceded(tag(" "), body_extension)),
|
||||||
|
))(i)?;
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
BodyExt1Part {
|
||||||
|
md5: md5.map(Cow::Borrowed),
|
||||||
|
disposition,
|
||||||
|
language,
|
||||||
|
location: location.map(Cow::Borrowed),
|
||||||
|
extension,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// body-ext-mpart = body-fld-param [SP body-fld-dsp [SP body-fld-lang
|
||||||
|
// [SP body-fld-loc *(SP body-extension)]]]
|
||||||
|
// ; MUST NOT be returned on non-extensible
|
||||||
|
// ; "BODY" fetch
|
||||||
|
fn body_ext_mpart(i: &[u8]) -> IResult<&[u8], BodyExtMPart> {
|
||||||
|
let (i, (param, disposition, language, location, extension)) = tuple((
|
||||||
|
opt_opt(preceded(tag(" "), body_param)),
|
||||||
|
opt_opt(preceded(tag(" "), body_disposition)),
|
||||||
|
opt_opt(preceded(tag(" "), body_lang)),
|
||||||
|
// Location appears to reference a URL, which by RFC 1738 (section 2.2) should be ASCII
|
||||||
|
opt_opt(preceded(tag(" "), nstring_utf8)),
|
||||||
|
opt(preceded(tag(" "), body_extension)),
|
||||||
|
))(i)?;
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
BodyExtMPart {
|
||||||
|
param,
|
||||||
|
disposition,
|
||||||
|
language,
|
||||||
|
location: location.map(Cow::Borrowed),
|
||||||
|
extension,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_encoding(i: &[u8]) -> IResult<&[u8], ContentEncoding> {
|
||||||
|
alt((
|
||||||
|
delimited(
|
||||||
|
char('"'),
|
||||||
|
alt((
|
||||||
|
map(tag_no_case("7BIT"), |_| ContentEncoding::SevenBit),
|
||||||
|
map(tag_no_case("8BIT"), |_| ContentEncoding::EightBit),
|
||||||
|
map(tag_no_case("BINARY"), |_| ContentEncoding::Binary),
|
||||||
|
map(tag_no_case("BASE64"), |_| ContentEncoding::Base64),
|
||||||
|
map(tag_no_case("QUOTED-PRINTABLE"), |_| {
|
||||||
|
ContentEncoding::QuotedPrintable
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
char('"'),
|
||||||
|
),
|
||||||
|
map(string_utf8, |enc| {
|
||||||
|
ContentEncoding::Other(Cow::Borrowed(enc))
|
||||||
|
}),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_lang(i: &[u8]) -> IResult<&[u8], Option<Vec<Cow<'_, str>>>> {
|
||||||
|
alt((
|
||||||
|
// body language seems to refer to RFC 3066 language tags, which should be ASCII-only
|
||||||
|
map(nstring_utf8, |v| v.map(|s| vec![Cow::Borrowed(s)])),
|
||||||
|
map(
|
||||||
|
parenthesized_nonempty_list(map(string_utf8, Cow::Borrowed)),
|
||||||
|
Option::from,
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_param(i: &[u8]) -> IResult<&[u8], BodyParams> {
|
||||||
|
alt((
|
||||||
|
map(nil, |_| None),
|
||||||
|
map(
|
||||||
|
parenthesized_nonempty_list(map(
|
||||||
|
tuple((string_utf8, tag(" "), string_utf8)),
|
||||||
|
|(key, _, val)| (Cow::Borrowed(key), Cow::Borrowed(val)),
|
||||||
|
)),
|
||||||
|
Option::from,
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_extension(i: &[u8]) -> IResult<&[u8], BodyExtension> {
|
||||||
|
alt((
|
||||||
|
map(number, BodyExtension::Num),
|
||||||
|
// Cannot find documentation on character encoding for body extension values.
|
||||||
|
// So far, assuming UTF-8 seems fine, please report if you run into issues here.
|
||||||
|
map(nstring_utf8, |v| BodyExtension::Str(v.map(Cow::Borrowed))),
|
||||||
|
map(
|
||||||
|
parenthesized_nonempty_list(body_extension),
|
||||||
|
BodyExtension::List,
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_disposition(i: &[u8]) -> IResult<&[u8], Option<ContentDisposition>> {
|
||||||
|
alt((
|
||||||
|
map(nil, |_| None),
|
||||||
|
paren_delimited(map(
|
||||||
|
tuple((string_utf8, tag(" "), body_param)),
|
||||||
|
|(ty, _, params)| {
|
||||||
|
Some(ContentDisposition {
|
||||||
|
ty: Cow::Borrowed(ty),
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_type_basic(i: &[u8]) -> IResult<&[u8], BodyStructure> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
string_utf8,
|
||||||
|
tag(" "),
|
||||||
|
string_utf8,
|
||||||
|
tag(" "),
|
||||||
|
body_fields,
|
||||||
|
body_ext_1part,
|
||||||
|
)),
|
||||||
|
|(ty, _, subtype, _, fields, ext)| BodyStructure::Basic {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed(ty),
|
||||||
|
subtype: Cow::Borrowed(subtype),
|
||||||
|
params: fields.param,
|
||||||
|
},
|
||||||
|
disposition: ext.disposition,
|
||||||
|
language: ext.language,
|
||||||
|
location: ext.location,
|
||||||
|
},
|
||||||
|
other: BodyContentSinglePart {
|
||||||
|
id: fields.id,
|
||||||
|
md5: ext.md5,
|
||||||
|
octets: fields.octets,
|
||||||
|
description: fields.description,
|
||||||
|
transfer_encoding: fields.transfer_encoding,
|
||||||
|
},
|
||||||
|
extension: ext.extension,
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_type_text(i: &[u8]) -> IResult<&[u8], BodyStructure> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
tag_no_case("\"TEXT\""),
|
||||||
|
tag(" "),
|
||||||
|
string_utf8,
|
||||||
|
tag(" "),
|
||||||
|
body_fields,
|
||||||
|
tag(" "),
|
||||||
|
number,
|
||||||
|
body_ext_1part,
|
||||||
|
)),
|
||||||
|
|(_, _, subtype, _, fields, _, lines, ext)| BodyStructure::Text {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("TEXT"),
|
||||||
|
subtype: Cow::Borrowed(subtype),
|
||||||
|
params: fields.param,
|
||||||
|
},
|
||||||
|
disposition: ext.disposition,
|
||||||
|
language: ext.language,
|
||||||
|
location: ext.location,
|
||||||
|
},
|
||||||
|
other: BodyContentSinglePart {
|
||||||
|
id: fields.id,
|
||||||
|
md5: ext.md5,
|
||||||
|
octets: fields.octets,
|
||||||
|
description: fields.description,
|
||||||
|
transfer_encoding: fields.transfer_encoding,
|
||||||
|
},
|
||||||
|
lines,
|
||||||
|
extension: ext.extension,
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_type_message(i: &[u8]) -> IResult<&[u8], BodyStructure> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
tag_no_case("\"MESSAGE\" \"RFC822\""),
|
||||||
|
tag(" "),
|
||||||
|
body_fields,
|
||||||
|
tag(" "),
|
||||||
|
envelope,
|
||||||
|
tag(" "),
|
||||||
|
body,
|
||||||
|
tag(" "),
|
||||||
|
number,
|
||||||
|
body_ext_1part,
|
||||||
|
)),
|
||||||
|
|(_, _, fields, _, envelope, _, body, _, lines, ext)| BodyStructure::Message {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("MESSAGE"),
|
||||||
|
subtype: Cow::Borrowed("RFC822"),
|
||||||
|
params: fields.param,
|
||||||
|
},
|
||||||
|
disposition: ext.disposition,
|
||||||
|
language: ext.language,
|
||||||
|
location: ext.location,
|
||||||
|
},
|
||||||
|
other: BodyContentSinglePart {
|
||||||
|
id: fields.id,
|
||||||
|
md5: ext.md5,
|
||||||
|
octets: fields.octets,
|
||||||
|
description: fields.description,
|
||||||
|
transfer_encoding: fields.transfer_encoding,
|
||||||
|
},
|
||||||
|
envelope,
|
||||||
|
body: Box::new(body),
|
||||||
|
lines,
|
||||||
|
extension: ext.extension,
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_type_multipart(i: &[u8]) -> IResult<&[u8], BodyStructure> {
|
||||||
|
map(
|
||||||
|
tuple((many1(body), tag(" "), string_utf8, body_ext_mpart)),
|
||||||
|
|(bodies, _, subtype, ext)| BodyStructure::Multipart {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("MULTIPART"),
|
||||||
|
subtype: Cow::Borrowed(subtype),
|
||||||
|
params: ext.param,
|
||||||
|
},
|
||||||
|
disposition: ext.disposition,
|
||||||
|
language: ext.language,
|
||||||
|
location: ext.location,
|
||||||
|
},
|
||||||
|
bodies,
|
||||||
|
extension: ext.extension,
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn body(i: &[u8]) -> IResult<&[u8], BodyStructure> {
|
||||||
|
paren_delimited(alt((
|
||||||
|
body_type_text,
|
||||||
|
body_type_message,
|
||||||
|
body_type_basic,
|
||||||
|
body_type_multipart,
|
||||||
|
)))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn msg_att_body_structure(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(tuple((tag_no_case("BODYSTRUCTURE "), body)), |(_, body)| {
|
||||||
|
AttributeValue::BodyStructure(body)
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
|
||||||
|
const EMPTY: &[u8] = &[];
|
||||||
|
|
||||||
|
// body-fld-param SP body-fld-id SP body-fld-desc SP body-fld-enc SP body-fld-octets
|
||||||
|
const BODY_FIELDS: &str = r#"("foo" "bar") "id" "desc" "7BIT" 1337"#;
|
||||||
|
const BODY_FIELD_PARAM_PAIR: (Cow<'_, str>, Cow<'_, str>) =
|
||||||
|
(Cow::Borrowed("foo"), Cow::Borrowed("bar"));
|
||||||
|
const BODY_FIELD_ID: Option<Cow<'_, str>> = Some(Cow::Borrowed("id"));
|
||||||
|
const BODY_FIELD_DESC: Option<Cow<'_, str>> = Some(Cow::Borrowed("desc"));
|
||||||
|
const BODY_FIELD_ENC: ContentEncoding = ContentEncoding::SevenBit;
|
||||||
|
const BODY_FIELD_OCTETS: u32 = 1337;
|
||||||
|
|
||||||
|
fn mock_body_text() -> (String, BodyStructure<'static>) {
|
||||||
|
(
|
||||||
|
format!(r#"("TEXT" "PLAIN" {} 42)"#, BODY_FIELDS),
|
||||||
|
BodyStructure::Text {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("TEXT"),
|
||||||
|
subtype: Cow::Borrowed("PLAIN"),
|
||||||
|
params: Some(vec![BODY_FIELD_PARAM_PAIR]),
|
||||||
|
},
|
||||||
|
disposition: None,
|
||||||
|
language: None,
|
||||||
|
location: None,
|
||||||
|
},
|
||||||
|
other: BodyContentSinglePart {
|
||||||
|
md5: None,
|
||||||
|
transfer_encoding: BODY_FIELD_ENC,
|
||||||
|
octets: BODY_FIELD_OCTETS,
|
||||||
|
id: BODY_FIELD_ID,
|
||||||
|
description: BODY_FIELD_DESC,
|
||||||
|
},
|
||||||
|
lines: 42,
|
||||||
|
extension: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_param_data() {
|
||||||
|
assert_matches!(body_param(br#"NIL"#), Ok((EMPTY, None)));
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_param(br#"("foo" "bar")"#),
|
||||||
|
Ok((EMPTY, Some(param))) => {
|
||||||
|
assert_eq!(param, vec![(Cow::Borrowed("foo"), Cow::Borrowed("bar"))]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_lang_data() {
|
||||||
|
assert_matches!(
|
||||||
|
body_lang(br#""bob""#),
|
||||||
|
Ok((EMPTY, Some(langs))) => {
|
||||||
|
assert_eq!(langs, vec!["bob"]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_lang(br#"("one" "two")"#),
|
||||||
|
Ok((EMPTY, Some(langs))) => {
|
||||||
|
assert_eq!(langs, vec!["one", "two"]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(body_lang(br#"NIL"#), Ok((EMPTY, None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_extension_data() {
|
||||||
|
assert_matches!(
|
||||||
|
body_extension(br#""blah""#),
|
||||||
|
Ok((EMPTY, BodyExtension::Str(Some(Cow::Borrowed("blah")))))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_extension(br#"NIL"#),
|
||||||
|
Ok((EMPTY, BodyExtension::Str(None)))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_extension(br#"("hello")"#),
|
||||||
|
Ok((EMPTY, BodyExtension::List(list))) => {
|
||||||
|
assert_eq!(list, vec![BodyExtension::Str(Some(Cow::Borrowed("hello")))]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_extension(br#"(1337)"#),
|
||||||
|
Ok((EMPTY, BodyExtension::List(list))) => {
|
||||||
|
assert_eq!(list, vec![BodyExtension::Num(1337)]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_disposition_data() {
|
||||||
|
assert_matches!(body_disposition(br#"NIL"#), Ok((EMPTY, None)));
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body_disposition(br#"("attachment" ("FILENAME" "pages.pdf"))"#),
|
||||||
|
Ok((EMPTY, Some(disposition))) => {
|
||||||
|
assert_eq!(disposition, ContentDisposition {
|
||||||
|
ty: Cow::Borrowed("attachment"),
|
||||||
|
params: Some(vec![
|
||||||
|
(Cow::Borrowed("FILENAME"), Cow::Borrowed("pages.pdf"))
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure_text() {
|
||||||
|
let (body_str, body_struct) = mock_body_text();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body(body_str.as_bytes()),
|
||||||
|
Ok((_, text)) => {
|
||||||
|
assert_eq!(text, body_struct);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure_text_with_ext() {
|
||||||
|
let body_str = format!(r#"("TEXT" "PLAIN" {} 42 NIL NIL NIL NIL)"#, BODY_FIELDS);
|
||||||
|
let (_, text_body_struct) = mock_body_text();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body(body_str.as_bytes()),
|
||||||
|
Ok((_, text)) => {
|
||||||
|
assert_eq!(text, text_body_struct)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure_basic() {
|
||||||
|
const BODY: &[u8] = br#"("APPLICATION" "PDF" ("NAME" "pages.pdf") NIL NIL "BASE64" 38838 NIL ("attachment" ("FILENAME" "pages.pdf")) NIL NIL)"#;
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body(BODY),
|
||||||
|
Ok((_, basic)) => {
|
||||||
|
assert_eq!(basic, BodyStructure::Basic {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("APPLICATION"),
|
||||||
|
subtype: Cow::Borrowed("PDF"),
|
||||||
|
params: Some(vec![(Cow::Borrowed("NAME"), Cow::Borrowed("pages.pdf"))])
|
||||||
|
},
|
||||||
|
disposition: Some(ContentDisposition {
|
||||||
|
ty: Cow::Borrowed("attachment"),
|
||||||
|
params: Some(vec![(Cow::Borrowed("FILENAME"), Cow::Borrowed("pages.pdf"))])
|
||||||
|
}),
|
||||||
|
language: None,
|
||||||
|
location: None,
|
||||||
|
},
|
||||||
|
other: BodyContentSinglePart {
|
||||||
|
transfer_encoding: ContentEncoding::Base64,
|
||||||
|
octets: 38838,
|
||||||
|
id: None,
|
||||||
|
md5: None,
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
extension: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure_message() {
|
||||||
|
let (text_body_str, _) = mock_body_text();
|
||||||
|
let envelope_str = r#"("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>")"#;
|
||||||
|
let body_str = format!(
|
||||||
|
r#"("MESSAGE" "RFC822" {} {} {} 42)"#,
|
||||||
|
BODY_FIELDS, envelope_str, text_body_str
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body(body_str.as_bytes()),
|
||||||
|
Ok((_, BodyStructure::Message { .. }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure_multipart() {
|
||||||
|
let (text_body_str1, text_body_struct1) = mock_body_text();
|
||||||
|
let (text_body_str2, text_body_struct2) = mock_body_text();
|
||||||
|
let body_str = format!(
|
||||||
|
r#"({}{} "ALTERNATIVE" NIL NIL NIL NIL)"#,
|
||||||
|
text_body_str1, text_body_str2
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
body(body_str.as_bytes()),
|
||||||
|
Ok((_, multipart)) => {
|
||||||
|
assert_eq!(multipart, BodyStructure::Multipart {
|
||||||
|
common: BodyContentCommon {
|
||||||
|
ty: ContentType {
|
||||||
|
ty: Cow::Borrowed("MULTIPART"),
|
||||||
|
subtype: Cow::Borrowed("ALTERNATIVE"),
|
||||||
|
params: None
|
||||||
|
},
|
||||||
|
language: None,
|
||||||
|
location: None,
|
||||||
|
disposition: None,
|
||||||
|
},
|
||||||
|
bodies: vec![
|
||||||
|
text_body_struct1,
|
||||||
|
text_body_struct2,
|
||||||
|
],
|
||||||
|
extension: None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
754
src/parser/rfc3501/mod.rs
Normal file
754
src/parser/rfc3501/mod.rs
Normal file
|
@ -0,0 +1,754 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc3501
|
||||||
|
//!
|
||||||
|
//! INTERNET MESSAGE ACCESS PROTOCOL
|
||||||
|
//!
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::str::from_utf8;
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case, take_while, take_while1},
|
||||||
|
character::streaming::char,
|
||||||
|
combinator::{map, map_res, opt, recognize},
|
||||||
|
multi::{many0, many1},
|
||||||
|
sequence::{delimited, pair, preceded, terminated, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parser::{
|
||||||
|
core::*, rfc2087, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551, rfc5161,
|
||||||
|
rfc5256, rfc5464, rfc7162,
|
||||||
|
},
|
||||||
|
types::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod body;
|
||||||
|
pub mod body_structure;
|
||||||
|
|
||||||
|
fn is_tag_char(c: u8) -> bool {
|
||||||
|
c != b'+' && is_astring_char(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_ok(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
map(tag_no_case("OK"), |_s| Status::Ok)(i)
|
||||||
|
}
|
||||||
|
fn status_no(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
map(tag_no_case("NO"), |_s| Status::No)(i)
|
||||||
|
}
|
||||||
|
fn status_bad(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
map(tag_no_case("BAD"), |_s| Status::Bad)(i)
|
||||||
|
}
|
||||||
|
fn status_preauth(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
map(tag_no_case("PREAUTH"), |_s| Status::PreAuth)(i)
|
||||||
|
}
|
||||||
|
fn status_bye(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
map(tag_no_case("BYE"), |_s| Status::Bye)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(i: &[u8]) -> IResult<&[u8], Status> {
|
||||||
|
alt((status_ok, status_no, status_bad, status_preauth, status_bye))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map(astring_utf8, |s| {
|
||||||
|
if s.eq_ignore_ascii_case("INBOX") {
|
||||||
|
"INBOX"
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flag_extension(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(
|
||||||
|
recognize(pair(tag(b"\\"), take_while(is_atom_char))),
|
||||||
|
from_utf8,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flag(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
alt((flag_extension, atom))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flag_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<str>>> {
|
||||||
|
// Correct code is
|
||||||
|
// parenthesized_list(flag)(i)
|
||||||
|
//
|
||||||
|
// Unfortunately, Zoho Mail Server (imap.zoho.com) sends the following response:
|
||||||
|
// * FLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)
|
||||||
|
//
|
||||||
|
// As a workaround, "\*" is allowed here.
|
||||||
|
parenthesized_list(map(flag_perm, Cow::Borrowed))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flag_perm(i: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
alt((map_res(tag(b"\\*"), from_utf8), flag))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_alert(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"ALERT"), |_| ResponseCode::Alert)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_badcharset(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(
|
||||||
|
tag_no_case(b"BADCHARSET"),
|
||||||
|
opt(preceded(
|
||||||
|
tag(b" "),
|
||||||
|
parenthesized_nonempty_list(map(astring_utf8, Cow::Borrowed)),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
ResponseCode::BadCharset,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_capability(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(capability_data, ResponseCode::Capabilities)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_parse(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"PARSE"), |_| ResponseCode::Parse)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_permanent_flags(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(
|
||||||
|
tag_no_case(b"PERMANENTFLAGS "),
|
||||||
|
parenthesized_list(map(flag_perm, Cow::Borrowed)),
|
||||||
|
),
|
||||||
|
ResponseCode::PermanentFlags,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_read_only(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"READ-ONLY"), |_| ResponseCode::ReadOnly)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_read_write(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"READ-WRITE"), |_| ResponseCode::ReadWrite)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_try_create(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"TRYCREATE"), |_| ResponseCode::TryCreate)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_uid_validity(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"UIDVALIDITY "), number),
|
||||||
|
ResponseCode::UidValidity,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_uid_next(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"UIDNEXT "), number),
|
||||||
|
ResponseCode::UidNext,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code_unseen(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"UNSEEN "), number),
|
||||||
|
ResponseCode::Unseen,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resp_text_code(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
// Per the spec, the closing tag should be "] ".
|
||||||
|
// See `resp_text` for more on why this is done differently.
|
||||||
|
delimited(
|
||||||
|
tag(b"["),
|
||||||
|
alt((
|
||||||
|
resp_text_code_alert,
|
||||||
|
resp_text_code_badcharset,
|
||||||
|
resp_text_code_capability,
|
||||||
|
resp_text_code_parse,
|
||||||
|
resp_text_code_permanent_flags,
|
||||||
|
resp_text_code_uid_validity,
|
||||||
|
resp_text_code_uid_next,
|
||||||
|
resp_text_code_unseen,
|
||||||
|
resp_text_code_read_only,
|
||||||
|
resp_text_code_read_write,
|
||||||
|
resp_text_code_try_create,
|
||||||
|
rfc4551::resp_text_code_highest_mod_seq,
|
||||||
|
rfc4315::resp_text_code_append_uid,
|
||||||
|
rfc4315::resp_text_code_copy_uid,
|
||||||
|
rfc4315::resp_text_code_uid_not_sticky,
|
||||||
|
rfc5464::resp_text_code_metadata_long_entries,
|
||||||
|
rfc5464::resp_text_code_metadata_max_size,
|
||||||
|
rfc5464::resp_text_code_metadata_too_many,
|
||||||
|
rfc5464::resp_text_code_metadata_no_private,
|
||||||
|
)),
|
||||||
|
tag(b"]"),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
|
||||||
|
alt((
|
||||||
|
map(tag_no_case(b"IMAP4rev1"), |_| Capability::Imap4rev1),
|
||||||
|
map(
|
||||||
|
map(preceded(tag_no_case(b"AUTH="), atom), Cow::Borrowed),
|
||||||
|
Capability::Auth,
|
||||||
|
),
|
||||||
|
map(map(atom, Cow::Borrowed), Capability::Atom),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_capabilities_contains_imap4rev(
|
||||||
|
capabilities: Vec<Capability<'_>>,
|
||||||
|
) -> Result<Vec<Capability<'_>>, ()> {
|
||||||
|
if capabilities.contains(&Capability::Imap4rev1) {
|
||||||
|
Ok(capabilities)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capability_data(i: &[u8]) -> IResult<&[u8], Vec<Capability>> {
|
||||||
|
map_res(
|
||||||
|
preceded(
|
||||||
|
tag_no_case(b"CAPABILITY"),
|
||||||
|
many0(preceded(char(' '), capability)),
|
||||||
|
),
|
||||||
|
ensure_capabilities_contains_imap4rev,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_search(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
// Technically, trailing whitespace is not allowed here, but multiple
|
||||||
|
// email servers in the wild seem to have it anyway (see #34, #108).
|
||||||
|
terminated(
|
||||||
|
preceded(tag_no_case(b"SEARCH"), many0(preceded(tag(" "), number))),
|
||||||
|
opt(tag(" ")),
|
||||||
|
),
|
||||||
|
MailboxDatum::Search,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_flags(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("FLAGS "), flag_list),
|
||||||
|
MailboxDatum::Flags,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_exists(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
terminated(number, tag_no_case(" EXISTS")),
|
||||||
|
MailboxDatum::Exists,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn mailbox_list(i: &[u8]) -> IResult<&[u8], (Vec<Cow<str>>, Option<&str>, &str)> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
flag_list,
|
||||||
|
tag(b" "),
|
||||||
|
alt((map(quoted_utf8, Some), map(nil, |_| None))),
|
||||||
|
tag(b" "),
|
||||||
|
mailbox,
|
||||||
|
)),
|
||||||
|
|(flags, _, delimiter, _, name)| (flags, delimiter, name),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_list(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(preceded(tag_no_case("LIST "), mailbox_list), |data| {
|
||||||
|
MailboxDatum::List {
|
||||||
|
flags: data.0,
|
||||||
|
delimiter: data.1.map(Cow::Borrowed),
|
||||||
|
name: Cow::Borrowed(data.2),
|
||||||
|
}
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_lsub(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(preceded(tag_no_case("LSUB "), mailbox_list), |data| {
|
||||||
|
MailboxDatum::List {
|
||||||
|
flags: data.0,
|
||||||
|
delimiter: data.1.map(Cow::Borrowed),
|
||||||
|
name: Cow::Borrowed(data.2),
|
||||||
|
}
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike `status_att` in the RFC syntax, this includes the value,
|
||||||
|
// so that it can return a valid enum object instead of just a key.
|
||||||
|
fn status_att(i: &[u8]) -> IResult<&[u8], StatusAttribute> {
|
||||||
|
alt((
|
||||||
|
rfc4551::status_att_val_highest_mod_seq,
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("MESSAGES "), number),
|
||||||
|
StatusAttribute::Messages,
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("RECENT "), number),
|
||||||
|
StatusAttribute::Recent,
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("UIDNEXT "), number),
|
||||||
|
StatusAttribute::UidNext,
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("UIDVALIDITY "), number),
|
||||||
|
StatusAttribute::UidValidity,
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("UNSEEN "), number),
|
||||||
|
StatusAttribute::Unseen,
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_att_list(i: &[u8]) -> IResult<&[u8], Vec<StatusAttribute>> {
|
||||||
|
parenthesized_nonempty_list(status_att)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_status(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
tuple((tag_no_case("STATUS "), mailbox, tag(" "), status_att_list)),
|
||||||
|
|(_, mailbox, _, status)| MailboxDatum::Status {
|
||||||
|
mailbox: Cow::Borrowed(mailbox),
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data_recent(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
terminated(number, tag_no_case(" RECENT")),
|
||||||
|
MailboxDatum::Recent,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mailbox_data(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
alt((
|
||||||
|
mailbox_data_flags,
|
||||||
|
mailbox_data_exists,
|
||||||
|
mailbox_data_list,
|
||||||
|
mailbox_data_lsub,
|
||||||
|
mailbox_data_status,
|
||||||
|
mailbox_data_recent,
|
||||||
|
mailbox_data_search,
|
||||||
|
rfc5256::mailbox_data_sort,
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An address structure is a parenthesized list that describes an
|
||||||
|
// electronic mail address.
|
||||||
|
fn address(i: &[u8]) -> IResult<&[u8], Address> {
|
||||||
|
paren_delimited(map(
|
||||||
|
tuple((
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
)),
|
||||||
|
|(name, _, adl, _, mailbox, _, host)| Address {
|
||||||
|
name: name.map(Cow::Borrowed),
|
||||||
|
adl: adl.map(Cow::Borrowed),
|
||||||
|
mailbox: mailbox.map(Cow::Borrowed),
|
||||||
|
host: host.map(Cow::Borrowed),
|
||||||
|
},
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opt_addresses(i: &[u8]) -> IResult<&[u8], Option<Vec<Address>>> {
|
||||||
|
alt((
|
||||||
|
map(nil, |_s| None),
|
||||||
|
map(
|
||||||
|
paren_delimited(many1(terminated(address, opt(char(' '))))),
|
||||||
|
Some,
|
||||||
|
),
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envelope = "(" env-date SP env-subject SP env-from SP
|
||||||
|
// env-sender SP env-reply-to SP env-to SP env-cc SP
|
||||||
|
// env-bcc SP env-in-reply-to SP env-message-id ")"
|
||||||
|
//
|
||||||
|
// env-bcc = "(" 1*address ")" / nil
|
||||||
|
//
|
||||||
|
// env-cc = "(" 1*address ")" / nil
|
||||||
|
//
|
||||||
|
// env-date = nstring
|
||||||
|
//
|
||||||
|
// env-from = "(" 1*address ")" / nil
|
||||||
|
//
|
||||||
|
// env-in-reply-to = nstring
|
||||||
|
//
|
||||||
|
// env-message-id = nstring
|
||||||
|
//
|
||||||
|
// env-reply-to = "(" 1*address ")" / nil
|
||||||
|
//
|
||||||
|
// env-sender = "(" 1*address ")" / nil
|
||||||
|
//
|
||||||
|
// env-subject = nstring
|
||||||
|
//
|
||||||
|
// env-to = "(" 1*address ")" / nil
|
||||||
|
pub(crate) fn envelope(i: &[u8]) -> IResult<&[u8], Envelope> {
|
||||||
|
paren_delimited(map(
|
||||||
|
tuple((
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
opt_addresses,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
tag(" "),
|
||||||
|
nstring,
|
||||||
|
)),
|
||||||
|
|(
|
||||||
|
date,
|
||||||
|
_,
|
||||||
|
subject,
|
||||||
|
_,
|
||||||
|
from,
|
||||||
|
_,
|
||||||
|
sender,
|
||||||
|
_,
|
||||||
|
reply_to,
|
||||||
|
_,
|
||||||
|
to,
|
||||||
|
_,
|
||||||
|
cc,
|
||||||
|
_,
|
||||||
|
bcc,
|
||||||
|
_,
|
||||||
|
in_reply_to,
|
||||||
|
_,
|
||||||
|
message_id,
|
||||||
|
)| Envelope {
|
||||||
|
date: date.map(Cow::Borrowed),
|
||||||
|
subject: subject.map(Cow::Borrowed),
|
||||||
|
from,
|
||||||
|
sender,
|
||||||
|
reply_to,
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
in_reply_to: in_reply_to.map(Cow::Borrowed),
|
||||||
|
message_id: message_id.map(Cow::Borrowed),
|
||||||
|
},
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_envelope(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(preceded(tag_no_case("ENVELOPE "), envelope), |envelope| {
|
||||||
|
AttributeValue::Envelope(Box::new(envelope))
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_internal_date(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("INTERNALDATE "), nstring_utf8),
|
||||||
|
|date| AttributeValue::InternalDate(Cow::Borrowed(date.unwrap())),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_flags(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("FLAGS "), flag_list),
|
||||||
|
AttributeValue::Flags,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_rfc822(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(preceded(tag_no_case("RFC822 "), nstring), |v| {
|
||||||
|
AttributeValue::Rfc822(v.map(Cow::Borrowed))
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_rfc822_header(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
// extra space workaround for DavMail
|
||||||
|
map(
|
||||||
|
tuple((tag_no_case("RFC822.HEADER "), opt(tag(b" ")), nstring)),
|
||||||
|
|(_, _, raw)| AttributeValue::Rfc822Header(raw.map(Cow::Borrowed)),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_rfc822_size(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case("RFC822.SIZE "), number),
|
||||||
|
AttributeValue::Rfc822Size,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_rfc822_text(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(preceded(tag_no_case("RFC822.TEXT "), nstring), |v| {
|
||||||
|
AttributeValue::Rfc822Text(v.map(Cow::Borrowed))
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_uid(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
map(preceded(tag_no_case("UID "), number), AttributeValue::Uid)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// msg-att = "(" (msg-att-dynamic / msg-att-static)
|
||||||
|
// *(SP (msg-att-dynamic / msg-att-static)) ")"
|
||||||
|
//
|
||||||
|
// msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
|
||||||
|
// ; MAY change for a message
|
||||||
|
//
|
||||||
|
// msg-att-static = "ENVELOPE" SP envelope / "INTERNALDATE" SP date-time /
|
||||||
|
// "RFC822" [".HEADER" / ".TEXT"] SP nstring /
|
||||||
|
// "RFC822.SIZE" SP number /
|
||||||
|
// "BODY" ["STRUCTURE"] SP body /
|
||||||
|
// "BODY" section ["<" number ">"] SP nstring /
|
||||||
|
// "UID" SP uniqueid
|
||||||
|
// ; MUST NOT change for a message
|
||||||
|
fn msg_att(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
alt((
|
||||||
|
msg_att_body_section,
|
||||||
|
msg_att_body_structure,
|
||||||
|
msg_att_envelope,
|
||||||
|
msg_att_internal_date,
|
||||||
|
msg_att_flags,
|
||||||
|
rfc4551::msg_att_mod_seq,
|
||||||
|
msg_att_rfc822,
|
||||||
|
msg_att_rfc822_header,
|
||||||
|
msg_att_rfc822_size,
|
||||||
|
msg_att_rfc822_text,
|
||||||
|
msg_att_uid,
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg_att_list(i: &[u8]) -> IResult<&[u8], Vec<AttributeValue>> {
|
||||||
|
parenthesized_nonempty_list(msg_att)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
|
||||||
|
fn message_data_fetch(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
map(
|
||||||
|
tuple((number, tag_no_case(" FETCH "), msg_att_list)),
|
||||||
|
|(num, _, attrs)| Response::Fetch(num, attrs),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
|
||||||
|
fn message_data_expunge(i: &[u8]) -> IResult<&[u8], u32> {
|
||||||
|
terminated(number, tag_no_case(" EXPUNGE"))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag = 1*<any ASTRING-CHAR except "+">
|
||||||
|
fn imap_tag(i: &[u8]) -> IResult<&[u8], RequestId> {
|
||||||
|
map(map_res(take_while1(is_tag_char), from_utf8), |s| {
|
||||||
|
RequestId(s.to_string())
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not quite according to spec, which mandates the following:
|
||||||
|
// ["[" resp-text-code "]" SP] text
|
||||||
|
// However, examples in RFC 4551 (Conditional STORE) counteract this by giving
|
||||||
|
// examples of `resp-text` that do not include the trailing space and text.
|
||||||
|
fn resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
|
||||||
|
map(tuple((opt(resp_text_code), text)), |(code, text)| {
|
||||||
|
let res = if text.is_empty() {
|
||||||
|
None
|
||||||
|
} else if code.is_some() {
|
||||||
|
Some(&text[1..])
|
||||||
|
} else {
|
||||||
|
Some(text)
|
||||||
|
};
|
||||||
|
(code, res)
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// an response-text if it is at the end of a response. Empty text is then allowed without the normally needed trailing space.
|
||||||
|
fn trailing_resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
|
||||||
|
map(opt(tuple((tag(b" "), resp_text))), |resptext| {
|
||||||
|
resptext.map(|(_, tuple)| tuple).unwrap_or((None, None))
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue-req = "+" SP (resp-text / base64) CRLF
|
||||||
|
pub(crate) fn continue_req(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
// Some servers do not send the space :/
|
||||||
|
// TODO: base64
|
||||||
|
map(
|
||||||
|
tuple((tag("+"), opt(tag(" ")), resp_text, tag("\r\n"))),
|
||||||
|
|(_, _, text, _)| Response::Continue {
|
||||||
|
code: text.0,
|
||||||
|
information: text.1.map(Cow::Borrowed),
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// response-tagged = tag SP resp-cond-state CRLF
|
||||||
|
//
|
||||||
|
// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
||||||
|
// ; Status condition
|
||||||
|
pub(crate) fn response_tagged(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
imap_tag,
|
||||||
|
tag(b" "),
|
||||||
|
status,
|
||||||
|
trailing_resp_text,
|
||||||
|
tag(b"\r\n"),
|
||||||
|
)),
|
||||||
|
|(tag, _, status, text, _)| Response::Done {
|
||||||
|
tag,
|
||||||
|
status,
|
||||||
|
code: text.0,
|
||||||
|
information: text.1.map(Cow::Borrowed),
|
||||||
|
},
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
|
||||||
|
// ; Authentication condition
|
||||||
|
//
|
||||||
|
// resp-cond-bye = "BYE" SP resp-text
|
||||||
|
//
|
||||||
|
// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
||||||
|
// ; Status condition
|
||||||
|
fn resp_cond(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
map(tuple((status, trailing_resp_text)), |(status, text)| {
|
||||||
|
Response::Data {
|
||||||
|
status,
|
||||||
|
code: text.0,
|
||||||
|
information: text.1.map(Cow::Borrowed),
|
||||||
|
}
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
||||||
|
// mailbox-data / message-data / capability-data / quota) CRLF
|
||||||
|
pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
delimited(
|
||||||
|
tag(b"* "),
|
||||||
|
alt((
|
||||||
|
resp_cond,
|
||||||
|
map(mailbox_data, Response::MailboxData),
|
||||||
|
map(message_data_expunge, Response::Expunge),
|
||||||
|
message_data_fetch,
|
||||||
|
map(capability_data, Response::Capabilities),
|
||||||
|
rfc5161::resp_enabled,
|
||||||
|
rfc5464::metadata_solicited,
|
||||||
|
rfc5464::metadata_unsolicited,
|
||||||
|
rfc7162::resp_vanished,
|
||||||
|
rfc2087::quota,
|
||||||
|
rfc2087::quota_root,
|
||||||
|
)),
|
||||||
|
tag(b"\r\n"),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::types::*;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list() {
|
||||||
|
match super::mailbox(b"iNboX ") {
|
||||||
|
Ok((_, mb)) => {
|
||||||
|
assert_eq!(mb, "INBOX");
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_envelope() {
|
||||||
|
let env = br#"ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") "#;
|
||||||
|
match super::msg_att_envelope(env) {
|
||||||
|
Ok((_, AttributeValue::Envelope(_))) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_opt_addresses() {
|
||||||
|
let addr = b"((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) ";
|
||||||
|
match super::opt_addresses(addr) {
|
||||||
|
Ok((_, _addresses)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_opt_addresses_no_space() {
|
||||||
|
let addr =
|
||||||
|
br#"((NIL NIL "test" "example@example.com")(NIL NIL "test" "example@example.com"))"#;
|
||||||
|
match super::opt_addresses(addr) {
|
||||||
|
Ok((_, _addresses)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_addresses() {
|
||||||
|
match super::address(b"(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\") ") {
|
||||||
|
Ok((_, _address)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal non-UTF8 address
|
||||||
|
match super::address(b"({12}\r\nJoh\xff Klensin NIL \"KLENSIN\" \"MIT.EDU\") ") {
|
||||||
|
Ok((_, _address)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_capability_data() {
|
||||||
|
// Minimal capabilities
|
||||||
|
assert_matches!(
|
||||||
|
super::capability_data(b"CAPABILITY IMAP4rev1\r\n"),
|
||||||
|
Ok((_, capabilities)) => {
|
||||||
|
assert_eq!(capabilities, vec![Capability::Imap4rev1])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
super::capability_data(b"CAPABILITY XPIG-LATIN IMAP4rev1 STARTTLS AUTH=GSSAPI\r\n"),
|
||||||
|
Ok((_, capabilities)) => {
|
||||||
|
assert_eq!(capabilities, vec![
|
||||||
|
Capability::Atom(Cow::Borrowed("XPIG-LATIN")),
|
||||||
|
Capability::Imap4rev1,
|
||||||
|
Capability::Atom(Cow::Borrowed("STARTTLS")),
|
||||||
|
Capability::Auth(Cow::Borrowed("GSSAPI")),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
super::capability_data(b"CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n"),
|
||||||
|
Ok((_, capabilities)) => {
|
||||||
|
assert_eq!(capabilities, vec![
|
||||||
|
Capability::Imap4rev1,
|
||||||
|
Capability::Auth(Cow::Borrowed("GSSAPI")),
|
||||||
|
Capability::Auth(Cow::Borrowed("PLAIN")),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Capability command must contain IMAP4rev1
|
||||||
|
assert_matches!(
|
||||||
|
super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
|
||||||
|
Err(_)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
src/parser/rfc4315.rs
Normal file
95
src/parser/rfc4315.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc4315
|
||||||
|
//!
|
||||||
|
//! The IMAP UIDPLUS Extension
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
combinator::map,
|
||||||
|
multi::separated_list1,
|
||||||
|
sequence::{preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::core::number;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
/// Extends resp-text-code as follows:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// resp-text-code =/ resp-code-apnd
|
||||||
|
/// resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
|
||||||
|
/// append-uid =/ uid-set
|
||||||
|
/// ; only permitted if client uses [MULTIAPPEND]
|
||||||
|
/// ; to append multiple messages.
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
|
||||||
|
pub(crate) fn resp_text_code_append_uid(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(
|
||||||
|
tag_no_case(b"APPENDUID "),
|
||||||
|
tuple((number, tag(" "), uid_set)),
|
||||||
|
),
|
||||||
|
|(fst, _, snd)| ResponseCode::AppendUid(fst, snd),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends resp-text-code as follows:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// resp-text-code =/ resp-code-copy
|
||||||
|
/// resp-code-copy = "COPYUID" SP nz-number SP uid-set
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
|
||||||
|
pub(crate) fn resp_text_code_copy_uid(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(
|
||||||
|
preceded(
|
||||||
|
tag_no_case(b"COPYUID "),
|
||||||
|
tuple((number, tag(" "), uid_set, tag(" "), uid_set)),
|
||||||
|
),
|
||||||
|
|(fst, _, snd, _, trd)| ResponseCode::CopyUid(fst, snd, trd),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends resp-text-code as follows:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// resp-text-code =/ "UIDNOTSTICKY"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
|
||||||
|
pub(crate) fn resp_text_code_uid_not_sticky(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
map(tag_no_case(b"UIDNOTSTICKY"), |_| ResponseCode::UidNotSticky)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the uid-set nonterminal:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// uid-set = (uniqueid / uid-range) *("," uid-set)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC4315 - 4 Formal Syntax](https://tools.ietf.org/html/rfc4315#section-4)
|
||||||
|
fn uid_set(i: &[u8]) -> IResult<&[u8], Vec<UidSetMember>> {
|
||||||
|
separated_list1(tag(","), alt((uid_range, map(number, From::from))))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the uid-set nonterminal:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// uid-range = (uniqueid ":" uniqueid)
|
||||||
|
/// ; two uniqueid values and all values
|
||||||
|
/// ; between these two regards of order.
|
||||||
|
/// ; Example: 2:4 and 4:2 are equivalent.
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC4315 - 4 Formal Syntax](https://tools.ietf.org/html/rfc4315#section-4)
|
||||||
|
fn uid_range(i: &[u8]) -> IResult<&[u8], UidSetMember> {
|
||||||
|
map(
|
||||||
|
nom::sequence::separated_pair(number, tag(":"), number),
|
||||||
|
|(fst, snd)| if fst <= snd { fst..=snd } else { snd..=fst }.into(),
|
||||||
|
)(i)
|
||||||
|
}
|
36
src/parser/rfc4551.rs
Normal file
36
src/parser/rfc4551.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc4551
|
||||||
|
//!
|
||||||
|
//! IMAP Extension for Conditional STORE Operation
|
||||||
|
//! or Quick Flag Changes Resynchronization
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{bytes::streaming::tag_no_case, sequence::tuple, IResult};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parser::core::{number_64, paren_delimited},
|
||||||
|
types::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The highest mod-sequence value of all messages in the mailbox.
|
||||||
|
// Extends resp-test-code defined in rfc3501.
|
||||||
|
// [RFC4551 - 3.6 HIGHESTMODSEQ Status Data Items](https://tools.ietf.org/html/rfc4551#section-3.6)
|
||||||
|
// [RFC4551 - 4. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc4551#section-4)
|
||||||
|
pub(crate) fn resp_text_code_highest_mod_seq(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
let (i, (_, num)) = tuple((tag_no_case("HIGHESTMODSEQ "), number_64))(i)?;
|
||||||
|
Ok((i, ResponseCode::HighestModSeq(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extends status-att/status-att-list defined in rfc3501
|
||||||
|
// [RFC4551 - 3.6 - HIGHESTMODSEQ Status Data Items](https://tools.ietf.org/html/rfc4551#section-3.6)
|
||||||
|
// [RFC4551 - 4. Formal Syntax - status-att-val](https://tools.ietf.org/html/rfc4551#section-4)
|
||||||
|
pub(crate) fn status_att_val_highest_mod_seq(i: &[u8]) -> IResult<&[u8], StatusAttribute> {
|
||||||
|
let (i, (_, num)) = tuple((tag_no_case("HIGHESTMODSEQ "), number_64))(i)?;
|
||||||
|
Ok((i, StatusAttribute::HighestModSeq(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// [RFC4551 - 4. Formal Syntax - fetch-mod-resp](https://tools.ietf.org/html/rfc4551#section-4)
|
||||||
|
pub(crate) fn msg_att_mod_seq(i: &[u8]) -> IResult<&[u8], AttributeValue> {
|
||||||
|
let (i, (_, num)) = tuple((tag_no_case("MODSEQ "), paren_delimited(number_64)))(i)?;
|
||||||
|
Ok((i, AttributeValue::ModSeq(num)))
|
||||||
|
}
|
37
src/parser/rfc5161.rs
Normal file
37
src/parser/rfc5161.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc5161
|
||||||
|
//!
|
||||||
|
//! The IMAP ENABLE Extension
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::streaming::tag_no_case,
|
||||||
|
character::streaming::char,
|
||||||
|
combinator::map,
|
||||||
|
multi::many0,
|
||||||
|
sequence::{preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::parser::core::atom;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
// The ENABLED response lists capabilities that were enabled in response
|
||||||
|
// to a ENABLE command.
|
||||||
|
// [RFC5161 - 3.2 The ENABLED Response](https://tools.ietf.org/html/rfc5161#section-3.2)
|
||||||
|
pub(crate) fn resp_enabled(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
map(enabled_data, Response::Capabilities)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled_data(i: &[u8]) -> IResult<&[u8], Vec<Capability>> {
|
||||||
|
let (i, (_, capabilities)) = tuple((
|
||||||
|
tag_no_case("ENABLED"),
|
||||||
|
many0(preceded(char(' '), capability)),
|
||||||
|
))(i)?;
|
||||||
|
Ok((i, capabilities))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
|
||||||
|
map(map(atom, Cow::Borrowed), Capability::Atom)(i)
|
||||||
|
}
|
46
src/parser/rfc5256.rs
Normal file
46
src/parser/rfc5256.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc5256
|
||||||
|
//!
|
||||||
|
//! SORT extension
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
combinator::{map, opt},
|
||||||
|
multi::many0,
|
||||||
|
sequence::{preceded, terminated},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{parser::core::number, types::MailboxDatum};
|
||||||
|
|
||||||
|
/// BASE.7.2.SORT. SORT Response
|
||||||
|
///
|
||||||
|
/// Data: zero or more numbers
|
||||||
|
///
|
||||||
|
/// The SORT response occurs as a result of a SORT or UID SORT
|
||||||
|
/// command. The number(s) refer to those messages that match the
|
||||||
|
/// search criteria. For SORT, these are message sequence numbers;
|
||||||
|
/// for UID SORT, these are unique identifiers. Each number is
|
||||||
|
/// delimited by a space.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// S: * SORT 2 3 6
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC5256 - 4 Additional Responses](https://tools.ietf.org/html/rfc5256#section-4)
|
||||||
|
pub(crate) fn mailbox_data_sort(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
|
||||||
|
map(
|
||||||
|
// Technically, trailing whitespace is not allowed for the SEARCH command,
|
||||||
|
// but multiple email servers in the wild seem to have it anyway (see #34, #108).
|
||||||
|
// Since the SORT command extends the SEARCH command, the trailing whitespace
|
||||||
|
// is exceptionnaly allowed here (as for the SEARCH command).
|
||||||
|
terminated(
|
||||||
|
preceded(tag_no_case(b"SORT"), many0(preceded(tag(" "), number))),
|
||||||
|
opt(tag(" ")),
|
||||||
|
),
|
||||||
|
MailboxDatum::Sort,
|
||||||
|
)(i)
|
||||||
|
}
|
386
src/parser/rfc5464.rs
Normal file
386
src/parser/rfc5464.rs
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc5464
|
||||||
|
//!
|
||||||
|
//! IMAP METADATA extension
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::streaming::{tag, tag_no_case},
|
||||||
|
combinator::{map, map_opt},
|
||||||
|
multi::separated_list0,
|
||||||
|
sequence::tuple,
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::{parser::core::*, types::*};
|
||||||
|
|
||||||
|
fn is_entry_component_char(c: u8) -> bool {
|
||||||
|
c < 0x80 && c > 0x19 && c != b'*' && c != b'%' && c != b'/'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EntryParseStage<'a> {
|
||||||
|
PrivateShared(usize),
|
||||||
|
Admin(usize),
|
||||||
|
VendorComment(usize),
|
||||||
|
Path(usize),
|
||||||
|
Done(usize),
|
||||||
|
Fail(nom::Err<&'a [u8]>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_private_shared(i: &[u8]) -> EntryParseStage {
|
||||||
|
if i.starts_with(b"/private") {
|
||||||
|
EntryParseStage::VendorComment(8)
|
||||||
|
} else if i.starts_with(b"/shared") {
|
||||||
|
EntryParseStage::Admin(7)
|
||||||
|
} else {
|
||||||
|
EntryParseStage::Fail(nom::Err::Error(
|
||||||
|
b"Entry Name doesn't start with /private or /shared",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_admin(i: &[u8], l: usize) -> EntryParseStage {
|
||||||
|
if i[l..].starts_with(b"/admin") {
|
||||||
|
EntryParseStage::Path(l + 6)
|
||||||
|
} else {
|
||||||
|
EntryParseStage::VendorComment(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_vendor_comment(i: &[u8], l: usize) -> EntryParseStage {
|
||||||
|
if i[l..].starts_with(b"/comment") {
|
||||||
|
EntryParseStage::Path(l + 8)
|
||||||
|
} else if i[l..].starts_with(b"/vendor") {
|
||||||
|
//make sure vendor name is present
|
||||||
|
if i.len() < l + 9 || i[l + 7] != b'/' || !is_entry_component_char(i[l + 8]) {
|
||||||
|
EntryParseStage::Fail(nom::Err::Incomplete(nom::Needed::Unknown))
|
||||||
|
} else {
|
||||||
|
EntryParseStage::Path(l + 7)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EntryParseStage::Fail(nom::Err::Error(
|
||||||
|
b"Entry name is not continued with /admin, /vendor or /comment",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_path(i: &[u8], l: usize) -> EntryParseStage {
|
||||||
|
if i.len() == l || i[l] == b' ' || i[l] == b'\r' {
|
||||||
|
return EntryParseStage::Done(l);
|
||||||
|
} else if i[l] != b'/' {
|
||||||
|
return EntryParseStage::Fail(nom::Err::Error(b"Entry name path is corrupted"));
|
||||||
|
}
|
||||||
|
for j in 1..(i.len() - l) {
|
||||||
|
if !is_entry_component_char(i[l + j]) {
|
||||||
|
return EntryParseStage::Path(l + j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntryParseStage::Done(i.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
let mut stage = EntryParseStage::PrivateShared(0);
|
||||||
|
loop {
|
||||||
|
match stage {
|
||||||
|
EntryParseStage::PrivateShared(_) => {
|
||||||
|
stage = check_private_shared(i);
|
||||||
|
}
|
||||||
|
EntryParseStage::Admin(l) => {
|
||||||
|
stage = check_admin(i, l);
|
||||||
|
}
|
||||||
|
EntryParseStage::VendorComment(l) => {
|
||||||
|
stage = check_vendor_comment(i, l);
|
||||||
|
}
|
||||||
|
EntryParseStage::Path(l) => {
|
||||||
|
stage = check_path(i, l);
|
||||||
|
}
|
||||||
|
EntryParseStage::Done(l) => {
|
||||||
|
return Ok((&i[l..], &i[..l]));
|
||||||
|
}
|
||||||
|
EntryParseStage::Fail(nom::Err::Error(err_msg)) => {
|
||||||
|
return std::result::Result::Err(nom::Err::Error(nom::error::Error::new(
|
||||||
|
err_msg,
|
||||||
|
nom::error::ErrorKind::Verify,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
EntryParseStage::Fail(nom::Err::Incomplete(reason)) => {
|
||||||
|
return std::result::Result::Err(nom::Err::Incomplete(reason));
|
||||||
|
}
|
||||||
|
_ => panic!("Entry name verification failure"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
let astring_res = astring(i)?;
|
||||||
|
check_entry_name(astring_res.1)?;
|
||||||
|
Ok(astring_res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice_to_str(i: &[u8]) -> &str {
|
||||||
|
std::str::from_utf8(i).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nil_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
|
||||||
|
map_opt(tag_no_case("NIL"), |_| None)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
|
||||||
|
map(alt((quoted, literal)), |s| {
|
||||||
|
Some(slice_to_str(s).to_string())
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyval_list(i: &[u8]) -> IResult<&[u8], Vec<Metadata>> {
|
||||||
|
parenthesized_nonempty_list(map(
|
||||||
|
tuple((
|
||||||
|
map(entry_name, slice_to_str),
|
||||||
|
tag(" "),
|
||||||
|
alt((nil_value, string_value)),
|
||||||
|
)),
|
||||||
|
|(key, _, value)| Metadata {
|
||||||
|
entry: key.to_string(),
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<str>>> {
|
||||||
|
separated_list0(tag(" "), map(map(entry_name, slice_to_str), Cow::Borrowed))(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_common(i: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
let (i, (_, mbox, _)) = tuple((tag_no_case("METADATA "), quoted, tag(" ")))(i)?;
|
||||||
|
Ok((i, mbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// [RFC5464 - 4.4.1 METADATA Response with values]
|
||||||
|
pub(crate) fn metadata_solicited(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
let (i, (mailbox, values)) = tuple((metadata_common, keyval_list))(i)?;
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
Response::MailboxData(MailboxDatum::MetadataSolicited {
|
||||||
|
mailbox: Cow::Borrowed(slice_to_str(mailbox)),
|
||||||
|
values,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// [RFC5464 - 4.4.2 Unsolicited METADATA Response without values]
|
||||||
|
pub(crate) fn metadata_unsolicited(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
let (i, (mailbox, values)) = tuple((metadata_common, entry_list))(i)?;
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
Response::MailboxData(MailboxDatum::MetadataUnsolicited {
|
||||||
|
mailbox: Cow::Borrowed(slice_to_str(mailbox)),
|
||||||
|
values,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are any entries with values larger than the MAXSIZE limit given to GETMETADATA.
|
||||||
|
// Extends resp-test-code defined in rfc3501.
|
||||||
|
// [RFC5464 - 4.2.1 MAXSIZE GETMETADATA Command Option](https://tools.ietf.org/html/rfc5464#section-4.2.1)
|
||||||
|
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
|
||||||
|
pub(crate) fn resp_text_code_metadata_long_entries(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
let (i, (_, num)) = tuple((tag_no_case("METADATA LONGENTRIES "), number_64))(i)?;
|
||||||
|
Ok((i, ResponseCode::MetadataLongEntries(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is unable to set an annotation because the size of its value is too large.
|
||||||
|
// Extends resp-test-code defined in rfc3501.
|
||||||
|
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
|
||||||
|
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
|
||||||
|
pub(crate) fn resp_text_code_metadata_max_size(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
let (i, (_, num)) = tuple((tag_no_case("METADATA MAXSIZE "), number_64))(i)?;
|
||||||
|
Ok((i, ResponseCode::MetadataMaxSize(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is unable to set a new annotation because the maximum number of allowed annotations has already been reached.
|
||||||
|
// Extends resp-test-code defined in rfc3501.
|
||||||
|
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
|
||||||
|
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
|
||||||
|
pub(crate) fn resp_text_code_metadata_too_many(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
let (i, _) = tag_no_case("METADATA TOOMANY")(i)?;
|
||||||
|
Ok((i, ResponseCode::MetadataTooMany))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is unable to set a new annotation because it does not support private annotations on one of the specified mailboxes.
|
||||||
|
// Extends resp-test-code defined in rfc3501.
|
||||||
|
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
|
||||||
|
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
|
||||||
|
pub(crate) fn resp_text_code_metadata_no_private(i: &[u8]) -> IResult<&[u8], ResponseCode> {
|
||||||
|
let (i, _) = tag_no_case("METADATA NOPRIVATE")(i)?;
|
||||||
|
Ok((i, ResponseCode::MetadataNoPrivate))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{metadata_solicited, metadata_unsolicited};
|
||||||
|
use crate::types::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_solicited_fail_1() {
|
||||||
|
match metadata_solicited(b"METADATA \"\" (/asdfg \"asdf\")\r\n") {
|
||||||
|
Err(_) => {}
|
||||||
|
_ => panic!("Error required when entry name is not starting with /private or /shared"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_solicited_fail_2() {
|
||||||
|
match metadata_solicited(b"METADATA \"\" (/shared/asdfg \"asdf\")\r\n") {
|
||||||
|
Err(_) => {}
|
||||||
|
_ => panic!(
|
||||||
|
"Error required when in entry name /shared \
|
||||||
|
is not continued with /admin, /comment or /vendor"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_solicited_fail_3() {
|
||||||
|
match metadata_solicited(b"METADATA \"\" (/private/admin \"asdf\")\r\n") {
|
||||||
|
Err(_) => {}
|
||||||
|
_ => panic!(
|
||||||
|
"Error required when in entry name /private \
|
||||||
|
is not continued with /comment or /vendor"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_solicited_fail_4() {
|
||||||
|
match metadata_solicited(b"METADATA \"\" (/shared/vendor \"asdf\")\r\n") {
|
||||||
|
Err(_) => {}
|
||||||
|
_ => panic!("Error required when vendor name is not provided."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_solicited_success() {
|
||||||
|
match metadata_solicited(
|
||||||
|
b"METADATA \"mbox\" (/shared/vendor/vendorname \"asdf\" \
|
||||||
|
/private/comment/a \"bbb\")\r\n",
|
||||||
|
) {
|
||||||
|
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
|
||||||
|
assert_eq!(mailbox, "mbox");
|
||||||
|
assert_eq!(i, b"\r\n");
|
||||||
|
assert_eq!(values.len(), 2);
|
||||||
|
assert_eq!(values[0].entry, "/shared/vendor/vendorname");
|
||||||
|
assert_eq!(
|
||||||
|
values[0]
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.expect("None value is not expected"),
|
||||||
|
"asdf"
|
||||||
|
);
|
||||||
|
assert_eq!(values[1].entry, "/private/comment/a");
|
||||||
|
assert_eq!(
|
||||||
|
values[1]
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.expect("None value is not expected"),
|
||||||
|
"bbb"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("Correct METADATA response is not parsed properly."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_literal_success() {
|
||||||
|
// match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a \"AAA\")\r\n")
|
||||||
|
match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a {3}\r\nAAA)\r\n") {
|
||||||
|
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
|
||||||
|
assert_eq!(mailbox, "");
|
||||||
|
assert_eq!(i, b"\r\n");
|
||||||
|
assert_eq!(values.len(), 1);
|
||||||
|
assert_eq!(values[0].entry, "/shared/vendor/vendor.coi/a");
|
||||||
|
assert_eq!(
|
||||||
|
values[0]
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.expect("None value is not expected"),
|
||||||
|
"AAA"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => panic!("ERR: {:?}", e),
|
||||||
|
_ => panic!("Strange failure"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unsolicited_success() {
|
||||||
|
match metadata_unsolicited(b"METADATA \"theBox\" /shared/admin/qwe /private/comment/a\r\n")
|
||||||
|
{
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(i, b"\r\n");
|
||||||
|
assert_eq!(mailbox, "theBox");
|
||||||
|
assert_eq!(values.len(), 2);
|
||||||
|
assert_eq!(values[0], "/shared/admin/qwe");
|
||||||
|
assert_eq!(values[1], "/private/comment/a");
|
||||||
|
}
|
||||||
|
_ => panic!("Correct METADATA response is not parsed properly."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_codes() {
|
||||||
|
use crate::parser::parse_response;
|
||||||
|
|
||||||
|
match parse_response(b"* OK [METADATA LONGENTRIES 123] Some entries omitted.\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::MetadataLongEntries(123)),
|
||||||
|
information: Some(Cow::Borrowed("Some entries omitted.")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [METADATA MAXSIZE 123] Annotation too large.\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::MetadataMaxSize(123)),
|
||||||
|
information: Some(Cow::Borrowed("Annotation too large.")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [METADATA TOOMANY] Too many annotations.\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::MetadataTooMany),
|
||||||
|
information: Some(Cow::Borrowed("Too many annotations.")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [METADATA NOPRIVATE] Private annotations not supported.\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::MetadataNoPrivate),
|
||||||
|
information: Some(Cow::Borrowed("Private annotations not supported.")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/parser/rfc7162.rs
Normal file
36
src/parser/rfc7162.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! https://tools.ietf.org/html/rfc7162
|
||||||
|
//!
|
||||||
|
//! The IMAP QRESYNC Extensions
|
||||||
|
//!
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::streaming::tag_no_case, character::streaming::space1, combinator::opt, sequence::tuple,
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::parser::core::sequence_set;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
// The VANISHED response reports that the specified UIDs have been
|
||||||
|
// permanently removed from the mailbox. This response is similar to
|
||||||
|
// the EXPUNGE response (RFC3501); however, it can return information
|
||||||
|
// about multiple messages, and it returns UIDs instead of message
|
||||||
|
// numbers.
|
||||||
|
// [RFC7162 - VANISHED RESPONSE](https://tools.ietf.org/html/rfc7162#section-3.2.10)
|
||||||
|
pub(crate) fn resp_vanished(i: &[u8]) -> IResult<&[u8], Response> {
|
||||||
|
let (rest, (_, earlier, _, uids)) = tuple((
|
||||||
|
tag_no_case("VANISHED"),
|
||||||
|
opt(tuple((space1, tag_no_case("(EARLIER)")))),
|
||||||
|
space1,
|
||||||
|
sequence_set,
|
||||||
|
))(i)?;
|
||||||
|
Ok((
|
||||||
|
rest,
|
||||||
|
Response::Vanished {
|
||||||
|
earlier: earlier.is_some(),
|
||||||
|
uids,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
568
src/parser/tests.rs
Normal file
568
src/parser/tests.rs
Normal file
|
@ -0,0 +1,568 @@
|
||||||
|
use super::{bodystructure::BodyStructParser, parse_response};
|
||||||
|
use crate::types::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mailbox_data_response() {
|
||||||
|
match parse_response(b"* LIST (\\HasNoChildren) \".\" INBOX.Tests\r\n") {
|
||||||
|
Ok((_, Response::MailboxData(_))) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_number_overflow() {
|
||||||
|
match parse_response(b"* 2222222222222222222222222222222222222222222C\r\n") {
|
||||||
|
Err(_) => {}
|
||||||
|
_ => panic!("error required for integer overflow"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unseen() {
|
||||||
|
match parse_response(b"* OK [UNSEEN 3] Message 3 is first unseen\r\n").unwrap() {
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Unseen(3)),
|
||||||
|
information: Some(Cow::Borrowed("Message 3 is first unseen")),
|
||||||
|
},
|
||||||
|
) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_text() {
|
||||||
|
match parse_response(b"* 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n") {
|
||||||
|
Ok((_, Response::Fetch(_, attrs))) => {
|
||||||
|
let body = &attrs[0];
|
||||||
|
assert_eq!(
|
||||||
|
body,
|
||||||
|
&AttributeValue::BodySection {
|
||||||
|
section: Some(SectionPath::Full(MessageSection::Text)),
|
||||||
|
index: None,
|
||||||
|
data: Some(Cow::Borrowed(b"foo")),
|
||||||
|
},
|
||||||
|
"body = {:?}",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_structure() {
|
||||||
|
const RESPONSE: &[u8] = b"* 15 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"iso-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 1315 42 NIL NIL NIL NIL))\r\n";
|
||||||
|
match parse_response(RESPONSE) {
|
||||||
|
Ok((_, Response::Fetch(_, attrs))) => {
|
||||||
|
let body = &attrs[0];
|
||||||
|
assert!(
|
||||||
|
matches!(*body, AttributeValue::BodyStructure(_)),
|
||||||
|
"body = {:?}",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status() {
|
||||||
|
match parse_response(b"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n") {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Status { mailbox, status }))) => {
|
||||||
|
assert_eq!(mailbox, "blurdybloop");
|
||||||
|
assert_eq!(
|
||||||
|
status,
|
||||||
|
[
|
||||||
|
StatusAttribute::Messages(231),
|
||||||
|
StatusAttribute::UidNext(44292),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_notify() {
|
||||||
|
match parse_response(b"* 3501 EXPUNGE\r\n") {
|
||||||
|
Ok((_, Response::Expunge(3501))) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
match parse_response(b"* 3501 EXISTS\r\n") {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Exists(3501)))) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
match parse_response(b"+ idling\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Continue {
|
||||||
|
code: None,
|
||||||
|
information: Some(Cow::Borrowed("idling")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search() {
|
||||||
|
// also allow trailing whitespace in SEARCH responses
|
||||||
|
for empty_response in &["* SEARCH\r\n", "* SEARCH \r\n"] {
|
||||||
|
match parse_response(empty_response.as_bytes()) {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Search(ids)))) => {
|
||||||
|
assert!(ids.is_empty());
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for response in &["* SEARCH 12345 67890\r\n", "* SEARCH 12345 67890 \r\n"] {
|
||||||
|
match parse_response(response.as_bytes()) {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Search(ids)))) => {
|
||||||
|
assert_eq!(ids[0], 12345);
|
||||||
|
assert_eq!(ids[1], 67890);
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort() {
|
||||||
|
// also allow trailing whitespace in SEARCH responses
|
||||||
|
for empty_response in &["* SORT\r\n", "* SORT \r\n"] {
|
||||||
|
match parse_response(empty_response.as_bytes()) {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Sort(ids)))) => {
|
||||||
|
assert!(ids.is_empty());
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for response in &["* SORT 12345 67890\r\n", "* SORT 12345 67890 \r\n"] {
|
||||||
|
match parse_response(response.as_bytes()) {
|
||||||
|
Ok((_, Response::MailboxData(MailboxDatum::Sort(ids)))) => {
|
||||||
|
assert_eq!(ids[0], 12345);
|
||||||
|
assert_eq!(ids[1], 67890);
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uid_fetch() {
|
||||||
|
match parse_response(b"* 4 FETCH (UID 71372 RFC822.HEADER {10275}\r\n") {
|
||||||
|
Err(nom::Err::Incomplete(nom::Needed::Size(size))) => {
|
||||||
|
assert_eq!(size, NonZeroUsize::new(10275).unwrap());
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uid_fetch_extra_space() {
|
||||||
|
// DavMail inserts an extra space after RFC822.HEADER
|
||||||
|
match parse_response(b"* 4 FETCH (UID 71372 RFC822.HEADER {10275}\r\n") {
|
||||||
|
Err(nom::Err::Incomplete(nom::Needed::Size(size))) => {
|
||||||
|
assert_eq!(size, NonZeroUsize::new(10275).unwrap());
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_header_fields() {
|
||||||
|
const RESPONSE: &[u8] = b"* 1 FETCH (UID 1 BODY[HEADER.FIELDS (CHAT-VERSION)] {21}\r\nChat-Version: 1.0\r\n\r\n)\r\n";
|
||||||
|
|
||||||
|
match parse_response(RESPONSE) {
|
||||||
|
Ok((_, Response::Fetch(_, _))) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_codes() {
|
||||||
|
match parse_response(b"* OK [ALERT] Alert!\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Alert),
|
||||||
|
information: Some(Cow::Borrowed("Alert!")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [PARSE] Something\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::Parse),
|
||||||
|
information: Some(Cow::Borrowed("Something")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* OK [CAPABILITY IMAP4rev1 IDLE] Logged in\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Capabilities(c)),
|
||||||
|
information: Some(Cow::Borrowed("Logged in")),
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
assert_eq!(c.len(), 2);
|
||||||
|
assert_eq!(c[0], Capability::Imap4rev1);
|
||||||
|
assert_eq!(c[1], Capability::Atom(Cow::Borrowed("IDLE")));
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* OK [CAPABILITY UIDPLUS IMAP4rev1 IDLE] Logged in\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Capabilities(c)),
|
||||||
|
information: Some(Cow::Borrowed("Logged in")),
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
assert_eq!(c.len(), 3);
|
||||||
|
assert_eq!(c[0], Capability::Atom(Cow::Borrowed("UIDPLUS")));
|
||||||
|
assert_eq!(c[1], Capability::Imap4rev1);
|
||||||
|
assert_eq!(c[2], Capability::Atom(Cow::Borrowed("IDLE")));
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing IMAP4rev1
|
||||||
|
match parse_response(b"* OK [CAPABILITY UIDPLUS IDLE] Logged in\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: None,
|
||||||
|
information: Some(Cow::Borrowed("[CAPABILITY UIDPLUS IDLE] Logged in")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [BADCHARSET] error\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::BadCharset(None)),
|
||||||
|
information: Some(Cow::Borrowed("error")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [BADCHARSET (utf-8 latin1)] error\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::BadCharset(Some(v))),
|
||||||
|
information: Some(Cow::Borrowed("error")),
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
assert_eq!(v.len(), 2);
|
||||||
|
assert_eq!(v[0], "utf-8");
|
||||||
|
assert_eq!(v[1], "latin1");
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* NO [BADCHARSET ()] error\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: None,
|
||||||
|
information: Some(Cow::Borrowed("[BADCHARSET ()] error")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_incomplete_fetch() {
|
||||||
|
match parse_response(b"* 4644 FETCH (UID ") {
|
||||||
|
Err(nom::Err::Incomplete(_)) => {}
|
||||||
|
rsp => panic!("should be incomplete: {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_continuation() {
|
||||||
|
// regular RFC compliant
|
||||||
|
match parse_response(b"+ \r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Continue {
|
||||||
|
code: None,
|
||||||
|
information: None,
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
// short version, sent by yandex
|
||||||
|
match parse_response(b"+\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Continue {
|
||||||
|
code: None,
|
||||||
|
information: None,
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enabled() {
|
||||||
|
match parse_response(b"* ENABLED QRESYNC X-GOOD-IDEA\r\n") {
|
||||||
|
Ok((_, capabilities)) => assert_eq!(
|
||||||
|
capabilities,
|
||||||
|
Response::Capabilities(vec![
|
||||||
|
Capability::Atom(Cow::Borrowed("QRESYNC")),
|
||||||
|
Capability::Atom(Cow::Borrowed("X-GOOD-IDEA")),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flags() {
|
||||||
|
// Invalid response (FLAGS can't include \*) from Zoho Mail server.
|
||||||
|
//
|
||||||
|
// As a workaround, such response is parsed without error.
|
||||||
|
match parse_response(b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)\r\n") {
|
||||||
|
Ok((_, capabilities)) => assert_eq!(
|
||||||
|
capabilities,
|
||||||
|
Response::MailboxData(MailboxDatum::Flags(vec![
|
||||||
|
Cow::Borrowed("\\Answered"),
|
||||||
|
Cow::Borrowed("\\Flagged"),
|
||||||
|
Cow::Borrowed("\\Deleted"),
|
||||||
|
Cow::Borrowed("\\Seen"),
|
||||||
|
Cow::Borrowed("\\Draft"),
|
||||||
|
Cow::Borrowed("\\*")
|
||||||
|
]))
|
||||||
|
),
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vanished() {
|
||||||
|
match parse_response(b"* VANISHED (EARLIER) 1,2,3:8\r\n") {
|
||||||
|
Ok((_, Response::Vanished { earlier, uids })) => {
|
||||||
|
assert!(earlier);
|
||||||
|
assert_eq!(uids.len(), 3);
|
||||||
|
let v = &uids[0];
|
||||||
|
assert_eq!(*v.start(), 1);
|
||||||
|
assert_eq!(*v.end(), 1);
|
||||||
|
let v = &uids[1];
|
||||||
|
assert_eq!(*v.start(), 2);
|
||||||
|
assert_eq!(*v.end(), 2);
|
||||||
|
let v = &uids[2];
|
||||||
|
assert_eq!(*v.start(), 3);
|
||||||
|
assert_eq!(*v.end(), 8);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* VANISHED 1,2,3:8,10\r\n") {
|
||||||
|
Ok((_, Response::Vanished { earlier, uids })) => {
|
||||||
|
assert!(!earlier);
|
||||||
|
assert_eq!(uids.len(), 4);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* VANISHED (EARLIER) 1\r\n") {
|
||||||
|
Ok((_, Response::Vanished { earlier, uids })) => {
|
||||||
|
assert!(earlier);
|
||||||
|
assert_eq!(uids.len(), 1);
|
||||||
|
assert_eq!(uids[0].clone().collect::<Vec<u32>>(), vec![1]);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_response(b"* VANISHED 1\r\n") {
|
||||||
|
Ok((_, Response::Vanished { earlier, uids })) => {
|
||||||
|
assert!(!earlier);
|
||||||
|
assert_eq!(uids.len(), 1);
|
||||||
|
}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(parse_response(b"* VANISHED \r\n").is_err());
|
||||||
|
assert!(parse_response(b"* VANISHED (EARLIER) \r\n").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uidplus() {
|
||||||
|
match dbg!(parse_response(
|
||||||
|
b"* OK [APPENDUID 38505 3955] APPEND completed\r\n"
|
||||||
|
)) {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::AppendUid(38505, uid_set)),
|
||||||
|
information: Some(Cow::Borrowed("APPEND completed")),
|
||||||
|
},
|
||||||
|
)) if uid_set == [3955.into()] => {}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
match dbg!(parse_response(
|
||||||
|
b"* OK [COPYUID 38505 304,319:320 3956:3958] Done\r\n"
|
||||||
|
)) {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::CopyUid(38505, uid_set_src, uid_set_dst)),
|
||||||
|
information: Some(Cow::Borrowed("Done")),
|
||||||
|
},
|
||||||
|
)) if uid_set_src == [304.into(), (319..=320).into()]
|
||||||
|
&& uid_set_dst == [(3956..=3958).into()] => {}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
match dbg!(parse_response(
|
||||||
|
b"* NO [UIDNOTSTICKY] Non-persistent UIDs\r\n"
|
||||||
|
)) {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::No,
|
||||||
|
code: Some(ResponseCode::UidNotSticky),
|
||||||
|
information: Some(Cow::Borrowed("Non-persistent UIDs")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("Unexpected response: {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_imap_body_structure() {
|
||||||
|
let test = b"\
|
||||||
|
* 1569 FETCH (\
|
||||||
|
BODYSTRUCTURE (\
|
||||||
|
(\
|
||||||
|
(\
|
||||||
|
(\
|
||||||
|
\"TEXT\" \"PLAIN\" \
|
||||||
|
(\"CHARSET\" \"ISO-8859-1\") NIL NIL \
|
||||||
|
\"QUOTED-PRINTABLE\" 833 30 NIL NIL NIL\
|
||||||
|
)\
|
||||||
|
(\
|
||||||
|
\"TEXT\" \"HTML\" \
|
||||||
|
(\"CHARSET\" \"ISO-8859-1\") NIL NIL \
|
||||||
|
\"QUOTED-PRINTABLE\" 3412 62 NIL \
|
||||||
|
(\"INLINE\" NIL) NIL\
|
||||||
|
) \
|
||||||
|
\"ALTERNATIVE\" (\"BOUNDARY\" \"2__=fgrths\") NIL NIL\
|
||||||
|
)\
|
||||||
|
(\
|
||||||
|
\"IMAGE\" \"GIF\" \
|
||||||
|
(\"NAME\" \"485039.gif\") \"<2__=lgkfjr>\" NIL \
|
||||||
|
\"BASE64\" 64 NIL (\"INLINE\" (\"FILENAME\" \"485039.gif\")) \
|
||||||
|
NIL\
|
||||||
|
) \
|
||||||
|
\"RELATED\" (\"BOUNDARY\" \"1__=fgrths\") NIL NIL\
|
||||||
|
)\
|
||||||
|
(\
|
||||||
|
\"APPLICATION\" \"PDF\" \
|
||||||
|
(\"NAME\" \"title.pdf\") \
|
||||||
|
\"<1__=lgkfjr>\" NIL \"BASE64\" 333980 NIL \
|
||||||
|
(\"ATTACHMENT\" (\"FILENAME\" \"title.pdf\")) NIL\
|
||||||
|
) \
|
||||||
|
\"MIXED\" (\"BOUNDARY\" \"0__=fgrths\") NIL NIL\
|
||||||
|
)\
|
||||||
|
)\r\n";
|
||||||
|
|
||||||
|
let (_, resp) = parse_response(test).unwrap();
|
||||||
|
match resp {
|
||||||
|
Response::Fetch(_, f) => {
|
||||||
|
let bodystructure = f
|
||||||
|
.iter()
|
||||||
|
.flat_map(|f| match f {
|
||||||
|
AttributeValue::BodyStructure(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let parser = BodyStructParser::new(bodystructure);
|
||||||
|
|
||||||
|
let element = parser.search(|b: &BodyStructure| {
|
||||||
|
matches!(b, BodyStructure::Basic { ref common, .. } if common.ty.ty == "APPLICATION")
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(element, Some(vec![2]));
|
||||||
|
}
|
||||||
|
_ => panic!("invalid FETCH command test"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parsing_of_quota_capability_in_login_response() {
|
||||||
|
match parse_response(b"* OK [CAPABILITY IMAP4rev1 IDLE QUOTA] Logged in\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Capabilities(c)),
|
||||||
|
information: Some(Cow::Borrowed("Logged in")),
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
assert_eq!(c.len(), 3);
|
||||||
|
assert_eq!(c[0], Capability::Imap4rev1);
|
||||||
|
assert_eq!(c[1], Capability::Atom(Cow::Borrowed("IDLE")));
|
||||||
|
assert_eq!(c[2], Capability::Atom(Cow::Borrowed("QUOTA")));
|
||||||
|
}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parsing_of_bye_response() {
|
||||||
|
match parse_response(b"* BYE\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Bye,
|
||||||
|
code: None,
|
||||||
|
information: None,
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
};
|
||||||
|
match parse_response(b"* BYE Autologout; idle for too long\r\n") {
|
||||||
|
Ok((
|
||||||
|
_,
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Bye,
|
||||||
|
code: None,
|
||||||
|
information: Some(Cow::Borrowed("Autologout; idle for too long")),
|
||||||
|
},
|
||||||
|
)) => {}
|
||||||
|
rsp => panic!("unexpected response {:?}", rsp),
|
||||||
|
};
|
||||||
|
}
|
793
src/types.rs
Normal file
793
src/types.rs
Normal file
|
@ -0,0 +1,793 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
fn to_owned_cow<'a, T: ?Sized + ToOwned>(c: Cow<'a, T>) -> Cow<'static, T> {
|
||||||
|
Cow::Owned(c.into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Request<'a>(pub Cow<'a, [u8]>, pub Cow<'a, [u8]>);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AttrMacro {
|
||||||
|
All,
|
||||||
|
Fast,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Response<'a> {
|
||||||
|
Capabilities(Vec<Capability<'a>>),
|
||||||
|
Continue {
|
||||||
|
code: Option<ResponseCode<'a>>,
|
||||||
|
information: Option<Cow<'a, str>>,
|
||||||
|
},
|
||||||
|
Done {
|
||||||
|
tag: RequestId,
|
||||||
|
status: Status,
|
||||||
|
code: Option<ResponseCode<'a>>,
|
||||||
|
information: Option<Cow<'a, str>>,
|
||||||
|
},
|
||||||
|
Data {
|
||||||
|
status: Status,
|
||||||
|
code: Option<ResponseCode<'a>>,
|
||||||
|
information: Option<Cow<'a, str>>,
|
||||||
|
},
|
||||||
|
Expunge(u32),
|
||||||
|
Vanished {
|
||||||
|
earlier: bool,
|
||||||
|
uids: Vec<std::ops::RangeInclusive<u32>>,
|
||||||
|
},
|
||||||
|
Fetch(u32, Vec<AttributeValue<'a>>),
|
||||||
|
MailboxData(MailboxDatum<'a>),
|
||||||
|
Quota(Quota<'a>),
|
||||||
|
QuotaRoot(QuotaRoot<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Response<'a> {
|
||||||
|
pub fn from_bytes(buf: &'a [u8]) -> crate::ParseResult {
|
||||||
|
crate::parser::parse_response(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_owned(self) -> Response<'static> {
|
||||||
|
match self {
|
||||||
|
Response::Capabilities(capabilities) => Response::Capabilities(
|
||||||
|
capabilities
|
||||||
|
.into_iter()
|
||||||
|
.map(Capability::into_owned)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
Response::Continue { code, information } => Response::Continue {
|
||||||
|
code: code.map(ResponseCode::into_owned),
|
||||||
|
information: information.map(to_owned_cow),
|
||||||
|
},
|
||||||
|
Response::Done {
|
||||||
|
tag,
|
||||||
|
status,
|
||||||
|
code,
|
||||||
|
information,
|
||||||
|
} => Response::Done {
|
||||||
|
tag,
|
||||||
|
status,
|
||||||
|
code: code.map(ResponseCode::into_owned),
|
||||||
|
information: information.map(to_owned_cow),
|
||||||
|
},
|
||||||
|
Response::Data {
|
||||||
|
status,
|
||||||
|
code,
|
||||||
|
information,
|
||||||
|
} => Response::Data {
|
||||||
|
status,
|
||||||
|
code: code.map(ResponseCode::into_owned),
|
||||||
|
information: information.map(to_owned_cow),
|
||||||
|
},
|
||||||
|
Response::Expunge(seq) => Response::Expunge(seq),
|
||||||
|
Response::Vanished { earlier, uids } => Response::Vanished { earlier, uids },
|
||||||
|
Response::Fetch(seq, attrs) => Response::Fetch(
|
||||||
|
seq,
|
||||||
|
attrs.into_iter().map(AttributeValue::into_owned).collect(),
|
||||||
|
),
|
||||||
|
Response::MailboxData(datum) => Response::MailboxData(datum.into_owned()),
|
||||||
|
Response::Quota(quota) => Response::Quota(quota.into_owned()),
|
||||||
|
Response::QuotaRoot(quota_root) => Response::QuotaRoot(quota_root.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum Status {
|
||||||
|
Ok,
|
||||||
|
No,
|
||||||
|
Bad,
|
||||||
|
PreAuth,
|
||||||
|
Bye,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ResponseCode<'a> {
|
||||||
|
Alert,
|
||||||
|
BadCharset(Option<Vec<Cow<'a, str>>>),
|
||||||
|
Capabilities(Vec<Capability<'a>>),
|
||||||
|
HighestModSeq(u64), // RFC 4551, section 3.1.1
|
||||||
|
Parse,
|
||||||
|
PermanentFlags(Vec<Cow<'a, str>>),
|
||||||
|
ReadOnly,
|
||||||
|
ReadWrite,
|
||||||
|
TryCreate,
|
||||||
|
UidNext(u32),
|
||||||
|
UidValidity(u32),
|
||||||
|
Unseen(u32),
|
||||||
|
AppendUid(u32, Vec<UidSetMember>),
|
||||||
|
CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
|
||||||
|
UidNotSticky,
|
||||||
|
MetadataLongEntries(u64), // RFC 5464, section 4.2.1
|
||||||
|
MetadataMaxSize(u64), // RFC 5464, section 4.3
|
||||||
|
MetadataTooMany, // RFC 5464, section 4.3
|
||||||
|
MetadataNoPrivate, // RFC 5464, section 4.3
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum UidSetMember {
|
||||||
|
UidRange(RangeInclusive<u32>),
|
||||||
|
Uid(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeInclusive<u32>> for UidSetMember {
|
||||||
|
fn from(x: RangeInclusive<u32>) -> Self {
|
||||||
|
UidSetMember::UidRange(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for UidSetMember {
|
||||||
|
fn from(x: u32) -> Self {
|
||||||
|
UidSetMember::Uid(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ResponseCode<'a> {
|
||||||
|
pub fn into_owned(self) -> ResponseCode<'static> {
|
||||||
|
match self {
|
||||||
|
ResponseCode::Alert => ResponseCode::Alert,
|
||||||
|
ResponseCode::BadCharset(v) => {
|
||||||
|
ResponseCode::BadCharset(v.map(|vs| vs.into_iter().map(to_owned_cow).collect()))
|
||||||
|
}
|
||||||
|
ResponseCode::Capabilities(v) => {
|
||||||
|
ResponseCode::Capabilities(v.into_iter().map(Capability::into_owned).collect())
|
||||||
|
}
|
||||||
|
ResponseCode::HighestModSeq(v) => ResponseCode::HighestModSeq(v),
|
||||||
|
ResponseCode::Parse => ResponseCode::Parse,
|
||||||
|
ResponseCode::PermanentFlags(v) => {
|
||||||
|
ResponseCode::PermanentFlags(v.into_iter().map(to_owned_cow).collect())
|
||||||
|
}
|
||||||
|
ResponseCode::ReadOnly => ResponseCode::ReadOnly,
|
||||||
|
ResponseCode::ReadWrite => ResponseCode::ReadWrite,
|
||||||
|
ResponseCode::TryCreate => ResponseCode::TryCreate,
|
||||||
|
ResponseCode::UidNext(v) => ResponseCode::UidNext(v),
|
||||||
|
ResponseCode::UidValidity(v) => ResponseCode::UidValidity(v),
|
||||||
|
ResponseCode::Unseen(v) => ResponseCode::Unseen(v),
|
||||||
|
ResponseCode::AppendUid(a, b) => ResponseCode::AppendUid(a, b),
|
||||||
|
ResponseCode::CopyUid(a, b, c) => ResponseCode::CopyUid(a, b, c),
|
||||||
|
ResponseCode::UidNotSticky => ResponseCode::UidNotSticky,
|
||||||
|
ResponseCode::MetadataLongEntries(v) => ResponseCode::MetadataLongEntries(v),
|
||||||
|
ResponseCode::MetadataMaxSize(v) => ResponseCode::MetadataMaxSize(v),
|
||||||
|
ResponseCode::MetadataTooMany => ResponseCode::MetadataTooMany,
|
||||||
|
ResponseCode::MetadataNoPrivate => ResponseCode::MetadataNoPrivate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum StatusAttribute {
|
||||||
|
HighestModSeq(u64), // RFC 4551
|
||||||
|
Messages(u32),
|
||||||
|
Recent(u32),
|
||||||
|
UidNext(u32),
|
||||||
|
UidValidity(u32),
|
||||||
|
Unseen(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub struct Metadata {
|
||||||
|
pub entry: String,
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum MailboxDatum<'a> {
|
||||||
|
Exists(u32),
|
||||||
|
Flags(Vec<Cow<'a, str>>),
|
||||||
|
List {
|
||||||
|
flags: Vec<Cow<'a, str>>,
|
||||||
|
delimiter: Option<Cow<'a, str>>,
|
||||||
|
name: Cow<'a, str>,
|
||||||
|
},
|
||||||
|
Search(Vec<u32>),
|
||||||
|
Sort(Vec<u32>),
|
||||||
|
Status {
|
||||||
|
mailbox: Cow<'a, str>,
|
||||||
|
status: Vec<StatusAttribute>,
|
||||||
|
},
|
||||||
|
Recent(u32),
|
||||||
|
MetadataSolicited {
|
||||||
|
mailbox: Cow<'a, str>,
|
||||||
|
values: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
MetadataUnsolicited {
|
||||||
|
mailbox: Cow<'a, str>,
|
||||||
|
values: Vec<Cow<'a, str>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MailboxDatum<'a> {
|
||||||
|
pub fn into_owned(self) -> MailboxDatum<'static> {
|
||||||
|
match self {
|
||||||
|
MailboxDatum::Exists(seq) => MailboxDatum::Exists(seq),
|
||||||
|
MailboxDatum::Flags(flags) => {
|
||||||
|
MailboxDatum::Flags(flags.into_iter().map(to_owned_cow).collect())
|
||||||
|
}
|
||||||
|
MailboxDatum::List {
|
||||||
|
flags,
|
||||||
|
delimiter,
|
||||||
|
name,
|
||||||
|
} => MailboxDatum::List {
|
||||||
|
flags: flags.into_iter().map(to_owned_cow).collect(),
|
||||||
|
delimiter: delimiter.map(to_owned_cow),
|
||||||
|
name: to_owned_cow(name),
|
||||||
|
},
|
||||||
|
MailboxDatum::Search(seqs) => MailboxDatum::Search(seqs),
|
||||||
|
MailboxDatum::Sort(seqs) => MailboxDatum::Sort(seqs),
|
||||||
|
MailboxDatum::Status { mailbox, status } => MailboxDatum::Status {
|
||||||
|
mailbox: to_owned_cow(mailbox),
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
MailboxDatum::Recent(seq) => MailboxDatum::Recent(seq),
|
||||||
|
MailboxDatum::MetadataSolicited { mailbox, values } => {
|
||||||
|
MailboxDatum::MetadataSolicited {
|
||||||
|
mailbox: to_owned_cow(mailbox),
|
||||||
|
values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MailboxDatum::MetadataUnsolicited { mailbox, values } => {
|
||||||
|
MailboxDatum::MetadataUnsolicited {
|
||||||
|
mailbox: to_owned_cow(mailbox),
|
||||||
|
values: values.into_iter().map(to_owned_cow).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub enum Capability<'a> {
|
||||||
|
Imap4rev1,
|
||||||
|
Auth(Cow<'a, str>),
|
||||||
|
Atom(Cow<'a, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Capability<'a> {
|
||||||
|
pub fn into_owned(self) -> Capability<'static> {
|
||||||
|
match self {
|
||||||
|
Capability::Imap4rev1 => Capability::Imap4rev1,
|
||||||
|
Capability::Auth(v) => Capability::Auth(to_owned_cow(v)),
|
||||||
|
Capability::Atom(v) => Capability::Atom(to_owned_cow(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Attribute {
|
||||||
|
Body,
|
||||||
|
Envelope,
|
||||||
|
Flags,
|
||||||
|
InternalDate,
|
||||||
|
ModSeq, // RFC 4551, section 3.3.2
|
||||||
|
Rfc822,
|
||||||
|
Rfc822Size,
|
||||||
|
Rfc822Text,
|
||||||
|
Uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum MessageSection {
|
||||||
|
Header,
|
||||||
|
Mime,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum SectionPath {
|
||||||
|
Full(MessageSection),
|
||||||
|
Part(Vec<u32>, Option<MessageSection>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum AttributeValue<'a> {
|
||||||
|
BodySection {
|
||||||
|
section: Option<SectionPath>,
|
||||||
|
index: Option<u32>,
|
||||||
|
data: Option<Cow<'a, [u8]>>,
|
||||||
|
},
|
||||||
|
BodyStructure(BodyStructure<'a>),
|
||||||
|
Envelope(Box<Envelope<'a>>),
|
||||||
|
Flags(Vec<Cow<'a, str>>),
|
||||||
|
InternalDate(Cow<'a, str>),
|
||||||
|
ModSeq(u64), // RFC 4551, section 3.3.2
|
||||||
|
Rfc822(Option<Cow<'a, [u8]>>),
|
||||||
|
Rfc822Header(Option<Cow<'a, [u8]>>),
|
||||||
|
Rfc822Size(u32),
|
||||||
|
Rfc822Text(Option<Cow<'a, [u8]>>),
|
||||||
|
Uid(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AttributeValue<'a> {
|
||||||
|
pub fn into_owned(self) -> AttributeValue<'static> {
|
||||||
|
match self {
|
||||||
|
AttributeValue::BodySection {
|
||||||
|
section,
|
||||||
|
index,
|
||||||
|
data,
|
||||||
|
} => AttributeValue::BodySection {
|
||||||
|
section,
|
||||||
|
index,
|
||||||
|
data: data.map(to_owned_cow),
|
||||||
|
},
|
||||||
|
AttributeValue::BodyStructure(body) => AttributeValue::BodyStructure(body.into_owned()),
|
||||||
|
AttributeValue::Envelope(e) => AttributeValue::Envelope(Box::new(e.into_owned())),
|
||||||
|
AttributeValue::Flags(v) => {
|
||||||
|
AttributeValue::Flags(v.into_iter().map(to_owned_cow).collect())
|
||||||
|
}
|
||||||
|
AttributeValue::InternalDate(v) => AttributeValue::InternalDate(to_owned_cow(v)),
|
||||||
|
AttributeValue::ModSeq(v) => AttributeValue::ModSeq(v),
|
||||||
|
AttributeValue::Rfc822(v) => AttributeValue::Rfc822(v.map(to_owned_cow)),
|
||||||
|
AttributeValue::Rfc822Header(v) => AttributeValue::Rfc822Header(v.map(to_owned_cow)),
|
||||||
|
AttributeValue::Rfc822Size(v) => AttributeValue::Rfc822Size(v),
|
||||||
|
AttributeValue::Rfc822Text(v) => AttributeValue::Rfc822Text(v.map(to_owned_cow)),
|
||||||
|
AttributeValue::Uid(v) => AttributeValue::Uid(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum BodyStructure<'a> {
|
||||||
|
Basic {
|
||||||
|
common: BodyContentCommon<'a>,
|
||||||
|
other: BodyContentSinglePart<'a>,
|
||||||
|
extension: Option<BodyExtension<'a>>,
|
||||||
|
},
|
||||||
|
Text {
|
||||||
|
common: BodyContentCommon<'a>,
|
||||||
|
other: BodyContentSinglePart<'a>,
|
||||||
|
lines: u32,
|
||||||
|
extension: Option<BodyExtension<'a>>,
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
common: BodyContentCommon<'a>,
|
||||||
|
other: BodyContentSinglePart<'a>,
|
||||||
|
envelope: Envelope<'a>,
|
||||||
|
body: Box<BodyStructure<'a>>,
|
||||||
|
lines: u32,
|
||||||
|
extension: Option<BodyExtension<'a>>,
|
||||||
|
},
|
||||||
|
Multipart {
|
||||||
|
common: BodyContentCommon<'a>,
|
||||||
|
bodies: Vec<BodyStructure<'a>>,
|
||||||
|
extension: Option<BodyExtension<'a>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyStructure<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyStructure<'static> {
|
||||||
|
match self {
|
||||||
|
BodyStructure::Basic {
|
||||||
|
common,
|
||||||
|
other,
|
||||||
|
extension,
|
||||||
|
} => BodyStructure::Basic {
|
||||||
|
common: common.into_owned(),
|
||||||
|
other: other.into_owned(),
|
||||||
|
extension: extension.map(|v| v.into_owned()),
|
||||||
|
},
|
||||||
|
BodyStructure::Text {
|
||||||
|
common,
|
||||||
|
other,
|
||||||
|
lines,
|
||||||
|
extension,
|
||||||
|
} => BodyStructure::Text {
|
||||||
|
common: common.into_owned(),
|
||||||
|
other: other.into_owned(),
|
||||||
|
lines,
|
||||||
|
extension: extension.map(|v| v.into_owned()),
|
||||||
|
},
|
||||||
|
BodyStructure::Message {
|
||||||
|
common,
|
||||||
|
other,
|
||||||
|
envelope,
|
||||||
|
body,
|
||||||
|
lines,
|
||||||
|
extension,
|
||||||
|
} => BodyStructure::Message {
|
||||||
|
common: common.into_owned(),
|
||||||
|
other: other.into_owned(),
|
||||||
|
envelope: envelope.into_owned(),
|
||||||
|
body: Box::new(body.into_owned()),
|
||||||
|
lines,
|
||||||
|
extension: extension.map(|v| v.into_owned()),
|
||||||
|
},
|
||||||
|
BodyStructure::Multipart {
|
||||||
|
common,
|
||||||
|
bodies,
|
||||||
|
extension,
|
||||||
|
} => BodyStructure::Multipart {
|
||||||
|
common: common.into_owned(),
|
||||||
|
bodies: bodies.into_iter().map(|v| v.into_owned()).collect(),
|
||||||
|
extension: extension.map(|v| v.into_owned()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct BodyContentCommon<'a> {
|
||||||
|
pub ty: ContentType<'a>,
|
||||||
|
pub disposition: Option<ContentDisposition<'a>>,
|
||||||
|
pub language: Option<Vec<Cow<'a, str>>>,
|
||||||
|
pub location: Option<Cow<'a, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyContentCommon<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyContentCommon<'static> {
|
||||||
|
BodyContentCommon {
|
||||||
|
ty: self.ty.into_owned(),
|
||||||
|
disposition: self.disposition.map(|v| v.into_owned()),
|
||||||
|
language: self
|
||||||
|
.language
|
||||||
|
.map(|v| v.into_iter().map(to_owned_cow).collect()),
|
||||||
|
location: self.location.map(to_owned_cow),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct BodyContentSinglePart<'a> {
|
||||||
|
pub id: Option<Cow<'a, str>>,
|
||||||
|
pub md5: Option<Cow<'a, str>>,
|
||||||
|
pub description: Option<Cow<'a, str>>,
|
||||||
|
pub transfer_encoding: ContentEncoding<'a>,
|
||||||
|
pub octets: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyContentSinglePart<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyContentSinglePart<'static> {
|
||||||
|
BodyContentSinglePart {
|
||||||
|
id: self.id.map(to_owned_cow),
|
||||||
|
md5: self.md5.map(to_owned_cow),
|
||||||
|
description: self.description.map(to_owned_cow),
|
||||||
|
transfer_encoding: self.transfer_encoding.into_owned(),
|
||||||
|
octets: self.octets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct ContentType<'a> {
|
||||||
|
pub ty: Cow<'a, str>,
|
||||||
|
pub subtype: Cow<'a, str>,
|
||||||
|
pub params: BodyParams<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ContentType<'a> {
|
||||||
|
pub fn into_owned(self) -> ContentType<'static> {
|
||||||
|
ContentType {
|
||||||
|
ty: to_owned_cow(self.ty),
|
||||||
|
subtype: to_owned_cow(self.subtype),
|
||||||
|
params: body_param_owned(self.params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct ContentDisposition<'a> {
|
||||||
|
pub ty: Cow<'a, str>,
|
||||||
|
pub params: BodyParams<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ContentDisposition<'a> {
|
||||||
|
pub fn into_owned(self) -> ContentDisposition<'static> {
|
||||||
|
ContentDisposition {
|
||||||
|
ty: to_owned_cow(self.ty),
|
||||||
|
params: body_param_owned(self.params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum ContentEncoding<'a> {
|
||||||
|
SevenBit,
|
||||||
|
EightBit,
|
||||||
|
Binary,
|
||||||
|
Base64,
|
||||||
|
QuotedPrintable,
|
||||||
|
Other(Cow<'a, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ContentEncoding<'a> {
|
||||||
|
pub fn into_owned(self) -> ContentEncoding<'static> {
|
||||||
|
match self {
|
||||||
|
ContentEncoding::SevenBit => ContentEncoding::SevenBit,
|
||||||
|
ContentEncoding::EightBit => ContentEncoding::EightBit,
|
||||||
|
ContentEncoding::Binary => ContentEncoding::Binary,
|
||||||
|
ContentEncoding::Base64 => ContentEncoding::Base64,
|
||||||
|
ContentEncoding::QuotedPrintable => ContentEncoding::QuotedPrintable,
|
||||||
|
ContentEncoding::Other(v) => ContentEncoding::Other(to_owned_cow(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum BodyExtension<'a> {
|
||||||
|
Num(u32),
|
||||||
|
Str(Option<Cow<'a, str>>),
|
||||||
|
List(Vec<BodyExtension<'a>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyExtension<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyExtension<'static> {
|
||||||
|
match self {
|
||||||
|
BodyExtension::Num(v) => BodyExtension::Num(v),
|
||||||
|
BodyExtension::Str(v) => BodyExtension::Str(v.map(to_owned_cow)),
|
||||||
|
BodyExtension::List(v) => {
|
||||||
|
BodyExtension::List(v.into_iter().map(|v| v.into_owned()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BodyParams<'a> = Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>;
|
||||||
|
|
||||||
|
fn body_param_owned<'a>(v: BodyParams<'a>) -> BodyParams<'static> {
|
||||||
|
v.map(|v| {
|
||||||
|
v.into_iter()
|
||||||
|
.map(|(k, v)| (to_owned_cow(k), to_owned_cow(v)))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An RFC 2822 envelope
|
||||||
|
///
|
||||||
|
/// See https://datatracker.ietf.org/doc/html/rfc2822#section-3.6 for more details.
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct Envelope<'a> {
|
||||||
|
pub date: Option<Cow<'a, [u8]>>,
|
||||||
|
pub subject: Option<Cow<'a, [u8]>>,
|
||||||
|
/// Author of the message; mailbox responsible for writing the message
|
||||||
|
pub from: Option<Vec<Address<'a>>>,
|
||||||
|
/// Mailbox of the agent responsible for the message's transmission
|
||||||
|
pub sender: Option<Vec<Address<'a>>>,
|
||||||
|
/// Mailbox that the author of the message suggests replies be sent to
|
||||||
|
pub reply_to: Option<Vec<Address<'a>>>,
|
||||||
|
pub to: Option<Vec<Address<'a>>>,
|
||||||
|
pub cc: Option<Vec<Address<'a>>>,
|
||||||
|
pub bcc: Option<Vec<Address<'a>>>,
|
||||||
|
pub in_reply_to: Option<Cow<'a, [u8]>>,
|
||||||
|
pub message_id: Option<Cow<'a, [u8]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Envelope<'a> {
|
||||||
|
pub fn into_owned(self) -> Envelope<'static> {
|
||||||
|
Envelope {
|
||||||
|
date: self.date.map(to_owned_cow),
|
||||||
|
subject: self.subject.map(to_owned_cow),
|
||||||
|
from: self
|
||||||
|
.from
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
sender: self
|
||||||
|
.sender
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
reply_to: self
|
||||||
|
.reply_to
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
to: self
|
||||||
|
.to
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
cc: self
|
||||||
|
.cc
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
bcc: self
|
||||||
|
.bcc
|
||||||
|
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
|
||||||
|
in_reply_to: self.in_reply_to.map(to_owned_cow),
|
||||||
|
message_id: self.message_id.map(to_owned_cow),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct Address<'a> {
|
||||||
|
pub name: Option<Cow<'a, [u8]>>,
|
||||||
|
pub adl: Option<Cow<'a, [u8]>>,
|
||||||
|
pub mailbox: Option<Cow<'a, [u8]>>,
|
||||||
|
pub host: Option<Cow<'a, [u8]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Address<'a> {
|
||||||
|
pub fn into_owned(self) -> Address<'static> {
|
||||||
|
Address {
|
||||||
|
name: self.name.map(to_owned_cow),
|
||||||
|
adl: self.adl.map(to_owned_cow),
|
||||||
|
mailbox: self.mailbox.map(to_owned_cow),
|
||||||
|
host: self.host.map(to_owned_cow),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RequestId(pub String);
|
||||||
|
|
||||||
|
impl RequestId {
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
self.0.as_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
NotAuthenticated,
|
||||||
|
Authenticated,
|
||||||
|
Selected,
|
||||||
|
Logout,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body Structure
|
||||||
|
|
||||||
|
pub struct BodyFields<'a> {
|
||||||
|
pub param: BodyParams<'a>,
|
||||||
|
pub id: Option<Cow<'a, str>>,
|
||||||
|
pub description: Option<Cow<'a, str>>,
|
||||||
|
pub transfer_encoding: ContentEncoding<'a>,
|
||||||
|
pub octets: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyFields<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyFields<'static> {
|
||||||
|
BodyFields {
|
||||||
|
param: body_param_owned(self.param),
|
||||||
|
id: self.id.map(to_owned_cow),
|
||||||
|
description: self.description.map(to_owned_cow),
|
||||||
|
transfer_encoding: self.transfer_encoding.into_owned(),
|
||||||
|
octets: self.octets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BodyExt1Part<'a> {
|
||||||
|
pub md5: Option<Cow<'a, str>>,
|
||||||
|
pub disposition: Option<ContentDisposition<'a>>,
|
||||||
|
pub language: Option<Vec<Cow<'a, str>>>,
|
||||||
|
pub location: Option<Cow<'a, str>>,
|
||||||
|
pub extension: Option<BodyExtension<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyExt1Part<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyExt1Part<'static> {
|
||||||
|
BodyExt1Part {
|
||||||
|
md5: self.md5.map(to_owned_cow),
|
||||||
|
disposition: self.disposition.map(|v| v.into_owned()),
|
||||||
|
language: self
|
||||||
|
.language
|
||||||
|
.map(|v| v.into_iter().map(to_owned_cow).collect()),
|
||||||
|
location: self.location.map(to_owned_cow),
|
||||||
|
extension: self.extension.map(|v| v.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BodyExtMPart<'a> {
|
||||||
|
pub param: BodyParams<'a>,
|
||||||
|
pub disposition: Option<ContentDisposition<'a>>,
|
||||||
|
pub language: Option<Vec<Cow<'a, str>>>,
|
||||||
|
pub location: Option<Cow<'a, str>>,
|
||||||
|
pub extension: Option<BodyExtension<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyExtMPart<'a> {
|
||||||
|
pub fn into_owned(self) -> BodyExtMPart<'static> {
|
||||||
|
BodyExtMPart {
|
||||||
|
param: body_param_owned(self.param),
|
||||||
|
disposition: self.disposition.map(|v| v.into_owned()),
|
||||||
|
language: self
|
||||||
|
.language
|
||||||
|
.map(|v| v.into_iter().map(to_owned_cow).collect()),
|
||||||
|
location: self.location.map(to_owned_cow),
|
||||||
|
extension: self.extension.map(|v| v.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP4 QUOTA extension (rfc2087)
|
||||||
|
|
||||||
|
/// https://tools.ietf.org/html/rfc2087#section-3
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub enum QuotaResourceName<'a> {
|
||||||
|
/// Sum of messages' RFC822.SIZE, in units of 1024 octets
|
||||||
|
Storage,
|
||||||
|
/// Number of messages
|
||||||
|
Message,
|
||||||
|
Atom(Cow<'a, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QuotaResourceName<'a> {
|
||||||
|
pub fn into_owned(self) -> QuotaResourceName<'static> {
|
||||||
|
match self {
|
||||||
|
QuotaResourceName::Message => QuotaResourceName::Message,
|
||||||
|
QuotaResourceName::Storage => QuotaResourceName::Storage,
|
||||||
|
QuotaResourceName::Atom(v) => QuotaResourceName::Atom(to_owned_cow(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub struct QuotaResource<'a> {
|
||||||
|
pub name: QuotaResourceName<'a>,
|
||||||
|
/// current usage of the resource
|
||||||
|
pub usage: u64,
|
||||||
|
/// resource limit
|
||||||
|
pub limit: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QuotaResource<'a> {
|
||||||
|
pub fn into_owned(self) -> QuotaResource<'static> {
|
||||||
|
QuotaResource {
|
||||||
|
name: self.name.into_owned(),
|
||||||
|
usage: self.usage,
|
||||||
|
limit: self.limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub struct Quota<'a> {
|
||||||
|
/// quota root name
|
||||||
|
pub root_name: Cow<'a, str>,
|
||||||
|
pub resources: Vec<QuotaResource<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Quota<'a> {
|
||||||
|
pub fn into_owned(self) -> Quota<'static> {
|
||||||
|
Quota {
|
||||||
|
root_name: to_owned_cow(self.root_name),
|
||||||
|
resources: self.resources.into_iter().map(|r| r.into_owned()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5.2. QUOTAROOT Response (https://tools.ietf.org/html/rfc2087#section-5.2)
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub struct QuotaRoot<'a> {
|
||||||
|
/// mailbox name
|
||||||
|
pub mailbox_name: Cow<'a, str>,
|
||||||
|
/// zero or more quota root names
|
||||||
|
pub quota_root_names: Vec<Cow<'a, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QuotaRoot<'a> {
|
||||||
|
pub fn into_owned(self) -> QuotaRoot<'static> {
|
||||||
|
QuotaRoot {
|
||||||
|
mailbox_name: to_owned_cow(self.mailbox_name),
|
||||||
|
quota_root_names: self
|
||||||
|
.quota_root_names
|
||||||
|
.into_iter()
|
||||||
|
.map(to_owned_cow)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue