implemented rly cursed patches on top of pest, i hate parsing
This commit is contained in:
parent
f0e5042a76
commit
50128cd10d
21 changed files with 657 additions and 3321 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -118,37 +118,19 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.7.3"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
|
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding",
|
|
||||||
"byte-tools",
|
|
||||||
"byteorder",
|
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-padding"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
|
|
||||||
dependencies = [
|
|
||||||
"byte-tools",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.6.1"
|
version = "3.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
|
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byte-tools"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
@ -245,6 +227,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpuid-bool"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -379,9 +367,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
@ -418,12 +406,6 @@ dependencies = [
|
||||||
"syn 1.0.60",
|
"syn 1.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fake-simd"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -580,11 +562,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.12.3"
|
version = "0.14.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
|
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -860,9 +843,9 @@ checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.2.3"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "panorama"
|
name = "panorama"
|
||||||
|
@ -951,8 +934,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
@ -960,8 +941,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
|
@ -970,8 +949,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
|
@ -983,8 +960,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"maplit",
|
"maplit",
|
||||||
"pest",
|
"pest",
|
||||||
|
@ -1206,13 +1181,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.8.2"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
|
checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"cpuid-bool",
|
||||||
"digest",
|
"digest",
|
||||||
"fake-simd",
|
|
||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
1
imap/.gitignore
vendored
Normal file
1
imap/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
out.rs
|
|
@ -20,8 +20,8 @@ derive_builder = "0.9.0"
|
||||||
futures = "0.3.12"
|
futures = "0.3.12"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
parking_lot = "0.11.1"
|
parking_lot = "0.11.1"
|
||||||
pest = "2.1.3"
|
pest = { path = "../../pest/pest" }
|
||||||
pest_derive = "2.1.0"
|
pest_derive = { path = "../../pest/derive" }
|
||||||
tokio = { version = "1.1.1", features = ["full"] }
|
tokio = { version = "1.1.1", features = ["full"] }
|
||||||
tokio-rustls = "0.22.0"
|
tokio-rustls = "0.22.0"
|
||||||
tokio-stream = "0.1.3"
|
tokio-stream = "0.1.3"
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,331 +0,0 @@
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
use crate::types::Response;
|
|
||||||
use nom::{branch::alt, IResult};
|
|
||||||
|
|
||||||
pub mod core;
|
|
||||||
|
|
||||||
pub mod bodystructure;
|
|
||||||
pub mod rfc3501;
|
|
||||||
pub mod rfc4315;
|
|
||||||
pub mod rfc4551;
|
|
||||||
pub mod rfc5161;
|
|
||||||
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>>;
|
|
|
@ -1,71 +0,0 @@
|
||||||
use nom::{
|
|
||||||
branch::alt,
|
|
||||||
bytes::streaming::{tag, tag_no_case},
|
|
||||||
character::streaming::char,
|
|
||||||
combinator::{map, opt},
|
|
||||||
multi::many0,
|
|
||||||
sequence::{delimited, preceded, tuple},
|
|
||||||
IResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{oldparser::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,
|
|
||||||
},
|
|
||||||
)(i)
|
|
||||||
}
|
|
|
@ -1,527 +0,0 @@
|
||||||
use nom::{
|
|
||||||
branch::alt,
|
|
||||||
bytes::streaming::{tag, tag_no_case},
|
|
||||||
character::streaming::char,
|
|
||||||
combinator::{map, opt},
|
|
||||||
multi::many1,
|
|
||||||
sequence::{delimited, preceded, tuple},
|
|
||||||
IResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
oldparser::{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,
|
|
||||||
description,
|
|
||||||
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,
|
|
||||||
disposition,
|
|
||||||
language,
|
|
||||||
location,
|
|
||||||
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,
|
|
||||||
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(enc)),
|
|
||||||
))(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn body_lang(i: &[u8]) -> IResult<&[u8], Option<Vec<&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![s])),
|
|
||||||
map(parenthesized_nonempty_list(string_utf8), 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)| (key, 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, BodyExtension::Str),
|
|
||||||
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, 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,
|
|
||||||
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: "TEXT",
|
|
||||||
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: "MESSAGE",
|
|
||||||
subtype: "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: "MULTIPART",
|
|
||||||
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: (&str, &str) = ("foo", "bar");
|
|
||||||
const BODY_FIELD_ID: Option<&str> = Some("id");
|
|
||||||
const BODY_FIELD_DESC: Option<&str> = Some("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: "TEXT",
|
|
||||||
subtype: "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![("foo", "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("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("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: "attachment",
|
|
||||||
params: Some(vec![
|
|
||||||
("FILENAME", "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: "APPLICATION",
|
|
||||||
subtype: "PDF",
|
|
||||||
params: Some(vec![("NAME", "pages.pdf")])
|
|
||||||
},
|
|
||||||
disposition: Some(ContentDisposition {
|
|
||||||
ty: "attachment",
|
|
||||||
params: Some(vec![("FILENAME", "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: "MULTIPART",
|
|
||||||
subtype: "ALTERNATIVE",
|
|
||||||
params: None
|
|
||||||
},
|
|
||||||
language: None,
|
|
||||||
location: None,
|
|
||||||
disposition: None,
|
|
||||||
},
|
|
||||||
bodies: vec![
|
|
||||||
text_body_struct1,
|
|
||||||
text_body_struct2,
|
|
||||||
],
|
|
||||||
extension: None
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,739 +0,0 @@
|
||||||
//!
|
|
||||||
//! https://tools.ietf.org/html/rfc3501
|
|
||||||
//!
|
|
||||||
//! INTERNET MESSAGE ACCESS PROTOCOL
|
|
||||||
//!
|
|
||||||
|
|
||||||
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::{
|
|
||||||
oldparser::{
|
|
||||||
core::*, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551, rfc5161, 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<&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(flag_perm)(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(astring_utf8),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
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(flag_perm),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
)),
|
|
||||||
tag(b"]"),
|
|
||||||
)(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
|
|
||||||
alt((
|
|
||||||
map(tag_no_case(b"IMAP4rev1"), |_| Capability::Imap4rev1),
|
|
||||||
map(preceded(tag_no_case(b"AUTH="), atom), Capability::Auth),
|
|
||||||
map(atom, 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<&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,
|
|
||||||
name: 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,
|
|
||||||
name: 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, 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,
|
|
||||||
))(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,
|
|
||||||
adl,
|
|
||||||
mailbox,
|
|
||||||
host,
|
|
||||||
},
|
|
||||||
))(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,
|
|
||||||
subject,
|
|
||||||
from,
|
|
||||||
sender,
|
|
||||||
reply_to,
|
|
||||||
to,
|
|
||||||
cc,
|
|
||||||
bcc,
|
|
||||||
in_reply_to,
|
|
||||||
message_id,
|
|
||||||
},
|
|
||||||
))(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(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),
|
|
||||||
AttributeValue::Rfc822,
|
|
||||||
)(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),
|
|
||||||
)(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),
|
|
||||||
AttributeValue::Rfc822Text,
|
|
||||||
)(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
)(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,
|
|
||||||
tag(b" "),
|
|
||||||
resp_text,
|
|
||||||
tag(b"\r\n"),
|
|
||||||
)),
|
|
||||||
|(tag, _, status, _, text, _)| Response::Done {
|
|
||||||
tag,
|
|
||||||
status,
|
|
||||||
code: text.0,
|
|
||||||
information: text.1,
|
|
||||||
},
|
|
||||||
)(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, tag(b" "), resp_text)),
|
|
||||||
|(status, _, text)| Response::Data {
|
|
||||||
status,
|
|
||||||
code: text.0,
|
|
||||||
information: text.1,
|
|
||||||
},
|
|
||||||
)(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
|
||||||
// mailbox-data / message-data / capability-data) 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,
|
|
||||||
)),
|
|
||||||
tag(b"\r\n"),
|
|
||||||
)(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::types::*;
|
|
||||||
use assert_matches::assert_matches;
|
|
||||||
|
|
||||||
#[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 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("XPIG-LATIN"), Capability::Imap4rev1,
|
|
||||||
Capability::Atom("STARTTLS"), Capability::Auth("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("GSSAPI"), Capability::Auth("PLAIN")
|
|
||||||
])
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Capability command must contain IMAP4rev1
|
|
||||||
assert_matches!(
|
|
||||||
super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
|
|
||||||
Err(_)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
//!
|
|
||||||
//! 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::oldparser::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)
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
//!
|
|
||||||
//! 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::{
|
|
||||||
oldparser::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)))
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
//!
|
|
||||||
//! 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 crate::oldparser::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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
|
|
||||||
map(atom, Capability::Atom)(i)
|
|
||||||
}
|
|
|
@ -1,295 +0,0 @@
|
||||||
//!
|
|
||||||
//! 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 crate::{oldparser::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<&str>> {
|
|
||||||
separated_list0(tag(" "), map(entry_name, slice_to_str))(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: 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: slice_to_str(mailbox),
|
|
||||||
values,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{metadata_solicited, metadata_unsolicited};
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
#[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."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//! 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::oldparser::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,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -1,499 +0,0 @@
|
||||||
use super::{bodystructure::BodyStructParser, parse_response};
|
|
||||||
use crate::types::*;
|
|
||||||
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("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(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("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_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("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("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("Logged in"),
|
|
||||||
},
|
|
||||||
)) => {
|
|
||||||
assert_eq!(c.len(), 2);
|
|
||||||
assert_eq!(c[0], Capability::Imap4rev1);
|
|
||||||
assert_eq!(c[1], Capability::Atom("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("Logged in"),
|
|
||||||
},
|
|
||||||
)) => {
|
|
||||||
assert_eq!(c.len(), 3);
|
|
||||||
assert_eq!(c[0], Capability::Atom("UIDPLUS"));
|
|
||||||
assert_eq!(c[1], Capability::Imap4rev1);
|
|
||||||
assert_eq!(c[2], Capability::Atom("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("[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("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("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("[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("QRESYNC"),
|
|
||||||
Capability::Atom("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![
|
|
||||||
"\\Answered",
|
|
||||||
"\\Flagged",
|
|
||||||
"\\Deleted",
|
|
||||||
"\\Seen",
|
|
||||||
"\\Draft",
|
|
||||||
"\\*"
|
|
||||||
]))
|
|
||||||
),
|
|
||||||
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_eq!(earlier, true);
|
|
||||||
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_eq!(earlier, false);
|
|
||||||
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_eq!(earlier, true);
|
|
||||||
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_eq!(earlier, false);
|
|
||||||
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("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("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("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"),
|
|
||||||
};
|
|
||||||
}
|
|
76
imap/src/parser/literal.rs
Normal file
76
imap/src/parser/literal.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use pest::{ParseResult as PestResult, ParserState};
|
||||||
|
|
||||||
|
use super::old::Rule;
|
||||||
|
|
||||||
|
type PSR<'a> = Box<ParserState<'a, Rule>>;
|
||||||
|
|
||||||
|
pub(crate) fn literal_internal(state: PSR) -> PestResult<PSR> {
|
||||||
|
use pest::Atomicity;
|
||||||
|
|
||||||
|
// yoinked from the generated code
|
||||||
|
#[inline]
|
||||||
|
#[allow(non_snake_case, unused_variables)]
|
||||||
|
pub fn digit(state: PSR) -> PestResult<PSR> {
|
||||||
|
state.rule(Rule::digit, |state| {
|
||||||
|
state.atomic(Atomicity::Atomic, |state| {
|
||||||
|
state.match_range('\u{30}'..'\u{39}')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
#[allow(non_snake_case, unused_variables)]
|
||||||
|
pub fn number(state: PSR) -> PestResult<PSR> {
|
||||||
|
state.rule(Rule::number, |state| {
|
||||||
|
state.atomic(Atomicity::Atomic, |state| {
|
||||||
|
state.sequence(|state| {
|
||||||
|
digit(state).and_then(|state| state.repeat(|state| digit(state)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
#[allow(non_snake_case, unused_variables)]
|
||||||
|
pub fn char8(state: PSR) -> PestResult<PSR> {
|
||||||
|
state.rule(Rule::char8, |state| {
|
||||||
|
state.atomic(Atomicity::Atomic, |state| {
|
||||||
|
state.match_range('\u{1}'..'\u{ff}')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
#[allow(non_snake_case, unused_variables)]
|
||||||
|
pub fn crlf(state: PSR) -> PestResult<PSR> {
|
||||||
|
state.sequence(|state| state.match_string("\r")?.match_string("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = state.match_string("{").and_then(number)?;
|
||||||
|
let num_chars = {
|
||||||
|
let mut queue = state.queue().iter().rev();
|
||||||
|
println!("QUEUE: {:?}", queue);
|
||||||
|
let end = queue.next().unwrap();
|
||||||
|
let start = queue.next().unwrap();
|
||||||
|
let inp = state.position().get_str();
|
||||||
|
let seg = &inp[start.input_pos()..end.input_pos()];
|
||||||
|
seg.parse::<usize>().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
.match_string("}")
|
||||||
|
.and_then(crlf)?
|
||||||
|
.rule(Rule::literal_str, |state| {
|
||||||
|
state.atomic(Atomicity::Atomic, |state| {
|
||||||
|
let mut state = Ok(state);
|
||||||
|
for _ in 0..num_chars {
|
||||||
|
state = state.and_then(char8);
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// todo!("hit internal state: {:?}", state,);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn noop(state: PSR) -> PestResult<PSR> {
|
||||||
|
// TODO: probably should be unreachable?
|
||||||
|
Ok(state)
|
||||||
|
}
|
|
@ -1,508 +1,26 @@
|
||||||
//! Module that implements parsers for all of the IMAP types.
|
mod literal;
|
||||||
|
mod old;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use anyhow::Result;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use pest::{error::Error, iterators::Pair, Parser};
|
|
||||||
|
|
||||||
use crate::response::*;
|
use crate::response::*;
|
||||||
|
|
||||||
#[derive(Parser)]
|
use self::literal::literal_internal;
|
||||||
#[grammar = "parser/rfc3501.pest"]
|
|
||||||
struct Rfc3501;
|
|
||||||
|
|
||||||
pub type ParseResult<T, E = Error<Rule>> = Result<T, E>;
|
pub fn parse_capability(s: impl AsRef<str>) -> Result<Capability> {
|
||||||
|
let s = s.as_ref();
|
||||||
pub fn parse_capability(s: impl AsRef<str>) -> ParseResult<Capability> {
|
if s == "IMAP4rev1" {
|
||||||
let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?;
|
Ok(Capability::Imap4rev1)
|
||||||
let pair = pairs.next().unwrap();
|
} else if s.to_lowercase().starts_with("AUTH=") {
|
||||||
Ok(build_capability(pair))
|
Ok(Capability::Auth(s[5..].to_owned()))
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_response(s: impl AsRef<str>) -> ParseResult<Response> {
|
|
||||||
let mut pairs = Rfc3501::parse(Rule::response, s.as_ref())?;
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
Ok(build_response(pair))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_response(pair: Pair<Rule>) -> Response {
|
|
||||||
if !matches!(pair.as_rule(), Rule::response) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::response_done => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::response_tagged => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let tag = pair.as_str().to_owned();
|
|
||||||
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let (status, code, information) = build_resp_cond_state(pair);
|
|
||||||
Response::Done {
|
|
||||||
tag,
|
|
||||||
status,
|
|
||||||
code,
|
|
||||||
information,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rule::response_data => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::resp_cond_state => {
|
|
||||||
let (status, code, information) = build_resp_cond_state(pair);
|
|
||||||
Response::Data {
|
|
||||||
status,
|
|
||||||
code,
|
|
||||||
information,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)),
|
|
||||||
Rule::capability_data => Response::Capabilities(build_capabilities(pair)),
|
|
||||||
Rule::message_data => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let seq: u32 = build_number(pair);
|
|
||||||
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::message_data_expunge => Response::Expunge(seq),
|
|
||||||
Rule::message_data_fetch => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let msg_att = pairs.next().unwrap();
|
|
||||||
let attrs = msg_att.into_inner().map(build_msg_att).collect();
|
|
||||||
Response::Fetch(seq, attrs)
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rule::continue_req => {
|
|
||||||
let (code, s) = build_resp_text(unwrap1(pair));
|
|
||||||
Response::Continue {
|
|
||||||
code,
|
|
||||||
information: Some(s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_resp_text(pair: Pair<Rule>) -> (Option<ResponseCode>, String) {
|
|
||||||
assert!(matches!(pair.as_rule(), Rule::resp_text));
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let mut pair = pairs.next().unwrap();
|
|
||||||
let mut resp_code = None;
|
|
||||||
if let Rule::resp_text_code = pair.as_rule() {
|
|
||||||
resp_code = build_resp_text_code(pair);
|
|
||||||
pair = pairs.next().unwrap();
|
|
||||||
}
|
|
||||||
assert!(matches!(pair.as_rule(), Rule::text));
|
|
||||||
let s = pair.as_str().to_owned();
|
|
||||||
(resp_code, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_msg_att(pair: Pair<Rule>) -> AttributeValue {
|
|
||||||
if !matches!(pair.as_rule(), Rule::msg_att_dyn_or_stat) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::msg_att_dynamic => AttributeValue::Flags(pair.into_inner().map(build_flag).collect()),
|
|
||||||
Rule::msg_att_static => build_msg_att_static(pair),
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_msg_att_static(pair: Pair<Rule>) -> AttributeValue {
|
|
||||||
if !matches!(pair.as_rule(), Rule::msg_att_static) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::msg_att_static_internaldate => {
|
|
||||||
AttributeValue::InternalDate(build_string(unwrap1(pair)))
|
|
||||||
}
|
|
||||||
Rule::msg_att_static_rfc822_size => AttributeValue::Rfc822Size(build_number(unwrap1(pair))),
|
|
||||||
Rule::msg_att_static_envelope => AttributeValue::Envelope(build_envelope(unwrap1(pair))),
|
|
||||||
// TODO: do this
|
|
||||||
Rule::msg_att_static_body => AttributeValue::BodySection {
|
|
||||||
section: None,
|
|
||||||
index: None,
|
|
||||||
data: None,
|
|
||||||
},
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_envelope(_pair: Pair<Rule>) -> Envelope {
|
|
||||||
// TODO: do this
|
|
||||||
Envelope::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_resp_cond_state(pair: Pair<Rule>) -> (Status, Option<ResponseCode>, Option<String>) {
|
|
||||||
if !matches!(pair.as_rule(), Rule::resp_cond_state) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let status = build_status(pair);
|
|
||||||
let mut code = None;
|
|
||||||
let mut information = None;
|
|
||||||
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let pairs = pair.into_inner();
|
|
||||||
for pair in pairs {
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::resp_text_code => code = build_resp_text_code(pair),
|
|
||||||
Rule::text => information = Some(pair.as_str().to_owned()),
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(status, code, information)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_resp_text_code(pair: Pair<Rule>) -> Option<ResponseCode> {
|
|
||||||
if !matches!(pair.as_rule(), Rule::resp_text_code) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next()?;
|
|
||||||
Some(match pair.as_rule() {
|
|
||||||
Rule::capability_data => ResponseCode::Capabilities(build_capabilities(pair)),
|
|
||||||
Rule::resp_text_code_readwrite => ResponseCode::ReadWrite,
|
|
||||||
Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(unwrap1(pair))),
|
|
||||||
Rule::resp_text_code_uidnext => ResponseCode::UidNext(build_number(unwrap1(pair))),
|
|
||||||
Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(unwrap1(pair))),
|
|
||||||
// TODO: maybe have an actual type for these flags instead of just string
|
|
||||||
Rule::resp_text_code_permanentflags => {
|
|
||||||
ResponseCode::PermanentFlags(pair.into_inner().map(|p| p.as_str().to_owned()).collect())
|
|
||||||
}
|
|
||||||
Rule::resp_text_code_other => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let a = pair.as_str().to_owned();
|
|
||||||
let mut b = None;
|
|
||||||
if let Some(pair) = pairs.next() {
|
|
||||||
b = Some(pair.as_str().to_owned());
|
|
||||||
}
|
|
||||||
ResponseCode::Other(a, b)
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_capability(pair: Pair<Rule>) -> Capability {
|
|
||||||
if !matches!(pair.as_rule(), Rule::capability) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()),
|
|
||||||
Rule::atom => match pair.as_str() {
|
|
||||||
"IMAP4rev1" => Capability::Imap4rev1,
|
|
||||||
s => Capability::Atom(s.to_uppercase().to_owned()),
|
|
||||||
},
|
|
||||||
_ => unreachable!("{:?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_capabilities(pair: Pair<Rule>) -> Vec<Capability> {
|
|
||||||
if !matches!(pair.as_rule(), Rule::capability_data) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair.into_inner().map(build_capability).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_status(pair: Pair<Rule>) -> Status {
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::resp_status => match pair.as_str().to_uppercase().as_str() {
|
|
||||||
"OK" => Status::Ok,
|
|
||||||
"NO" => Status::No,
|
|
||||||
"BAD" => Status::Bad,
|
|
||||||
s => unreachable!("invalid status {:?}", s),
|
|
||||||
},
|
|
||||||
_ => unreachable!("{:?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_flag_list(pair: Pair<Rule>) -> Vec<MailboxFlag> {
|
|
||||||
if !matches!(pair.as_rule(), Rule::flag_list) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair.into_inner().map(build_flag).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_flag(mut pair: Pair<Rule>) -> MailboxFlag {
|
|
||||||
if matches!(pair.as_rule(), Rule::flag_fetch) {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
pair = pairs.next().unwrap();
|
|
||||||
|
|
||||||
if matches!(pair.as_rule(), Rule::flag_fetch_recent) {
|
|
||||||
return MailboxFlag::Recent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matches!(pair.as_rule(), Rule::flag) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
match pair.as_str() {
|
|
||||||
"\\Answered" => MailboxFlag::Answered,
|
|
||||||
"\\Flagged" => MailboxFlag::Flagged,
|
|
||||||
"\\Deleted" => MailboxFlag::Deleted,
|
|
||||||
"\\Seen" => MailboxFlag::Seen,
|
|
||||||
"\\Draft" => MailboxFlag::Draft,
|
|
||||||
// s if s.starts_with("\\") => MailboxFlag::Ext(s.to_owned()),
|
|
||||||
// TODO: what??
|
|
||||||
s => MailboxFlag::Ext(s.to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mailbox_data(pair: Pair<Rule>) -> MailboxData {
|
|
||||||
if !matches!(pair.as_rule(), Rule::mailbox_data) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
match pair.as_rule() {
|
|
||||||
Rule::mailbox_data_exists => MailboxData::Exists(build_number(unwrap1(pair))),
|
|
||||||
Rule::mailbox_data_flags => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let flags = build_flag_list(pair);
|
|
||||||
MailboxData::Flags(flags)
|
|
||||||
}
|
|
||||||
Rule::mailbox_data_recent => MailboxData::Recent(build_number(unwrap1(pair))),
|
|
||||||
Rule::mailbox_data_list => {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let pair = pairs.next().unwrap();
|
|
||||||
let (flags, delimiter, name) = build_mailbox_list(pair);
|
|
||||||
MailboxData::List {
|
|
||||||
flags,
|
|
||||||
delimiter,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!("{:#?}", pair),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mailbox_list(pair: Pair<Rule>) -> (Vec<String>, Option<String>, String) {
|
|
||||||
if !matches!(pair.as_rule(), Rule::mailbox_list) {
|
|
||||||
unreachable!("{:#?}", pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
let mut pair = pairs.next().unwrap();
|
|
||||||
|
|
||||||
// let mut flags = Vec::new();
|
|
||||||
let flags = if let Rule::mailbox_list_flags = pair.as_rule() {
|
|
||||||
let pairs_ = pair.into_inner();
|
|
||||||
let mut flags = Vec::new();
|
|
||||||
for pair in pairs_ {
|
|
||||||
flags.extend(build_mbx_list_flags(pair));
|
|
||||||
}
|
|
||||||
pair = pairs.next().unwrap();
|
|
||||||
flags
|
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Ok(Capability::Atom(s.to_owned()))
|
||||||
};
|
|
||||||
|
|
||||||
assert!(matches!(pair.as_rule(), Rule::mailbox_list_string));
|
|
||||||
let s = build_nstring(pair);
|
|
||||||
|
|
||||||
pair = pairs.next().unwrap();
|
|
||||||
assert!(matches!(pair.as_rule(), Rule::mailbox));
|
|
||||||
let mailbox = build_string(pair);
|
|
||||||
|
|
||||||
(flags, s, mailbox)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mbx_list_flags(pair: Pair<Rule>) -> Vec<String> {
|
|
||||||
assert!(matches!(pair.as_rule(), Rule::mbx_list_flags));
|
|
||||||
pair.into_inner()
|
|
||||||
.map(|pair| pair.as_str().to_owned())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unwraps a singleton pair (a pair that only has one element in its `inner` list)
|
|
||||||
fn unwrap1(pair: Pair<Rule>) -> Pair<Rule> {
|
|
||||||
let mut pairs = pair.into_inner();
|
|
||||||
pairs.next().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts a numerical type, generic over anything that could possibly be read as a number
|
|
||||||
// TODO: should probably restrict this to a few cases
|
|
||||||
fn build_number<T>(pair: Pair<Rule>) -> T
|
|
||||||
where
|
|
||||||
T: FromStr,
|
|
||||||
T::Err: Debug,
|
|
||||||
{
|
|
||||||
if !matches!(pair.as_rule(), Rule::nz_number | Rule::number) {
|
|
||||||
unreachable!("not a number {:#?}", pair);
|
|
||||||
}
|
|
||||||
pair.as_str().parse::<T>().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper around [build_string][1], except return None for the `nil` case
|
|
||||||
///
|
|
||||||
/// [1]: self::build_string
|
|
||||||
fn build_nstring(pair: Pair<Rule>) -> Option<String> {
|
|
||||||
if matches!(pair.as_rule(), Rule::nil) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(build_string(pair))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts a string-type, discarding the surrounding quotes and unescaping the escaped characters
|
|
||||||
fn build_string(pair: Pair<Rule>) -> String {
|
|
||||||
// TODO: actually get rid of the quotes and escaped chars
|
|
||||||
pair.as_str().to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::response::*;
|
|
||||||
use pest::Parser;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[rustfmt::skip]
|
|
||||||
fn test_capability() {
|
|
||||||
assert_eq!(parse_capability("IMAP4rev1"), Ok(Capability::Imap4rev1));
|
|
||||||
assert_eq!(parse_capability("LOGINDISABLED"), Ok(Capability::Atom("LOGINDISABLED".to_owned())));
|
|
||||||
assert_eq!(parse_capability("AUTH=PLAIN"), Ok(Capability::Auth("PLAIN".to_owned())));
|
|
||||||
assert_eq!(parse_capability("auth=plain"), Ok(Capability::Auth("PLAIN".to_owned())));
|
|
||||||
|
|
||||||
assert!(parse_capability("(OSU)").is_err());
|
|
||||||
assert!(parse_capability("\x01HELLO").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[rustfmt::skip]
|
|
||||||
fn test_nil() {
|
|
||||||
assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok());
|
|
||||||
assert!(Rfc3501::parse(Rule::nil, "anything else").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_section_8() {
|
|
||||||
// this little exchange is from section 8 of rfc3501
|
|
||||||
// https://tools.ietf.org/html/rfc3501#section-8
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* OK IMAP4rev1 Service Ready\r\n"),
|
|
||||||
Ok(Response::Data {
|
|
||||||
status: Status::Ok,
|
|
||||||
code: None,
|
|
||||||
information: Some("IMAP4rev1 Service Ready".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("a001 OK LOGIN completed\r\n"),
|
|
||||||
Ok(Response::Done {
|
|
||||||
tag: "a001".to_owned(),
|
|
||||||
status: Status::Ok,
|
|
||||||
code: None,
|
|
||||||
information: Some("LOGIN completed".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* 18 EXISTS\r\n"),
|
|
||||||
Ok(Response::MailboxData(MailboxData::Exists(18)))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"),
|
|
||||||
Ok(Response::MailboxData(MailboxData::Flags(vec![
|
|
||||||
MailboxFlag::Answered,
|
|
||||||
MailboxFlag::Flagged,
|
|
||||||
MailboxFlag::Deleted,
|
|
||||||
MailboxFlag::Seen,
|
|
||||||
MailboxFlag::Draft,
|
|
||||||
])))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* 2 RECENT\r\n"),
|
|
||||||
Ok(Response::MailboxData(MailboxData::Recent(2)))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* OK [UNSEEN 17] Message 17 is the first unseen message\r\n"),
|
|
||||||
Ok(Response::Data {
|
|
||||||
status: Status::Ok,
|
|
||||||
code: Some(ResponseCode::Unseen(17)),
|
|
||||||
information: Some("Message 17 is the first unseen message".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"),
|
|
||||||
Ok(Response::Data {
|
|
||||||
status: Status::Ok,
|
|
||||||
code: Some(ResponseCode::UidValidity(3857529045)),
|
|
||||||
information: Some("UIDs valid".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response("a002 OK [READ-WRITE] SELECT completed\r\n"),
|
|
||||||
Ok(Response::Done {
|
|
||||||
tag: "a002".to_owned(),
|
|
||||||
status: Status::Ok,
|
|
||||||
code: Some(ResponseCode::ReadWrite),
|
|
||||||
information: Some("SELECT completed".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_response(concat!(
|
|
||||||
r#"* 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 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>") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 302892))"#,
|
|
||||||
"\r\n",
|
|
||||||
)),
|
|
||||||
Ok(Response::Fetch(
|
|
||||||
12,
|
|
||||||
vec![
|
|
||||||
AttributeValue::Flags(vec![MailboxFlag::Seen]),
|
|
||||||
AttributeValue::InternalDate("\"17-Jul-1996 02:44:25 -0700\"".to_owned()),
|
|
||||||
AttributeValue::Rfc822Size(4286),
|
|
||||||
AttributeValue::Envelope(Envelope::default()),
|
|
||||||
AttributeValue::BodySection {
|
|
||||||
section: None,
|
|
||||||
index: None,
|
|
||||||
data: None,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_response(s: impl AsRef<str>) -> Result<Response> {
|
||||||
|
let s = s.as_ref();
|
||||||
|
let mut parts = s.split(' ');
|
||||||
|
let tag = parts.next().unwrap();
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
528
imap/src/parser/old.rs
Normal file
528
imap/src/parser/old.rs
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
//! Module that implements parsers for all of the IMAP types.
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::mem;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use pest::{error::Error, iterators::Pair, ParseResult as PestResult, Parser, ParserState};
|
||||||
|
|
||||||
|
use crate::response::*;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[grammar = "parser/rfc3501.pest"]
|
||||||
|
|
||||||
|
struct Rfc3501;
|
||||||
|
|
||||||
|
pub type ParseResult<T, E = Error<Rule>> = Result<T, E>;
|
||||||
|
|
||||||
|
pub fn parse_capability(s: impl AsRef<str>) -> ParseResult<Capability> {
|
||||||
|
let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?;
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
Ok(build_capability(pair))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_response(s: impl AsRef<str>) -> ParseResult<Response> {
|
||||||
|
let mut pairs = Rfc3501::parse(Rule::response, s.as_ref())?;
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
Ok(build_response(pair))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_response(pair: Pair<Rule>) -> Response {
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::response));
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::response_done => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::response_tagged => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let tag = pair.as_str().to_owned();
|
||||||
|
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let (status, code, information) = build_resp_cond_state(pair);
|
||||||
|
Response::Done {
|
||||||
|
tag,
|
||||||
|
status,
|
||||||
|
code,
|
||||||
|
information,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rule::response_data => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::resp_cond_state => {
|
||||||
|
let (status, code, information) = build_resp_cond_state(pair);
|
||||||
|
Response::Data {
|
||||||
|
status,
|
||||||
|
code,
|
||||||
|
information,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)),
|
||||||
|
Rule::capability_data => Response::Capabilities(build_capabilities(pair)),
|
||||||
|
Rule::message_data => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let seq: u32 = build_number(pair);
|
||||||
|
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::message_data_expunge => Response::Expunge(seq),
|
||||||
|
Rule::message_data_fetch => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let msg_att = pairs.next().unwrap();
|
||||||
|
let attrs = msg_att.into_inner().map(build_msg_att).collect();
|
||||||
|
Response::Fetch(seq, attrs)
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rule::continue_req => {
|
||||||
|
let (code, s) = build_resp_text(unwrap1(pair));
|
||||||
|
Response::Continue {
|
||||||
|
code,
|
||||||
|
information: Some(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_resp_text(pair: Pair<Rule>) -> (Option<ResponseCode>, String) {
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::resp_text));
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let mut pair = pairs.next().unwrap();
|
||||||
|
let mut resp_code = None;
|
||||||
|
if let Rule::resp_text_code = pair.as_rule() {
|
||||||
|
resp_code = build_resp_text_code(pair);
|
||||||
|
pair = pairs.next().unwrap();
|
||||||
|
}
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::text));
|
||||||
|
let s = pair.as_str().to_owned();
|
||||||
|
(resp_code, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_msg_att(pair: Pair<Rule>) -> AttributeValue {
|
||||||
|
if !matches!(pair.as_rule(), Rule::msg_att_dyn_or_stat) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::msg_att_dynamic => AttributeValue::Flags(pair.into_inner().map(build_flag).collect()),
|
||||||
|
Rule::msg_att_static => build_msg_att_static(pair),
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_msg_att_static(pair: Pair<Rule>) -> AttributeValue {
|
||||||
|
if !matches!(pair.as_rule(), Rule::msg_att_static) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::msg_att_static_internaldate => {
|
||||||
|
AttributeValue::InternalDate(build_string(unwrap1(pair)))
|
||||||
|
}
|
||||||
|
Rule::msg_att_static_rfc822_size => AttributeValue::Rfc822Size(build_number(unwrap1(pair))),
|
||||||
|
Rule::msg_att_static_envelope => AttributeValue::Envelope(build_envelope(unwrap1(pair))),
|
||||||
|
// TODO: do this
|
||||||
|
Rule::msg_att_static_body => AttributeValue::BodySection {
|
||||||
|
section: None,
|
||||||
|
index: None,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_envelope(_pair: Pair<Rule>) -> Envelope {
|
||||||
|
// TODO: do this
|
||||||
|
Envelope::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_resp_cond_state(pair: Pair<Rule>) -> (Status, Option<ResponseCode>, Option<String>) {
|
||||||
|
if !matches!(pair.as_rule(), Rule::resp_cond_state) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let status = build_status(pair);
|
||||||
|
let mut code = None;
|
||||||
|
let mut information = None;
|
||||||
|
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let pairs = pair.into_inner();
|
||||||
|
for pair in pairs {
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::resp_text_code => code = build_resp_text_code(pair),
|
||||||
|
Rule::text => information = Some(pair.as_str().to_owned()),
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(status, code, information)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_resp_text_code(pair: Pair<Rule>) -> Option<ResponseCode> {
|
||||||
|
if !matches!(pair.as_rule(), Rule::resp_text_code) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next()?;
|
||||||
|
Some(match pair.as_rule() {
|
||||||
|
Rule::capability_data => ResponseCode::Capabilities(build_capabilities(pair)),
|
||||||
|
Rule::resp_text_code_readwrite => ResponseCode::ReadWrite,
|
||||||
|
Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(unwrap1(pair))),
|
||||||
|
Rule::resp_text_code_uidnext => ResponseCode::UidNext(build_number(unwrap1(pair))),
|
||||||
|
Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(unwrap1(pair))),
|
||||||
|
// TODO: maybe have an actual type for these flags instead of just string
|
||||||
|
Rule::resp_text_code_permanentflags => {
|
||||||
|
ResponseCode::PermanentFlags(pair.into_inner().map(|p| p.as_str().to_owned()).collect())
|
||||||
|
}
|
||||||
|
Rule::resp_text_code_other => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let a = pair.as_str().to_owned();
|
||||||
|
let mut b = None;
|
||||||
|
if let Some(pair) = pairs.next() {
|
||||||
|
b = Some(pair.as_str().to_owned());
|
||||||
|
}
|
||||||
|
ResponseCode::Other(a, b)
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_capability(pair: Pair<Rule>) -> Capability {
|
||||||
|
if !matches!(pair.as_rule(), Rule::capability) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()),
|
||||||
|
Rule::atom => match pair.as_str() {
|
||||||
|
"IMAP4rev1" => Capability::Imap4rev1,
|
||||||
|
s => Capability::Atom(s.to_uppercase().to_owned()),
|
||||||
|
},
|
||||||
|
_ => unreachable!("{:?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_capabilities(pair: Pair<Rule>) -> Vec<Capability> {
|
||||||
|
if !matches!(pair.as_rule(), Rule::capability_data) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.into_inner().map(build_capability).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_status(pair: Pair<Rule>) -> Status {
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::resp_status => match pair.as_str().to_uppercase().as_str() {
|
||||||
|
"OK" => Status::Ok,
|
||||||
|
"NO" => Status::No,
|
||||||
|
"BAD" => Status::Bad,
|
||||||
|
s => unreachable!("invalid status {:?}", s),
|
||||||
|
},
|
||||||
|
_ => unreachable!("{:?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_flag_list(pair: Pair<Rule>) -> Vec<MailboxFlag> {
|
||||||
|
if !matches!(pair.as_rule(), Rule::flag_list) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.into_inner().map(build_flag).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_flag(mut pair: Pair<Rule>) -> MailboxFlag {
|
||||||
|
if matches!(pair.as_rule(), Rule::flag_fetch) {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
pair = pairs.next().unwrap();
|
||||||
|
|
||||||
|
if matches!(pair.as_rule(), Rule::flag_fetch_recent) {
|
||||||
|
return MailboxFlag::Recent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(pair.as_rule(), Rule::flag) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
match pair.as_str() {
|
||||||
|
"\\Answered" => MailboxFlag::Answered,
|
||||||
|
"\\Flagged" => MailboxFlag::Flagged,
|
||||||
|
"\\Deleted" => MailboxFlag::Deleted,
|
||||||
|
"\\Seen" => MailboxFlag::Seen,
|
||||||
|
"\\Draft" => MailboxFlag::Draft,
|
||||||
|
// s if s.starts_with("\\") => MailboxFlag::Ext(s.to_owned()),
|
||||||
|
// TODO: what??
|
||||||
|
s => MailboxFlag::Ext(s.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mailbox_data(pair: Pair<Rule>) -> MailboxData {
|
||||||
|
if !matches!(pair.as_rule(), Rule::mailbox_data) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
match pair.as_rule() {
|
||||||
|
Rule::mailbox_data_exists => MailboxData::Exists(build_number(unwrap1(pair))),
|
||||||
|
Rule::mailbox_data_flags => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let flags = build_flag_list(pair);
|
||||||
|
MailboxData::Flags(flags)
|
||||||
|
}
|
||||||
|
Rule::mailbox_data_recent => MailboxData::Recent(build_number(unwrap1(pair))),
|
||||||
|
Rule::mailbox_data_list => {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
let (flags, delimiter, name) = build_mailbox_list(pair);
|
||||||
|
MailboxData::List {
|
||||||
|
flags,
|
||||||
|
delimiter,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("{:#?}", pair),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mailbox_list(pair: Pair<Rule>) -> (Vec<String>, Option<String>, String) {
|
||||||
|
if !matches!(pair.as_rule(), Rule::mailbox_list) {
|
||||||
|
unreachable!("{:#?}", pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let mut pair = pairs.next().unwrap();
|
||||||
|
|
||||||
|
// let mut flags = Vec::new();
|
||||||
|
let flags = if let Rule::mailbox_list_flags = pair.as_rule() {
|
||||||
|
let pairs_ = pair.into_inner();
|
||||||
|
let mut flags = Vec::new();
|
||||||
|
for pair in pairs_ {
|
||||||
|
flags.extend(build_mbx_list_flags(pair));
|
||||||
|
}
|
||||||
|
pair = pairs.next().unwrap();
|
||||||
|
flags
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::mailbox_list_string));
|
||||||
|
let s = build_nstring(pair);
|
||||||
|
|
||||||
|
pair = pairs.next().unwrap();
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::mailbox));
|
||||||
|
let mailbox = build_string(pair);
|
||||||
|
|
||||||
|
(flags, s, mailbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mbx_list_flags(pair: Pair<Rule>) -> Vec<String> {
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::mbx_list_flags));
|
||||||
|
pair.into_inner()
|
||||||
|
.map(|pair| pair.as_str().to_owned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwraps a singleton pair (a pair that only has one element in its `inner` list)
|
||||||
|
fn unwrap1(pair: Pair<Rule>) -> Pair<Rule> {
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
pairs.next().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a numerical type, generic over anything that could possibly be read as a number
|
||||||
|
// TODO: should probably restrict this to a few cases
|
||||||
|
fn build_number<T>(pair: Pair<Rule>) -> T
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Debug,
|
||||||
|
{
|
||||||
|
if !matches!(pair.as_rule(), Rule::nz_number | Rule::number) {
|
||||||
|
unreachable!("not a number {:#?}", pair);
|
||||||
|
}
|
||||||
|
pair.as_str().parse::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around [build_string][1], except return None for the `nil` case
|
||||||
|
///
|
||||||
|
/// [1]: self::build_string
|
||||||
|
fn build_nstring(pair: Pair<Rule>) -> Option<String> {
|
||||||
|
if matches!(pair.as_rule(), Rule::nil) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(build_string(pair))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a string-type, discarding the surrounding quotes and unescaping the escaped characters
|
||||||
|
fn build_string(pair: Pair<Rule>) -> String {
|
||||||
|
// TODO: actually get rid of the quotes and escaped chars
|
||||||
|
pair.as_str().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_literal(s: impl AsRef<str>) -> ParseResult<String> {
|
||||||
|
let mut pairs = Rfc3501::parse(Rule::literal, s.as_ref())?;
|
||||||
|
let pair = pairs.next().unwrap();
|
||||||
|
Ok(build_literal(pair))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_literal(pair: Pair<Rule>) -> String {
|
||||||
|
assert!(matches!(pair.as_rule(), Rule::literal));
|
||||||
|
|
||||||
|
let mut pairs = pair.into_inner();
|
||||||
|
let _ = pairs.next().unwrap();
|
||||||
|
let literal_str = pairs.next().unwrap();
|
||||||
|
literal_str.as_str().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::response::*;
|
||||||
|
use pest::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_literal() {
|
||||||
|
assert_eq!(parse_literal("{7}\r\nhellosu"), Ok("hellosu".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn test_capability() {
|
||||||
|
assert_eq!(parse_capability("IMAP4rev1"), Ok(Capability::Imap4rev1));
|
||||||
|
assert_eq!(parse_capability("LOGINDISABLED"), Ok(Capability::Atom("LOGINDISABLED".to_owned())));
|
||||||
|
assert_eq!(parse_capability("AUTH=PLAIN"), Ok(Capability::Auth("PLAIN".to_owned())));
|
||||||
|
assert_eq!(parse_capability("auth=plain"), Ok(Capability::Auth("PLAIN".to_owned())));
|
||||||
|
|
||||||
|
assert!(parse_capability("(OSU)").is_err());
|
||||||
|
assert!(parse_capability("\x01HELLO").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn test_nil() {
|
||||||
|
assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok());
|
||||||
|
assert!(Rfc3501::parse(Rule::nil, "anything else").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_section_8() {
|
||||||
|
// this little exchange is from section 8 of rfc3501
|
||||||
|
// https://tools.ietf.org/html/rfc3501#section-8
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* OK IMAP4rev1 Service Ready\r\n"),
|
||||||
|
Ok(Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: None,
|
||||||
|
information: Some("IMAP4rev1 Service Ready".to_owned()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("a001 OK LOGIN completed\r\n"),
|
||||||
|
Ok(Response::Done {
|
||||||
|
tag: "a001".to_owned(),
|
||||||
|
status: Status::Ok,
|
||||||
|
code: None,
|
||||||
|
information: Some("LOGIN completed".to_owned()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* 18 EXISTS\r\n"),
|
||||||
|
Ok(Response::MailboxData(MailboxData::Exists(18)))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"),
|
||||||
|
Ok(Response::MailboxData(MailboxData::Flags(vec![
|
||||||
|
MailboxFlag::Answered,
|
||||||
|
MailboxFlag::Flagged,
|
||||||
|
MailboxFlag::Deleted,
|
||||||
|
MailboxFlag::Seen,
|
||||||
|
MailboxFlag::Draft,
|
||||||
|
])))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* 2 RECENT\r\n"),
|
||||||
|
Ok(Response::MailboxData(MailboxData::Recent(2)))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* OK [UNSEEN 17] Message 17 is the first unseen message\r\n"),
|
||||||
|
Ok(Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::Unseen(17)),
|
||||||
|
information: Some("Message 17 is the first unseen message".to_owned()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"),
|
||||||
|
Ok(Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::UidValidity(3857529045)),
|
||||||
|
information: Some("UIDs valid".to_owned()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response("a002 OK [READ-WRITE] SELECT completed\r\n"),
|
||||||
|
Ok(Response::Done {
|
||||||
|
tag: "a002".to_owned(),
|
||||||
|
status: Status::Ok,
|
||||||
|
code: Some(ResponseCode::ReadWrite),
|
||||||
|
information: Some("SELECT completed".to_owned()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_response(concat!(
|
||||||
|
r#"* 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 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>") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 302892))"#,
|
||||||
|
"\r\n",
|
||||||
|
)),
|
||||||
|
Ok(Response::Fetch(
|
||||||
|
12,
|
||||||
|
vec![
|
||||||
|
AttributeValue::Flags(vec![MailboxFlag::Seen]),
|
||||||
|
AttributeValue::InternalDate("\"17-Jul-1996 02:44:25 -0700\"".to_owned()),
|
||||||
|
AttributeValue::Rfc822Size(4286),
|
||||||
|
AttributeValue::Envelope(Envelope::default()),
|
||||||
|
AttributeValue::BodySection {
|
||||||
|
section: None,
|
||||||
|
index: None,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,7 +63,9 @@ flag_perm = { flag | "\\*" }
|
||||||
header_fld_name = { astring }
|
header_fld_name = { astring }
|
||||||
header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" }
|
header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" }
|
||||||
list_wildcards = @{ "%" | "*" }
|
list_wildcards = @{ "%" | "*" }
|
||||||
literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* }
|
// literal = @{ "{" ~ number ~ "}" ~ crlf ~ literal_str }
|
||||||
|
literal = { #crate::parser::literal_internal }
|
||||||
|
literal_str = { #crate::parser::literal::noop }
|
||||||
mailbox = { ^"INBOX" | astring }
|
mailbox = { ^"INBOX" | astring }
|
||||||
mailbox_data = { mailbox_data_flags | mailbox_data_list | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | mailbox_data_recent }
|
mailbox_data = { mailbox_data_flags | mailbox_data_list | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | mailbox_data_recent }
|
||||||
mailbox_data_exists = { number ~ sp ~ ^"EXISTS" }
|
mailbox_data_exists = { number ~ sp ~ ^"EXISTS" }
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use tui::{widgets::{Widget, StatefulWidget}, buffer::Buffer, layout::Rect};
|
use tui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
widgets::{StatefulWidget, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct MailTabState {
|
pub struct MailTabState {}
|
||||||
}
|
|
||||||
|
|
||||||
impl MailTabState {
|
impl MailTabState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -14,6 +17,5 @@ pub struct MailTab;
|
||||||
impl StatefulWidget for MailTab {
|
impl StatefulWidget for MailTab {
|
||||||
type State = MailTabState;
|
type State = MailTabState;
|
||||||
|
|
||||||
fn render(self, rect: Rect, buffer: &mut Buffer, state: &mut Self::State) {
|
fn render(self, rect: Rect, buffer: &mut Buffer, state: &mut Self::State) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,10 @@ use tui::{
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::Spans,
|
text::Spans,
|
||||||
widgets::*,
|
widgets::*,
|
||||||
Frame,
|
Frame, Terminal,
|
||||||
Terminal,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::mail_tab::{MailTabState, MailTab};
|
use self::mail_tab::{MailTab, MailTabState};
|
||||||
|
|
||||||
// pub(crate) type FrameType<'a> = Frame<'a, CrosstermBackend<Stdout>>;
|
// pub(crate) type FrameType<'a> = Frame<'a, CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue