This commit is contained in:
Michael Zhang 2021-08-06 12:03:06 -05:00
commit 48477b8ff2
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
23 changed files with 4555 additions and 0 deletions

22
Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "imap-proto"
version = "0.14.3"
authors = ["Dirkjan Ochtman <dirkjan@ochtman.nl>"]
description = "IMAP protocol parser and data structures"
documentation = "https://docs.rs/imap-proto"
keywords = ["imap", "email"]
categories = ["email", "network-programming", "parser-implementations"]
homepage = "https://github.com/djc/tokio-imap"
repository = "https://github.com/djc/tokio-imap"
license = "MIT/Apache-2.0"
edition = "2018"
readme = "../README.md"
[badges]
maintenance = { status = "passively-maintained" }
[dependencies]
nom = { version = "6", default-features = false, features = ["std"] }
[dev-dependencies]
assert_matches = "1.3"

View file

@ -0,0 +1,31 @@
use imap_proto::Response;
use std::io::Write;
fn main() -> std::io::Result<()> {
loop {
let line = {
print!("Enter IMAP4REV1 response: ");
std::io::stdout().flush().unwrap();
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
line
};
match Response::from_bytes(line.replace("\n", "\r\n").as_bytes()) {
Ok((remaining, command)) => {
println!("{:#?}", command);
if !remaining.is_empty() {
println!("Remaining data in buffer: {:?}", remaining);
}
}
Err(_) => {
println!("Error parsing the response. Is it correct? Exiting.");
break;
}
}
}
Ok(())
}

4
fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target
corpus
artifacts

21
fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "imap-proto-fuzz"
version = "0.0.1"
authors = ["Automatically generated"]
publish = false
[package.metadata]
cargo-fuzz = true
[dependencies.imap-proto]
path = ".."
[dependencies.libfuzzer-sys]
git = "https://github.com/rust-fuzz/libfuzzer-sys.git"
[[bin]]
name = "utf8_parse_response"
path = "fuzz_targets/utf8_parse_response.rs"
# Prevent this from interfering with workspaces
[workspace]
members = ["."]

View file

@ -0,0 +1,8 @@
#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate imap_proto;
// UTF-8
fuzz_target!(|data: &[u8]| {
let _ = imap_proto::Response::from_bytes(data);
});

387
src/builders/command.rs Normal file
View file

@ -0,0 +1,387 @@
use std::borrow::Cow;
use std::marker::PhantomData;
use std::ops::{RangeFrom, RangeInclusive};
use std::str;
use crate::types::{AttrMacro, Attribute, State};
pub struct CommandBuilder {}
impl CommandBuilder {
pub fn check() -> Command {
let args = b"CHECK".to_vec();
Command {
args,
next_state: None,
}
}
pub fn close() -> Command {
let args = b"CLOSE".to_vec();
Command {
args,
next_state: Some(State::Authenticated),
}
}
pub fn examine(mailbox: &str) -> SelectCommand<select::NoParams> {
let args = format!("EXAMINE \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
SelectCommand {
args,
state: PhantomData::default(),
}
}
pub fn fetch() -> FetchCommand<fetch::Empty> {
FetchCommand {
args: b"FETCH ".to_vec(),
state: PhantomData::default(),
}
}
pub fn list(reference: &str, glob: &str) -> Command {
let args = format!(
"LIST \"{}\" \"{}\"",
quoted_string(reference).unwrap(),
quoted_string(glob).unwrap()
)
.into_bytes();
Command {
args,
next_state: None,
}
}
pub fn login(user_name: &str, password: &str) -> Command {
let args = format!(
"LOGIN \"{}\" \"{}\"",
quoted_string(user_name).unwrap(),
quoted_string(password).unwrap()
)
.into_bytes();
Command {
args,
next_state: Some(State::Authenticated),
}
}
pub fn select(mailbox: &str) -> SelectCommand<select::NoParams> {
let args = format!("SELECT \"{}\"", quoted_string(mailbox).unwrap()).into_bytes();
SelectCommand {
args,
state: PhantomData::default(),
}
}
pub fn uid_fetch() -> FetchCommand<fetch::Empty> {
FetchCommand {
args: b"UID FETCH ".to_vec(),
state: PhantomData::default(),
}
}
}
pub struct Command {
pub args: Vec<u8>,
pub next_state: Option<State>,
}
pub struct SelectCommand<T> {
args: Vec<u8>,
state: PhantomData<T>,
}
impl SelectCommand<select::NoParams> {
// RFC 4551 CONDSTORE parameter (based on RFC 4466 `select-param`)
pub fn cond_store(mut self) -> SelectCommand<select::Params> {
self.args.extend(b" (CONDSTORE");
SelectCommand {
args: self.args,
state: PhantomData::default(),
}
}
}
impl From<SelectCommand<select::NoParams>> for Command {
fn from(cmd: SelectCommand<select::NoParams>) -> Command {
Command {
args: cmd.args,
next_state: Some(State::Selected),
}
}
}
impl From<SelectCommand<select::Params>> for Command {
fn from(mut cmd: SelectCommand<select::Params>) -> Command {
cmd.args.push(b')');
Command {
args: cmd.args,
next_state: Some(State::Selected),
}
}
}
pub mod select {
pub struct NoParams;
pub struct Params;
}
pub mod fetch {
pub struct Empty;
pub struct Messages;
pub struct Attributes;
pub struct Modifiers;
}
pub struct FetchCommand<T> {
args: Vec<u8>,
state: PhantomData<T>,
}
impl FetchCommand<fetch::Empty> {
pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
sequence_num(&mut self.args, num);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
sequence_range(&mut self.args, range);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
range_from(&mut self.args, range);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
}
impl FetchCommand<fetch::Messages> {
pub fn num(mut self, num: u32) -> FetchCommand<fetch::Messages> {
self.args.extend(b",");
sequence_num(&mut self.args, num);
self
}
pub fn range(mut self, range: RangeInclusive<u32>) -> FetchCommand<fetch::Messages> {
self.args.extend(b",");
sequence_range(&mut self.args, range);
self
}
pub fn range_from(mut self, range: RangeFrom<u32>) -> FetchCommand<fetch::Messages> {
self.args.extend(b",");
range_from(&mut self.args, range);
self
}
pub fn attr_macro(mut self, named: AttrMacro) -> FetchCommand<fetch::Modifiers> {
self.args.push(b' ');
self.args.extend(
match named {
AttrMacro::All => "ALL",
AttrMacro::Fast => "FAST",
AttrMacro::Full => "FULL",
}
.as_bytes(),
);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
self.args.extend(b" (");
push_attr(&mut self.args, attr);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
}
fn sequence_num(cmd: &mut Vec<u8>, num: u32) {
cmd.extend(num.to_string().as_bytes());
}
fn sequence_range(cmd: &mut Vec<u8>, range: RangeInclusive<u32>) {
cmd.extend(range.start().to_string().as_bytes());
cmd.push(b':');
cmd.extend(range.end().to_string().as_bytes());
}
fn range_from(cmd: &mut Vec<u8>, range: RangeFrom<u32>) {
cmd.extend(range.start.to_string().as_bytes());
cmd.extend(b":*");
}
impl FetchCommand<fetch::Attributes> {
pub fn attr(mut self, attr: Attribute) -> FetchCommand<fetch::Attributes> {
self.args.push(b' ');
push_attr(&mut self.args, attr);
self
}
pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
self.args.push(b')');
changed_since(&mut self.args, seq);
FetchCommand {
args: self.args,
state: PhantomData::default(),
}
}
}
fn push_attr(cmd: &mut Vec<u8>, attr: Attribute) {
cmd.extend(
match attr {
Attribute::Body => "BODY",
Attribute::Envelope => "ENVELOPE",
Attribute::Flags => "FLAGS",
Attribute::InternalDate => "INTERNALDATE",
Attribute::ModSeq => "MODSEQ",
Attribute::Rfc822 => "RFC822",
Attribute::Rfc822Size => "RFC822.SIZE",
Attribute::Rfc822Text => "RFC822.TEXT",
Attribute::Uid => "UID",
}
.as_bytes(),
);
}
impl From<FetchCommand<fetch::Attributes>> for Command {
fn from(mut cmd: FetchCommand<fetch::Attributes>) -> Command {
cmd.args.push(b')');
Command {
args: cmd.args,
next_state: None,
}
}
}
impl From<FetchCommand<fetch::Modifiers>> for Command {
fn from(cmd: FetchCommand<fetch::Modifiers>) -> Command {
Command {
args: cmd.args,
next_state: None,
}
}
}
impl FetchCommand<fetch::Modifiers> {
pub fn changed_since(mut self, seq: u64) -> FetchCommand<fetch::Modifiers> {
changed_since(&mut self.args, seq);
self
}
}
fn changed_since(cmd: &mut Vec<u8>, seq: u64) {
cmd.extend(b" (CHANGEDSINCE ");
cmd.extend(seq.to_string().as_bytes());
cmd.push(b')');
}
/// Returns an escaped string if necessary for use as a "quoted" string per
/// the IMAPv4 RFC. Return value does not include surrounding quote characters.
/// Will return Err if the argument contains illegal characters.
///
/// Relevant definitions from RFC 3501 formal syntax:
///
/// string = quoted / literal [literal elided here]
/// quoted = DQUOTE *QUOTED-CHAR DQUOTE
/// QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
/// quoted-specials = DQUOTE / "\"
/// TEXT-CHAR = <any CHAR except CR and LF>
fn quoted_string(s: &str) -> Result<Cow<str>, &'static str> {
let bytes = s.as_bytes();
let (mut start, mut new) = (0, Vec::<u8>::new());
for (i, b) in bytes.iter().enumerate() {
match *b {
b'\r' | b'\n' => {
return Err("CR and LF not allowed in quoted strings");
}
b'\\' | b'"' => {
if start < i {
new.extend(&bytes[start..i]);
}
new.push(b'\\');
new.push(*b);
start = i + 1;
}
_ => {}
};
}
if start == 0 {
Ok(Cow::Borrowed(s))
} else {
if start < bytes.len() {
new.extend(&bytes[start..]);
}
// Since the argument is a str, it must contain valid UTF-8. Since
// this function's transformation preserves the UTF-8 validity,
// unwrapping here should be okay.
Ok(Cow::Owned(String::from_utf8(new).unwrap()))
}
}
#[cfg(test)]
mod tests {
use super::{quoted_string, Attribute, Command, CommandBuilder};
#[test]
fn login() {
assert_eq!(
CommandBuilder::login("djc", "s3cr3t").args,
b"LOGIN \"djc\" \"s3cr3t\""
);
assert_eq!(
CommandBuilder::login("djc", "domain\\password").args,
b"LOGIN \"djc\" \"domain\\\\password\""
);
}
#[test]
fn select() {
let cmd = Command::from(CommandBuilder::select("INBOX"));
assert_eq!(&cmd.args, br#"SELECT "INBOX""#);
let cmd = Command::from(CommandBuilder::examine("INBOX").cond_store());
assert_eq!(&cmd.args, br#"EXAMINE "INBOX" (CONDSTORE)"#);
}
#[test]
fn fetch() {
let cmd: Command = CommandBuilder::fetch()
.range_from(1..)
.attr(Attribute::Uid)
.attr(Attribute::ModSeq)
.changed_since(13)
.into();
assert_eq!(cmd.args, &b"FETCH 1:* (UID MODSEQ) (CHANGEDSINCE 13)"[..]);
let cmd: Command = CommandBuilder::fetch()
.num(1)
.num(2)
.attr(Attribute::Uid)
.attr(Attribute::ModSeq)
.into();
assert_eq!(cmd.args, &b"FETCH 1,2 (UID MODSEQ)"[..]);
}
#[test]
fn test_quoted_string() {
assert_eq!(quoted_string("a").unwrap(), "a");
assert_eq!(quoted_string("").unwrap(), "");
assert_eq!(quoted_string("a\"b\\c").unwrap(), "a\\\"b\\\\c");
assert_eq!(quoted_string("\"foo\\").unwrap(), "\\\"foo\\\\");
assert!(quoted_string("\n").is_err());
}
}

1
src/builders/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod command;

6
src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod builders;
pub mod parser;
pub mod types;
pub use crate::parser::ParseResult;
pub use crate::types::*;

View file

@ -0,0 +1,76 @@
use std::collections::HashMap;
use crate::types::BodyStructure;
/// An utility parser helping to find the appropriate
/// section part from a FETCH response.
pub struct BodyStructParser<'a> {
root: &'a BodyStructure<'a>,
prefix: Vec<u32>,
iter: u32,
map: HashMap<Vec<u32>, &'a BodyStructure<'a>>,
}
impl<'a> BodyStructParser<'a> {
/// Returns a new parser
///
/// # Arguments
///
/// * `root` - The root of the `BodyStructure response.
pub fn new(root: &'a BodyStructure<'a>) -> Self {
let mut parser = BodyStructParser {
root,
prefix: vec![],
iter: 1,
map: HashMap::new(),
};
parser.parse(parser.root);
parser
}
/// Search particular element within the bodystructure.
///
/// # Arguments
///
/// * `func` - The filter used to search elements within the bodystructure.
pub fn search<F>(&self, func: F) -> Option<Vec<u32>>
where
F: Fn(&'a BodyStructure<'a>) -> bool,
{
let elem: Vec<_> = self
.map
.iter()
.filter_map(|(k, v)| {
if func(*v) {
let slice: &[u32] = k;
Some(slice)
} else {
None
}
})
.collect();
elem.first().map(|a| a.to_vec())
}
/// Reetr
fn parse(&mut self, node: &'a BodyStructure) {
match node {
BodyStructure::Multipart { bodies, .. } => {
let vec = self.prefix.clone();
self.map.insert(vec, node);
for (i, n) in bodies.iter().enumerate() {
self.iter += i as u32;
self.prefix.push(self.iter);
self.parse(n);
self.prefix.pop();
}
self.iter = 1;
}
_ => {
let vec = self.prefix.clone();
self.map.insert(vec, node);
}
};
}
}

331
src/parser/core.rs Normal file
View file

@ -0,0 +1,331 @@
use nom::{
branch::alt,
bytes::streaming::{escaped, tag, tag_no_case, take, take_while, take_while1},
character::streaming::{char, digit1, one_of},
combinator::{map, map_res},
multi::{separated_list0, separated_list1},
sequence::{delimited, tuple},
IResult,
};
use std::str::{from_utf8, FromStr};
// ----- number -----
// number = 1*DIGIT
// ; Unsigned 32-bit integer
// ; (0 <= n < 4,294,967,296)
pub fn number(i: &[u8]) -> IResult<&[u8], u32> {
let (i, bytes) = digit1(i)?;
match from_utf8(bytes).ok().and_then(|s| u32::from_str(s).ok()) {
Some(v) => Ok((i, v)),
None => Err(nom::Err::Error(nom::error::make_error(
i,
nom::error::ErrorKind::ParseTo,
))),
}
}
// same as `number` but 64-bit
pub fn number_64(i: &[u8]) -> IResult<&[u8], u64> {
let (i, bytes) = digit1(i)?;
match from_utf8(bytes).ok().and_then(|s| u64::from_str(s).ok()) {
Some(v) => Ok((i, v)),
None => Err(nom::Err::Error(nom::error::make_error(
i,
nom::error::ErrorKind::ParseTo,
))),
}
}
// seq-range = seq-number ":" seq-number
// ; two seq-number values and all values between
// ; these two regardless of order.
// ; seq-number is a nz-number
pub fn sequence_range(i: &[u8]) -> IResult<&[u8], std::ops::RangeInclusive<u32>> {
map(tuple((number, tag(":"), number)), |(s, _, e)| s..=e)(i)
}
// sequence-set = (seq-number / seq-range) *("," sequence-set)
// ; set of seq-number values, regardless of order.
// ; Servers MAY coalesce overlaps and/or execute the
// ; sequence in any order.
pub fn sequence_set(i: &[u8]) -> IResult<&[u8], Vec<std::ops::RangeInclusive<u32>>> {
separated_list1(tag(","), alt((sequence_range, map(number, |n| n..=n))))(i)
}
// ----- string -----
// string = quoted / literal
pub fn string(i: &[u8]) -> IResult<&[u8], &[u8]> {
alt((quoted, literal))(i)
}
// string bytes as utf8
pub fn string_utf8(i: &[u8]) -> IResult<&[u8], &str> {
map_res(string, from_utf8)(i)
}
// quoted = DQUOTE *QUOTED-CHAR DQUOTE
pub fn quoted(i: &[u8]) -> IResult<&[u8], &[u8]> {
delimited(
char('"'),
escaped(
take_while1(|byte| is_text_char(byte) && !is_quoted_specials(byte)),
'\\',
one_of("\\\""),
),
char('"'),
)(i)
}
// quoted bytes as utf8
pub fn quoted_utf8(i: &[u8]) -> IResult<&[u8], &str> {
map_res(quoted, from_utf8)(i)
}
// quoted-specials = DQUOTE / "\"
pub fn is_quoted_specials(c: u8) -> bool {
c == b'"' || c == b'\\'
}
/// literal = "{" number "}" CRLF *CHAR8
/// ; Number represents the number of CHAR8s
pub fn literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let mut parser = tuple((tag(b"{"), number, tag(b"}"), tag("\r\n")));
let (remaining, (_, count, _, _)) = parser(input)?;
let (remaining, data) = take(count)(remaining)?;
if !data.iter().all(|byte| is_char8(*byte)) {
// FIXME: what ErrorKind should this have?
return Err(nom::Err::Error(nom::error::Error::new(
remaining,
nom::error::ErrorKind::Verify,
)));
}
Ok((remaining, data))
}
/// CHAR8 = %x01-ff ; any OCTET except NUL, %x00
pub fn is_char8(i: u8) -> bool {
i != 0
}
// ----- astring ----- atom (roughly) or string
// astring = 1*ASTRING-CHAR / string
pub fn astring(i: &[u8]) -> IResult<&[u8], &[u8]> {
alt((take_while1(is_astring_char), string))(i)
}
// astring bytes as utf8
pub fn astring_utf8(i: &[u8]) -> IResult<&[u8], &str> {
map_res(astring, from_utf8)(i)
}
// ASTRING-CHAR = ATOM-CHAR / resp-specials
pub fn is_astring_char(c: u8) -> bool {
is_atom_char(c) || is_resp_specials(c)
}
// ATOM-CHAR = <any CHAR except atom-specials>
pub fn is_atom_char(c: u8) -> bool {
is_char(c) && !is_atom_specials(c)
}
// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials
pub fn is_atom_specials(c: u8) -> bool {
c == b'('
|| c == b')'
|| c == b'{'
|| c == b' '
|| c < 32
|| is_list_wildcards(c)
|| is_quoted_specials(c)
|| is_resp_specials(c)
}
// resp-specials = "]"
pub fn is_resp_specials(c: u8) -> bool {
c == b']'
}
// atom = 1*ATOM-CHAR
pub fn atom(i: &[u8]) -> IResult<&[u8], &str> {
map_res(take_while1(is_atom_char), from_utf8)(i)
}
// ----- nstring ----- nil or string
// nstring = string / nil
pub fn nstring(i: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
alt((map(nil, |_| None), map(string, Some)))(i)
}
// nstring bytes as utf8
pub fn nstring_utf8(i: &[u8]) -> IResult<&[u8], Option<&str>> {
alt((map(nil, |_| None), map(string_utf8, Some)))(i)
}
// nil = "NIL"
pub fn nil(i: &[u8]) -> IResult<&[u8], &[u8]> {
tag_no_case("NIL")(i)
}
// ----- text -----
// text = 1*TEXT-CHAR
pub fn text(i: &[u8]) -> IResult<&[u8], &str> {
map_res(take_while(is_text_char), from_utf8)(i)
}
// TEXT-CHAR = <any CHAR except CR and LF>
pub fn is_text_char(c: u8) -> bool {
is_char(c) && c != b'\r' && c != b'\n'
}
// CHAR = %x01-7F
// ; any 7-bit US-ASCII character,
// ; excluding NUL
// From RFC5234
pub fn is_char(c: u8) -> bool {
matches!(c, 0x01..=0x7F)
}
// ----- others -----
// list-wildcards = "%" / "*"
pub fn is_list_wildcards(c: u8) -> bool {
c == b'%' || c == b'*'
}
pub fn paren_delimited<'a, F, O, E>(f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>
where
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
E: nom::error::ParseError<&'a [u8]>,
{
delimited(char('('), f, char(')'))
}
pub fn parenthesized_nonempty_list<'a, F, O, E>(
f: F,
) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Vec<O>, E>
where
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
E: nom::error::ParseError<&'a [u8]>,
{
delimited(char('('), separated_list1(char(' '), f), char(')'))
}
pub fn parenthesized_list<'a, F, O, E>(f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Vec<O>, E>
where
F: FnMut(&'a [u8]) -> IResult<&'a [u8], O, E>,
E: nom::error::ParseError<&'a [u8]>,
{
delimited(char('('), separated_list0(char(' '), f), char(')'))
}
pub fn opt_opt<'a, F, O, E>(mut f: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], Option<O>, E>
where
F: FnMut(&'a [u8]) -> IResult<&'a [u8], Option<O>, E>,
{
move |i: &[u8]| match f(i) {
Ok((i, o)) => Ok((i, o)),
Err(nom::Err::Error(_)) => Ok((i, None)),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
#[test]
fn test_quoted() {
let (rem, val) = quoted(br#""Hello"???"#).unwrap();
assert_eq!(rem, b"???");
assert_eq!(val, b"Hello");
// Allowed escapes...
assert!(quoted(br#""Hello \" "???"#).is_ok());
assert!(quoted(br#""Hello \\ "???"#).is_ok());
// Not allowed escapes...
assert!(quoted(br#""Hello \a "???"#).is_err());
assert!(quoted(br#""Hello \z "???"#).is_err());
assert!(quoted(br#""Hello \? "???"#).is_err());
let (rem, val) = quoted(br#""Hello \"World\""???"#).unwrap();
assert_eq!(rem, br#"???"#);
// Should it be this (Hello \"World\") ...
assert_eq!(val, br#"Hello \"World\""#);
// ... or this (Hello "World")?
//assert_eq!(val, br#"Hello "World""#); // fails
// Test Incomplete
assert_matches!(quoted(br#""#), Err(nom::Err::Incomplete(_)));
assert_matches!(quoted(br#""\"#), Err(nom::Err::Incomplete(_)));
assert_matches!(quoted(br#""Hello "#), Err(nom::Err::Incomplete(_)));
// Test Error
assert_matches!(quoted(br#"\"#), Err(nom::Err::Error(_)));
}
#[test]
fn test_string_literal() {
match string(b"{3}\r\nXYZ") {
Ok((_, value)) => {
assert_eq!(value, b"XYZ");
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_astring() {
match astring(b"text ") {
Ok((_, value)) => {
assert_eq!(value, b"text");
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_sequence_range() {
match sequence_range(b"23:28 ") {
Ok((_, value)) => {
assert_eq!(*value.start(), 23);
assert_eq!(*value.end(), 28);
assert_eq!(value.collect::<Vec<u32>>(), vec![23, 24, 25, 26, 27, 28]);
}
rsp => panic!("Unexpected response {:?}", rsp),
}
}
#[test]
fn test_sequence_set() {
match sequence_set(b"1,2:8,10,15:30 ") {
Ok((_, value)) => {
assert_eq!(value.len(), 4);
let v = &value[0];
assert_eq!(*v.start(), 1);
assert_eq!(*v.end(), 1);
let v = &value[1];
assert_eq!(*v.start(), 2);
assert_eq!(*v.end(), 8);
let v = &value[2];
assert_eq!(*v.start(), 10);
assert_eq!(*v.end(), 10);
let v = &value[3];
assert_eq!(*v.start(), 15);
assert_eq!(*v.end(), 30);
}
rsp => panic!("Unexpected response {:?}", rsp),
}
}
}

27
src/parser/mod.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::types::Response;
use nom::{branch::alt, IResult};
pub mod core;
pub mod bodystructure;
pub mod rfc2087;
pub mod rfc3501;
pub mod rfc4315;
pub mod rfc4551;
pub mod rfc5161;
pub mod rfc5256;
pub mod rfc5464;
pub mod rfc7162;
#[cfg(test)]
mod tests;
pub fn parse_response(msg: &[u8]) -> ParseResult {
alt((
rfc3501::continue_req,
rfc3501::response_data,
rfc3501::response_tagged,
))(msg)
}
pub type ParseResult<'a> = IResult<&'a [u8], Response<'a>>;

279
src/parser/rfc2087.rs Normal file
View file

@ -0,0 +1,279 @@
//!
//! https://tools.ietf.org/html/rfc2087
//!
//! IMAP4 QUOTA extension
//!
use std::borrow::Cow;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
character::streaming::space1,
combinator::map,
multi::many0,
multi::separated_list0,
sequence::{delimited, preceded, tuple},
IResult,
};
use crate::parser::core::astring_utf8;
use crate::types::*;
use super::core::number_64;
/// 5.1. QUOTA Response
/// ```ignore
/// quota_response ::= "QUOTA" SP astring SP quota_list
/// ```
pub(crate) fn quota(i: &[u8]) -> IResult<&[u8], Response> {
let (rest, (_, _, root_name, _, resources)) = tuple((
tag_no_case("QUOTA"),
space1,
map(astring_utf8, Cow::Borrowed),
space1,
quota_list,
))(i)?;
Ok((
rest,
Response::Quota(Quota {
root_name,
resources,
}),
))
}
/// ```ignore
/// quota_list ::= "(" #quota_resource ")"
/// ```
pub(crate) fn quota_list(i: &[u8]) -> IResult<&[u8], Vec<QuotaResource>> {
delimited(tag("("), separated_list0(space1, quota_resource), tag(")"))(i)
}
/// ```ignore
/// quota_resource ::= atom SP number SP number
/// ```
pub(crate) fn quota_resource(i: &[u8]) -> IResult<&[u8], QuotaResource> {
let (rest, (name, _, usage, _, limit)) = tuple((
quota_resource_name,
tag(" "),
number_64,
tag(" "),
number_64,
))(i)?;
Ok((rest, QuotaResource { name, usage, limit }))
}
pub(crate) fn quota_resource_name(i: &[u8]) -> IResult<&[u8], QuotaResourceName> {
alt((
map(tag_no_case("STORAGE"), |_| QuotaResourceName::Storage),
map(tag_no_case("MESSAGE"), |_| QuotaResourceName::Message),
map(map(astring_utf8, Cow::Borrowed), QuotaResourceName::Atom),
))(i)
}
/// 5.2. QUOTAROOT Response
/// ```ignore
/// quotaroot_response ::= "QUOTAROOT" SP astring *(SP astring)
/// ```
pub(crate) fn quota_root(i: &[u8]) -> IResult<&[u8], Response> {
let (rest, (_, _, mailbox_name, quota_root_names)) = tuple((
tag_no_case("QUOTAROOT"),
space1,
map(astring_utf8, Cow::Borrowed),
many0(preceded(space1, map(astring_utf8, Cow::Borrowed))),
))(i)?;
Ok((
rest,
Response::QuotaRoot(QuotaRoot {
mailbox_name,
quota_root_names,
}),
))
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use std::borrow::Cow;
#[test]
fn test_quota() {
assert_matches!(
quota(b"QUOTA \"\" (STORAGE 10 512)"),
Ok((_, r)) => {
assert_eq!(
r,
Response::Quota(Quota {
root_name: Cow::Borrowed(""),
resources: vec![QuotaResource {
name: QuotaResourceName::Storage,
usage: 10,
limit: 512
}]
})
);
}
);
}
#[test]
fn test_quota_response_data() {
assert_matches!(
crate::parser::rfc3501::response_data(b"* QUOTA \"\" (STORAGE 10 512)\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::Quota(Quota {
root_name: Cow::Borrowed(""),
resources: vec![QuotaResource {
name: QuotaResourceName::Storage,
usage: 10,
limit: 512
}]
})
);
}
);
}
#[test]
fn test_quota_list() {
assert_matches!(
quota_list(b"(STORAGE 10 512)"),
Ok((_, r)) => {
assert_eq!(
r,
vec![QuotaResource {
name: QuotaResourceName::Storage,
usage: 10,
limit: 512
}]
);
}
);
assert_matches!(
quota_list(b"(MESSAGE 100 512)"),
Ok((_, r)) => {
assert_eq!(
r,
vec![QuotaResource {
name: QuotaResourceName::Message,
usage: 100,
limit: 512
}]
);
}
);
assert_matches!(
quota_list(b"(DAILY 55 200)"),
Ok((_, r)) => {
assert_eq!(
r,
vec![QuotaResource {
name: QuotaResourceName::Atom(Cow::Borrowed("DAILY")),
usage: 55,
limit: 200
}]
);
}
);
}
#[test]
fn test_quota_root_response_data() {
assert_matches!(
crate::parser::rfc3501::response_data("* QUOTAROOT INBOX \"\"\r\n".as_bytes()),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("INBOX"),
quota_root_names: vec![Cow::Borrowed("")]
})
);
}
);
}
fn terminated_quota_root(i: &[u8]) -> IResult<&[u8], Response> {
nom::sequence::terminated(quota_root, nom::bytes::streaming::tag("\r\n"))(i)
}
#[test]
fn test_quota_root_without_root_names() {
assert_matches!(
terminated_quota_root(b"QUOTAROOT comp.mail.mime\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("comp.mail.mime"),
quota_root_names: vec![]
})
);
}
);
}
#[test]
fn test_quota_root2() {
assert_matches!(
terminated_quota_root(b"QUOTAROOT INBOX HU\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("INBOX"),
quota_root_names: vec![Cow::Borrowed("HU")]
})
);
}
);
assert_matches!(
terminated_quota_root(b"QUOTAROOT INBOX \"\"\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("INBOX"),
quota_root_names: vec![Cow::Borrowed("")]
})
);
}
);
assert_matches!(
terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\"\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("Inbox"),
quota_root_names: vec![Cow::Borrowed("#Account")]
})
);
}
);
assert_matches!(
terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\" \"#Mailbox\"\r\n"),
Ok((_, r)) => {
assert_eq!(
r,
Response::QuotaRoot(QuotaRoot{
mailbox_name: Cow::Borrowed("Inbox"),
quota_root_names: vec![Cow::Borrowed("#Account"), Cow::Borrowed("#Mailbox")]
})
);
}
);
}
}

View file

@ -0,0 +1,72 @@
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
character::streaming::char,
combinator::{map, opt},
multi::many0,
sequence::{delimited, preceded, tuple},
IResult,
};
use std::borrow::Cow;
use crate::{parser::core::*, types::*};
pub fn section_part(i: &[u8]) -> IResult<&[u8], Vec<u32>> {
let (i, (part, mut rest)) = tuple((number, many0(preceded(char('.'), number))))(i)?;
rest.insert(0, part);
Ok((i, rest))
}
pub fn section_msgtext(i: &[u8]) -> IResult<&[u8], MessageSection> {
alt((
map(
tuple((
tag_no_case("HEADER.FIELDS"),
opt(tag_no_case(".NOT")),
tag(" "),
parenthesized_list(astring),
)),
|_| MessageSection::Header,
),
map(tag_no_case("HEADER"), |_| MessageSection::Header),
map(tag_no_case("TEXT"), |_| MessageSection::Text),
))(i)
}
pub fn section_text(i: &[u8]) -> IResult<&[u8], MessageSection> {
alt((
section_msgtext,
map(tag_no_case("MIME"), |_| MessageSection::Mime),
))(i)
}
pub fn section_spec(i: &[u8]) -> IResult<&[u8], SectionPath> {
alt((
map(section_msgtext, SectionPath::Full),
map(
tuple((section_part, opt(preceded(char('.'), section_text)))),
|(part, text)| SectionPath::Part(part, text),
),
))(i)
}
pub fn section(i: &[u8]) -> IResult<&[u8], Option<SectionPath>> {
delimited(char('['), opt(section_spec), char(']'))(i)
}
pub fn msg_att_body_section(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(
tuple((
tag_no_case("BODY"),
section,
opt(delimited(char('<'), number, char('>'))),
tag(" "),
nstring,
)),
|(_, section, index, _, data)| AttributeValue::BodySection {
section,
index,
data: data.map(Cow::Borrowed),
},
)(i)
}

View file

@ -0,0 +1,539 @@
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
character::streaming::char,
combinator::{map, opt},
multi::many1,
sequence::{delimited, preceded, tuple},
IResult,
};
use std::borrow::Cow;
use crate::{
parser::{core::*, rfc3501::envelope},
types::*,
};
// body-fields = body-fld-param SP body-fld-id SP body-fld-desc SP
// body-fld-enc SP body-fld-octets
fn body_fields(i: &[u8]) -> IResult<&[u8], BodyFields> {
let (i, (param, _, id, _, description, _, transfer_encoding, _, octets)) = tuple((
body_param,
tag(" "),
// body id seems to refer to the Message-ID or possibly Content-ID header, which
// by the definition in RFC 2822 seems to resolve to all ASCII characters (through
// a large amount of indirection which I did not have the patience to fully explore)
nstring_utf8,
tag(" "),
// Per https://tools.ietf.org/html/rfc2045#section-8, description should be all ASCII
nstring_utf8,
tag(" "),
body_encoding,
tag(" "),
number,
))(i)?;
Ok((
i,
BodyFields {
param,
id: id.map(Cow::Borrowed),
description: description.map(Cow::Borrowed),
transfer_encoding,
octets,
},
))
}
// body-ext-1part = body-fld-md5 [SP body-fld-dsp [SP body-fld-lang
// [SP body-fld-loc *(SP body-extension)]]]
// ; MUST NOT be returned on non-extensible
// ; "BODY" fetch
fn body_ext_1part(i: &[u8]) -> IResult<&[u8], BodyExt1Part> {
let (i, (md5, disposition, language, location, extension)) = tuple((
// Per RFC 1864, MD5 values are base64-encoded
opt_opt(preceded(tag(" "), nstring_utf8)),
opt_opt(preceded(tag(" "), body_disposition)),
opt_opt(preceded(tag(" "), body_lang)),
// Location appears to reference a URL, which by RFC 1738 (section 2.2) should be ASCII
opt_opt(preceded(tag(" "), nstring_utf8)),
opt(preceded(tag(" "), body_extension)),
))(i)?;
Ok((
i,
BodyExt1Part {
md5: md5.map(Cow::Borrowed),
disposition,
language,
location: location.map(Cow::Borrowed),
extension,
},
))
}
// body-ext-mpart = body-fld-param [SP body-fld-dsp [SP body-fld-lang
// [SP body-fld-loc *(SP body-extension)]]]
// ; MUST NOT be returned on non-extensible
// ; "BODY" fetch
fn body_ext_mpart(i: &[u8]) -> IResult<&[u8], BodyExtMPart> {
let (i, (param, disposition, language, location, extension)) = tuple((
opt_opt(preceded(tag(" "), body_param)),
opt_opt(preceded(tag(" "), body_disposition)),
opt_opt(preceded(tag(" "), body_lang)),
// Location appears to reference a URL, which by RFC 1738 (section 2.2) should be ASCII
opt_opt(preceded(tag(" "), nstring_utf8)),
opt(preceded(tag(" "), body_extension)),
))(i)?;
Ok((
i,
BodyExtMPart {
param,
disposition,
language,
location: location.map(Cow::Borrowed),
extension,
},
))
}
fn body_encoding(i: &[u8]) -> IResult<&[u8], ContentEncoding> {
alt((
delimited(
char('"'),
alt((
map(tag_no_case("7BIT"), |_| ContentEncoding::SevenBit),
map(tag_no_case("8BIT"), |_| ContentEncoding::EightBit),
map(tag_no_case("BINARY"), |_| ContentEncoding::Binary),
map(tag_no_case("BASE64"), |_| ContentEncoding::Base64),
map(tag_no_case("QUOTED-PRINTABLE"), |_| {
ContentEncoding::QuotedPrintable
}),
)),
char('"'),
),
map(string_utf8, |enc| {
ContentEncoding::Other(Cow::Borrowed(enc))
}),
))(i)
}
fn body_lang(i: &[u8]) -> IResult<&[u8], Option<Vec<Cow<'_, str>>>> {
alt((
// body language seems to refer to RFC 3066 language tags, which should be ASCII-only
map(nstring_utf8, |v| v.map(|s| vec![Cow::Borrowed(s)])),
map(
parenthesized_nonempty_list(map(string_utf8, Cow::Borrowed)),
Option::from,
),
))(i)
}
fn body_param(i: &[u8]) -> IResult<&[u8], BodyParams> {
alt((
map(nil, |_| None),
map(
parenthesized_nonempty_list(map(
tuple((string_utf8, tag(" "), string_utf8)),
|(key, _, val)| (Cow::Borrowed(key), Cow::Borrowed(val)),
)),
Option::from,
),
))(i)
}
fn body_extension(i: &[u8]) -> IResult<&[u8], BodyExtension> {
alt((
map(number, BodyExtension::Num),
// Cannot find documentation on character encoding for body extension values.
// So far, assuming UTF-8 seems fine, please report if you run into issues here.
map(nstring_utf8, |v| BodyExtension::Str(v.map(Cow::Borrowed))),
map(
parenthesized_nonempty_list(body_extension),
BodyExtension::List,
),
))(i)
}
fn body_disposition(i: &[u8]) -> IResult<&[u8], Option<ContentDisposition>> {
alt((
map(nil, |_| None),
paren_delimited(map(
tuple((string_utf8, tag(" "), body_param)),
|(ty, _, params)| {
Some(ContentDisposition {
ty: Cow::Borrowed(ty),
params,
})
},
)),
))(i)
}
fn body_type_basic(i: &[u8]) -> IResult<&[u8], BodyStructure> {
map(
tuple((
string_utf8,
tag(" "),
string_utf8,
tag(" "),
body_fields,
body_ext_1part,
)),
|(ty, _, subtype, _, fields, ext)| BodyStructure::Basic {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed(ty),
subtype: Cow::Borrowed(subtype),
params: fields.param,
},
disposition: ext.disposition,
language: ext.language,
location: ext.location,
},
other: BodyContentSinglePart {
id: fields.id,
md5: ext.md5,
octets: fields.octets,
description: fields.description,
transfer_encoding: fields.transfer_encoding,
},
extension: ext.extension,
},
)(i)
}
fn body_type_text(i: &[u8]) -> IResult<&[u8], BodyStructure> {
map(
tuple((
tag_no_case("\"TEXT\""),
tag(" "),
string_utf8,
tag(" "),
body_fields,
tag(" "),
number,
body_ext_1part,
)),
|(_, _, subtype, _, fields, _, lines, ext)| BodyStructure::Text {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("TEXT"),
subtype: Cow::Borrowed(subtype),
params: fields.param,
},
disposition: ext.disposition,
language: ext.language,
location: ext.location,
},
other: BodyContentSinglePart {
id: fields.id,
md5: ext.md5,
octets: fields.octets,
description: fields.description,
transfer_encoding: fields.transfer_encoding,
},
lines,
extension: ext.extension,
},
)(i)
}
fn body_type_message(i: &[u8]) -> IResult<&[u8], BodyStructure> {
map(
tuple((
tag_no_case("\"MESSAGE\" \"RFC822\""),
tag(" "),
body_fields,
tag(" "),
envelope,
tag(" "),
body,
tag(" "),
number,
body_ext_1part,
)),
|(_, _, fields, _, envelope, _, body, _, lines, ext)| BodyStructure::Message {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("MESSAGE"),
subtype: Cow::Borrowed("RFC822"),
params: fields.param,
},
disposition: ext.disposition,
language: ext.language,
location: ext.location,
},
other: BodyContentSinglePart {
id: fields.id,
md5: ext.md5,
octets: fields.octets,
description: fields.description,
transfer_encoding: fields.transfer_encoding,
},
envelope,
body: Box::new(body),
lines,
extension: ext.extension,
},
)(i)
}
fn body_type_multipart(i: &[u8]) -> IResult<&[u8], BodyStructure> {
map(
tuple((many1(body), tag(" "), string_utf8, body_ext_mpart)),
|(bodies, _, subtype, ext)| BodyStructure::Multipart {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("MULTIPART"),
subtype: Cow::Borrowed(subtype),
params: ext.param,
},
disposition: ext.disposition,
language: ext.language,
location: ext.location,
},
bodies,
extension: ext.extension,
},
)(i)
}
pub(crate) fn body(i: &[u8]) -> IResult<&[u8], BodyStructure> {
paren_delimited(alt((
body_type_text,
body_type_message,
body_type_basic,
body_type_multipart,
)))(i)
}
pub(crate) fn msg_att_body_structure(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(tuple((tag_no_case("BODYSTRUCTURE "), body)), |(_, body)| {
AttributeValue::BodyStructure(body)
})(i)
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
const EMPTY: &[u8] = &[];
// body-fld-param SP body-fld-id SP body-fld-desc SP body-fld-enc SP body-fld-octets
const BODY_FIELDS: &str = r#"("foo" "bar") "id" "desc" "7BIT" 1337"#;
const BODY_FIELD_PARAM_PAIR: (Cow<'_, str>, Cow<'_, str>) =
(Cow::Borrowed("foo"), Cow::Borrowed("bar"));
const BODY_FIELD_ID: Option<Cow<'_, str>> = Some(Cow::Borrowed("id"));
const BODY_FIELD_DESC: Option<Cow<'_, str>> = Some(Cow::Borrowed("desc"));
const BODY_FIELD_ENC: ContentEncoding = ContentEncoding::SevenBit;
const BODY_FIELD_OCTETS: u32 = 1337;
fn mock_body_text() -> (String, BodyStructure<'static>) {
(
format!(r#"("TEXT" "PLAIN" {} 42)"#, BODY_FIELDS),
BodyStructure::Text {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("TEXT"),
subtype: Cow::Borrowed("PLAIN"),
params: Some(vec![BODY_FIELD_PARAM_PAIR]),
},
disposition: None,
language: None,
location: None,
},
other: BodyContentSinglePart {
md5: None,
transfer_encoding: BODY_FIELD_ENC,
octets: BODY_FIELD_OCTETS,
id: BODY_FIELD_ID,
description: BODY_FIELD_DESC,
},
lines: 42,
extension: None,
},
)
}
#[test]
fn test_body_param_data() {
assert_matches!(body_param(br#"NIL"#), Ok((EMPTY, None)));
assert_matches!(
body_param(br#"("foo" "bar")"#),
Ok((EMPTY, Some(param))) => {
assert_eq!(param, vec![(Cow::Borrowed("foo"), Cow::Borrowed("bar"))]);
}
);
}
#[test]
fn test_body_lang_data() {
assert_matches!(
body_lang(br#""bob""#),
Ok((EMPTY, Some(langs))) => {
assert_eq!(langs, vec!["bob"]);
}
);
assert_matches!(
body_lang(br#"("one" "two")"#),
Ok((EMPTY, Some(langs))) => {
assert_eq!(langs, vec!["one", "two"]);
}
);
assert_matches!(body_lang(br#"NIL"#), Ok((EMPTY, None)));
}
#[test]
fn test_body_extension_data() {
assert_matches!(
body_extension(br#""blah""#),
Ok((EMPTY, BodyExtension::Str(Some(Cow::Borrowed("blah")))))
);
assert_matches!(
body_extension(br#"NIL"#),
Ok((EMPTY, BodyExtension::Str(None)))
);
assert_matches!(
body_extension(br#"("hello")"#),
Ok((EMPTY, BodyExtension::List(list))) => {
assert_eq!(list, vec![BodyExtension::Str(Some(Cow::Borrowed("hello")))]);
}
);
assert_matches!(
body_extension(br#"(1337)"#),
Ok((EMPTY, BodyExtension::List(list))) => {
assert_eq!(list, vec![BodyExtension::Num(1337)]);
}
);
}
#[test]
fn test_body_disposition_data() {
assert_matches!(body_disposition(br#"NIL"#), Ok((EMPTY, None)));
assert_matches!(
body_disposition(br#"("attachment" ("FILENAME" "pages.pdf"))"#),
Ok((EMPTY, Some(disposition))) => {
assert_eq!(disposition, ContentDisposition {
ty: Cow::Borrowed("attachment"),
params: Some(vec![
(Cow::Borrowed("FILENAME"), Cow::Borrowed("pages.pdf"))
])
});
}
);
}
#[test]
fn test_body_structure_text() {
let (body_str, body_struct) = mock_body_text();
assert_matches!(
body(body_str.as_bytes()),
Ok((_, text)) => {
assert_eq!(text, body_struct);
}
);
}
#[test]
fn test_body_structure_text_with_ext() {
let body_str = format!(r#"("TEXT" "PLAIN" {} 42 NIL NIL NIL NIL)"#, BODY_FIELDS);
let (_, text_body_struct) = mock_body_text();
assert_matches!(
body(body_str.as_bytes()),
Ok((_, text)) => {
assert_eq!(text, text_body_struct)
}
);
}
#[test]
fn test_body_structure_basic() {
const BODY: &[u8] = br#"("APPLICATION" "PDF" ("NAME" "pages.pdf") NIL NIL "BASE64" 38838 NIL ("attachment" ("FILENAME" "pages.pdf")) NIL NIL)"#;
assert_matches!(
body(BODY),
Ok((_, basic)) => {
assert_eq!(basic, BodyStructure::Basic {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("APPLICATION"),
subtype: Cow::Borrowed("PDF"),
params: Some(vec![(Cow::Borrowed("NAME"), Cow::Borrowed("pages.pdf"))])
},
disposition: Some(ContentDisposition {
ty: Cow::Borrowed("attachment"),
params: Some(vec![(Cow::Borrowed("FILENAME"), Cow::Borrowed("pages.pdf"))])
}),
language: None,
location: None,
},
other: BodyContentSinglePart {
transfer_encoding: ContentEncoding::Base64,
octets: 38838,
id: None,
md5: None,
description: None,
},
extension: None,
})
}
);
}
#[test]
fn test_body_structure_message() {
let (text_body_str, _) = mock_body_text();
let envelope_str = r#"("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>")"#;
let body_str = format!(
r#"("MESSAGE" "RFC822" {} {} {} 42)"#,
BODY_FIELDS, envelope_str, text_body_str
);
assert_matches!(
body(body_str.as_bytes()),
Ok((_, BodyStructure::Message { .. }))
);
}
#[test]
fn test_body_structure_multipart() {
let (text_body_str1, text_body_struct1) = mock_body_text();
let (text_body_str2, text_body_struct2) = mock_body_text();
let body_str = format!(
r#"({}{} "ALTERNATIVE" NIL NIL NIL NIL)"#,
text_body_str1, text_body_str2
);
assert_matches!(
body(body_str.as_bytes()),
Ok((_, multipart)) => {
assert_eq!(multipart, BodyStructure::Multipart {
common: BodyContentCommon {
ty: ContentType {
ty: Cow::Borrowed("MULTIPART"),
subtype: Cow::Borrowed("ALTERNATIVE"),
params: None
},
language: None,
location: None,
disposition: None,
},
bodies: vec![
text_body_struct1,
text_body_struct2,
],
extension: None
});
}
);
}
}

754
src/parser/rfc3501/mod.rs Normal file
View file

@ -0,0 +1,754 @@
//!
//! https://tools.ietf.org/html/rfc3501
//!
//! INTERNET MESSAGE ACCESS PROTOCOL
//!
use std::borrow::Cow;
use std::str::from_utf8;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case, take_while, take_while1},
character::streaming::char,
combinator::{map, map_res, opt, recognize},
multi::{many0, many1},
sequence::{delimited, pair, preceded, terminated, tuple},
IResult,
};
use crate::{
parser::{
core::*, rfc2087, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551, rfc5161,
rfc5256, rfc5464, rfc7162,
},
types::*,
};
pub mod body;
pub mod body_structure;
fn is_tag_char(c: u8) -> bool {
c != b'+' && is_astring_char(c)
}
fn status_ok(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("OK"), |_s| Status::Ok)(i)
}
fn status_no(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("NO"), |_s| Status::No)(i)
}
fn status_bad(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("BAD"), |_s| Status::Bad)(i)
}
fn status_preauth(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("PREAUTH"), |_s| Status::PreAuth)(i)
}
fn status_bye(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("BYE"), |_s| Status::Bye)(i)
}
fn status(i: &[u8]) -> IResult<&[u8], Status> {
alt((status_ok, status_no, status_bad, status_preauth, status_bye))(i)
}
fn mailbox(i: &[u8]) -> IResult<&[u8], &str> {
map(astring_utf8, |s| {
if s.eq_ignore_ascii_case("INBOX") {
"INBOX"
} else {
s
}
})(i)
}
fn flag_extension(i: &[u8]) -> IResult<&[u8], &str> {
map_res(
recognize(pair(tag(b"\\"), take_while(is_atom_char))),
from_utf8,
)(i)
}
fn flag(i: &[u8]) -> IResult<&[u8], &str> {
alt((flag_extension, atom))(i)
}
fn flag_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<str>>> {
// Correct code is
// parenthesized_list(flag)(i)
//
// Unfortunately, Zoho Mail Server (imap.zoho.com) sends the following response:
// * FLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)
//
// As a workaround, "\*" is allowed here.
parenthesized_list(map(flag_perm, Cow::Borrowed))(i)
}
fn flag_perm(i: &[u8]) -> IResult<&[u8], &str> {
alt((map_res(tag(b"\\*"), from_utf8), flag))(i)
}
fn resp_text_code_alert(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"ALERT"), |_| ResponseCode::Alert)(i)
}
fn resp_text_code_badcharset(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(
tag_no_case(b"BADCHARSET"),
opt(preceded(
tag(b" "),
parenthesized_nonempty_list(map(astring_utf8, Cow::Borrowed)),
)),
),
ResponseCode::BadCharset,
)(i)
}
fn resp_text_code_capability(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(capability_data, ResponseCode::Capabilities)(i)
}
fn resp_text_code_parse(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"PARSE"), |_| ResponseCode::Parse)(i)
}
fn resp_text_code_permanent_flags(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(
tag_no_case(b"PERMANENTFLAGS "),
parenthesized_list(map(flag_perm, Cow::Borrowed)),
),
ResponseCode::PermanentFlags,
)(i)
}
fn resp_text_code_read_only(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"READ-ONLY"), |_| ResponseCode::ReadOnly)(i)
}
fn resp_text_code_read_write(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"READ-WRITE"), |_| ResponseCode::ReadWrite)(i)
}
fn resp_text_code_try_create(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"TRYCREATE"), |_| ResponseCode::TryCreate)(i)
}
fn resp_text_code_uid_validity(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(tag_no_case(b"UIDVALIDITY "), number),
ResponseCode::UidValidity,
)(i)
}
fn resp_text_code_uid_next(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(tag_no_case(b"UIDNEXT "), number),
ResponseCode::UidNext,
)(i)
}
fn resp_text_code_unseen(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(tag_no_case(b"UNSEEN "), number),
ResponseCode::Unseen,
)(i)
}
fn resp_text_code(i: &[u8]) -> IResult<&[u8], ResponseCode> {
// Per the spec, the closing tag should be "] ".
// See `resp_text` for more on why this is done differently.
delimited(
tag(b"["),
alt((
resp_text_code_alert,
resp_text_code_badcharset,
resp_text_code_capability,
resp_text_code_parse,
resp_text_code_permanent_flags,
resp_text_code_uid_validity,
resp_text_code_uid_next,
resp_text_code_unseen,
resp_text_code_read_only,
resp_text_code_read_write,
resp_text_code_try_create,
rfc4551::resp_text_code_highest_mod_seq,
rfc4315::resp_text_code_append_uid,
rfc4315::resp_text_code_copy_uid,
rfc4315::resp_text_code_uid_not_sticky,
rfc5464::resp_text_code_metadata_long_entries,
rfc5464::resp_text_code_metadata_max_size,
rfc5464::resp_text_code_metadata_too_many,
rfc5464::resp_text_code_metadata_no_private,
)),
tag(b"]"),
)(i)
}
fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
alt((
map(tag_no_case(b"IMAP4rev1"), |_| Capability::Imap4rev1),
map(
map(preceded(tag_no_case(b"AUTH="), atom), Cow::Borrowed),
Capability::Auth,
),
map(map(atom, Cow::Borrowed), Capability::Atom),
))(i)
}
fn ensure_capabilities_contains_imap4rev(
capabilities: Vec<Capability<'_>>,
) -> Result<Vec<Capability<'_>>, ()> {
if capabilities.contains(&Capability::Imap4rev1) {
Ok(capabilities)
} else {
Err(())
}
}
fn capability_data(i: &[u8]) -> IResult<&[u8], Vec<Capability>> {
map_res(
preceded(
tag_no_case(b"CAPABILITY"),
many0(preceded(char(' '), capability)),
),
ensure_capabilities_contains_imap4rev,
)(i)
}
fn mailbox_data_search(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
// Technically, trailing whitespace is not allowed here, but multiple
// email servers in the wild seem to have it anyway (see #34, #108).
terminated(
preceded(tag_no_case(b"SEARCH"), many0(preceded(tag(" "), number))),
opt(tag(" ")),
),
MailboxDatum::Search,
)(i)
}
fn mailbox_data_flags(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
preceded(tag_no_case("FLAGS "), flag_list),
MailboxDatum::Flags,
)(i)
}
fn mailbox_data_exists(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
terminated(number, tag_no_case(" EXISTS")),
MailboxDatum::Exists,
)(i)
}
#[allow(clippy::type_complexity)]
fn mailbox_list(i: &[u8]) -> IResult<&[u8], (Vec<Cow<str>>, Option<&str>, &str)> {
map(
tuple((
flag_list,
tag(b" "),
alt((map(quoted_utf8, Some), map(nil, |_| None))),
tag(b" "),
mailbox,
)),
|(flags, _, delimiter, _, name)| (flags, delimiter, name),
)(i)
}
fn mailbox_data_list(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(preceded(tag_no_case("LIST "), mailbox_list), |data| {
MailboxDatum::List {
flags: data.0,
delimiter: data.1.map(Cow::Borrowed),
name: Cow::Borrowed(data.2),
}
})(i)
}
fn mailbox_data_lsub(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(preceded(tag_no_case("LSUB "), mailbox_list), |data| {
MailboxDatum::List {
flags: data.0,
delimiter: data.1.map(Cow::Borrowed),
name: Cow::Borrowed(data.2),
}
})(i)
}
// Unlike `status_att` in the RFC syntax, this includes the value,
// so that it can return a valid enum object instead of just a key.
fn status_att(i: &[u8]) -> IResult<&[u8], StatusAttribute> {
alt((
rfc4551::status_att_val_highest_mod_seq,
map(
preceded(tag_no_case("MESSAGES "), number),
StatusAttribute::Messages,
),
map(
preceded(tag_no_case("RECENT "), number),
StatusAttribute::Recent,
),
map(
preceded(tag_no_case("UIDNEXT "), number),
StatusAttribute::UidNext,
),
map(
preceded(tag_no_case("UIDVALIDITY "), number),
StatusAttribute::UidValidity,
),
map(
preceded(tag_no_case("UNSEEN "), number),
StatusAttribute::Unseen,
),
))(i)
}
fn status_att_list(i: &[u8]) -> IResult<&[u8], Vec<StatusAttribute>> {
parenthesized_nonempty_list(status_att)(i)
}
fn mailbox_data_status(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
tuple((tag_no_case("STATUS "), mailbox, tag(" "), status_att_list)),
|(_, mailbox, _, status)| MailboxDatum::Status {
mailbox: Cow::Borrowed(mailbox),
status,
},
)(i)
}
fn mailbox_data_recent(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
terminated(number, tag_no_case(" RECENT")),
MailboxDatum::Recent,
)(i)
}
fn mailbox_data(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
alt((
mailbox_data_flags,
mailbox_data_exists,
mailbox_data_list,
mailbox_data_lsub,
mailbox_data_status,
mailbox_data_recent,
mailbox_data_search,
rfc5256::mailbox_data_sort,
))(i)
}
// An address structure is a parenthesized list that describes an
// electronic mail address.
fn address(i: &[u8]) -> IResult<&[u8], Address> {
paren_delimited(map(
tuple((
nstring,
tag(" "),
nstring,
tag(" "),
nstring,
tag(" "),
nstring,
)),
|(name, _, adl, _, mailbox, _, host)| Address {
name: name.map(Cow::Borrowed),
adl: adl.map(Cow::Borrowed),
mailbox: mailbox.map(Cow::Borrowed),
host: host.map(Cow::Borrowed),
},
))(i)
}
fn opt_addresses(i: &[u8]) -> IResult<&[u8], Option<Vec<Address>>> {
alt((
map(nil, |_s| None),
map(
paren_delimited(many1(terminated(address, opt(char(' '))))),
Some,
),
))(i)
}
// envelope = "(" env-date SP env-subject SP env-from SP
// env-sender SP env-reply-to SP env-to SP env-cc SP
// env-bcc SP env-in-reply-to SP env-message-id ")"
//
// env-bcc = "(" 1*address ")" / nil
//
// env-cc = "(" 1*address ")" / nil
//
// env-date = nstring
//
// env-from = "(" 1*address ")" / nil
//
// env-in-reply-to = nstring
//
// env-message-id = nstring
//
// env-reply-to = "(" 1*address ")" / nil
//
// env-sender = "(" 1*address ")" / nil
//
// env-subject = nstring
//
// env-to = "(" 1*address ")" / nil
pub(crate) fn envelope(i: &[u8]) -> IResult<&[u8], Envelope> {
paren_delimited(map(
tuple((
nstring,
tag(" "),
nstring,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
nstring,
tag(" "),
nstring,
)),
|(
date,
_,
subject,
_,
from,
_,
sender,
_,
reply_to,
_,
to,
_,
cc,
_,
bcc,
_,
in_reply_to,
_,
message_id,
)| Envelope {
date: date.map(Cow::Borrowed),
subject: subject.map(Cow::Borrowed),
from,
sender,
reply_to,
to,
cc,
bcc,
in_reply_to: in_reply_to.map(Cow::Borrowed),
message_id: message_id.map(Cow::Borrowed),
},
))(i)
}
fn msg_att_envelope(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(preceded(tag_no_case("ENVELOPE "), envelope), |envelope| {
AttributeValue::Envelope(Box::new(envelope))
})(i)
}
fn msg_att_internal_date(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(
preceded(tag_no_case("INTERNALDATE "), nstring_utf8),
|date| AttributeValue::InternalDate(Cow::Borrowed(date.unwrap())),
)(i)
}
fn msg_att_flags(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(
preceded(tag_no_case("FLAGS "), flag_list),
AttributeValue::Flags,
)(i)
}
fn msg_att_rfc822(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(preceded(tag_no_case("RFC822 "), nstring), |v| {
AttributeValue::Rfc822(v.map(Cow::Borrowed))
})(i)
}
fn msg_att_rfc822_header(i: &[u8]) -> IResult<&[u8], AttributeValue> {
// extra space workaround for DavMail
map(
tuple((tag_no_case("RFC822.HEADER "), opt(tag(b" ")), nstring)),
|(_, _, raw)| AttributeValue::Rfc822Header(raw.map(Cow::Borrowed)),
)(i)
}
fn msg_att_rfc822_size(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(
preceded(tag_no_case("RFC822.SIZE "), number),
AttributeValue::Rfc822Size,
)(i)
}
fn msg_att_rfc822_text(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(preceded(tag_no_case("RFC822.TEXT "), nstring), |v| {
AttributeValue::Rfc822Text(v.map(Cow::Borrowed))
})(i)
}
fn msg_att_uid(i: &[u8]) -> IResult<&[u8], AttributeValue> {
map(preceded(tag_no_case("UID "), number), AttributeValue::Uid)(i)
}
// msg-att = "(" (msg-att-dynamic / msg-att-static)
// *(SP (msg-att-dynamic / msg-att-static)) ")"
//
// msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
// ; MAY change for a message
//
// msg-att-static = "ENVELOPE" SP envelope / "INTERNALDATE" SP date-time /
// "RFC822" [".HEADER" / ".TEXT"] SP nstring /
// "RFC822.SIZE" SP number /
// "BODY" ["STRUCTURE"] SP body /
// "BODY" section ["<" number ">"] SP nstring /
// "UID" SP uniqueid
// ; MUST NOT change for a message
fn msg_att(i: &[u8]) -> IResult<&[u8], AttributeValue> {
alt((
msg_att_body_section,
msg_att_body_structure,
msg_att_envelope,
msg_att_internal_date,
msg_att_flags,
rfc4551::msg_att_mod_seq,
msg_att_rfc822,
msg_att_rfc822_header,
msg_att_rfc822_size,
msg_att_rfc822_text,
msg_att_uid,
))(i)
}
fn msg_att_list(i: &[u8]) -> IResult<&[u8], Vec<AttributeValue>> {
parenthesized_nonempty_list(msg_att)(i)
}
// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
fn message_data_fetch(i: &[u8]) -> IResult<&[u8], Response> {
map(
tuple((number, tag_no_case(" FETCH "), msg_att_list)),
|(num, _, attrs)| Response::Fetch(num, attrs),
)(i)
}
// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
fn message_data_expunge(i: &[u8]) -> IResult<&[u8], u32> {
terminated(number, tag_no_case(" EXPUNGE"))(i)
}
// tag = 1*<any ASTRING-CHAR except "+">
fn imap_tag(i: &[u8]) -> IResult<&[u8], RequestId> {
map(map_res(take_while1(is_tag_char), from_utf8), |s| {
RequestId(s.to_string())
})(i)
}
// This is not quite according to spec, which mandates the following:
// ["[" resp-text-code "]" SP] text
// However, examples in RFC 4551 (Conditional STORE) counteract this by giving
// examples of `resp-text` that do not include the trailing space and text.
fn resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
map(tuple((opt(resp_text_code), text)), |(code, text)| {
let res = if text.is_empty() {
None
} else if code.is_some() {
Some(&text[1..])
} else {
Some(text)
};
(code, res)
})(i)
}
// an response-text if it is at the end of a response. Empty text is then allowed without the normally needed trailing space.
fn trailing_resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
map(opt(tuple((tag(b" "), resp_text))), |resptext| {
resptext.map(|(_, tuple)| tuple).unwrap_or((None, None))
})(i)
}
// continue-req = "+" SP (resp-text / base64) CRLF
pub(crate) fn continue_req(i: &[u8]) -> IResult<&[u8], Response> {
// Some servers do not send the space :/
// TODO: base64
map(
tuple((tag("+"), opt(tag(" ")), resp_text, tag("\r\n"))),
|(_, _, text, _)| Response::Continue {
code: text.0,
information: text.1.map(Cow::Borrowed),
},
)(i)
}
// response-tagged = tag SP resp-cond-state CRLF
//
// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
// ; Status condition
pub(crate) fn response_tagged(i: &[u8]) -> IResult<&[u8], Response> {
map(
tuple((
imap_tag,
tag(b" "),
status,
trailing_resp_text,
tag(b"\r\n"),
)),
|(tag, _, status, text, _)| Response::Done {
tag,
status,
code: text.0,
information: text.1.map(Cow::Borrowed),
},
)(i)
}
// resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
// ; Authentication condition
//
// resp-cond-bye = "BYE" SP resp-text
//
// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
// ; Status condition
fn resp_cond(i: &[u8]) -> IResult<&[u8], Response> {
map(tuple((status, trailing_resp_text)), |(status, text)| {
Response::Data {
status,
code: text.0,
information: text.1.map(Cow::Borrowed),
}
})(i)
}
// response-data = "*" SP (resp-cond-state / resp-cond-bye /
// mailbox-data / message-data / capability-data / quota) CRLF
pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response> {
delimited(
tag(b"* "),
alt((
resp_cond,
map(mailbox_data, Response::MailboxData),
map(message_data_expunge, Response::Expunge),
message_data_fetch,
map(capability_data, Response::Capabilities),
rfc5161::resp_enabled,
rfc5464::metadata_solicited,
rfc5464::metadata_unsolicited,
rfc7162::resp_vanished,
rfc2087::quota,
rfc2087::quota_root,
)),
tag(b"\r\n"),
)(i)
}
#[cfg(test)]
mod tests {
use crate::types::*;
use assert_matches::assert_matches;
use std::borrow::Cow;
#[test]
fn test_list() {
match super::mailbox(b"iNboX ") {
Ok((_, mb)) => {
assert_eq!(mb, "INBOX");
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_envelope() {
let env = br#"ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") "#;
match super::msg_att_envelope(env) {
Ok((_, AttributeValue::Envelope(_))) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_opt_addresses() {
let addr = b"((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) ";
match super::opt_addresses(addr) {
Ok((_, _addresses)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_opt_addresses_no_space() {
let addr =
br#"((NIL NIL "test" "example@example.com")(NIL NIL "test" "example@example.com"))"#;
match super::opt_addresses(addr) {
Ok((_, _addresses)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_addresses() {
match super::address(b"(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\") ") {
Ok((_, _address)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
// Literal non-UTF8 address
match super::address(b"({12}\r\nJoh\xff Klensin NIL \"KLENSIN\" \"MIT.EDU\") ") {
Ok((_, _address)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_capability_data() {
// Minimal capabilities
assert_matches!(
super::capability_data(b"CAPABILITY IMAP4rev1\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![Capability::Imap4rev1])
}
);
assert_matches!(
super::capability_data(b"CAPABILITY XPIG-LATIN IMAP4rev1 STARTTLS AUTH=GSSAPI\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![
Capability::Atom(Cow::Borrowed("XPIG-LATIN")),
Capability::Imap4rev1,
Capability::Atom(Cow::Borrowed("STARTTLS")),
Capability::Auth(Cow::Borrowed("GSSAPI")),
])
}
);
assert_matches!(
super::capability_data(b"CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![
Capability::Imap4rev1,
Capability::Auth(Cow::Borrowed("GSSAPI")),
Capability::Auth(Cow::Borrowed("PLAIN")),
])
}
);
// Capability command must contain IMAP4rev1
assert_matches!(
super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
Err(_)
);
}
}

95
src/parser/rfc4315.rs Normal file
View file

@ -0,0 +1,95 @@
//!
//! https://tools.ietf.org/html/rfc4315
//!
//! The IMAP UIDPLUS Extension
//!
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::map,
multi::separated_list1,
sequence::{preceded, tuple},
IResult,
};
use crate::parser::core::number;
use crate::types::*;
/// Extends resp-text-code as follows:
///
/// ```ignore
/// resp-text-code =/ resp-code-apnd
/// resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
/// append-uid =/ uid-set
/// ; only permitted if client uses [MULTIAPPEND]
/// ; to append multiple messages.
/// ```
///
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
pub(crate) fn resp_text_code_append_uid(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(
tag_no_case(b"APPENDUID "),
tuple((number, tag(" "), uid_set)),
),
|(fst, _, snd)| ResponseCode::AppendUid(fst, snd),
)(i)
}
/// Extends resp-text-code as follows:
///
/// ```ignore
/// resp-text-code =/ resp-code-copy
/// resp-code-copy = "COPYUID" SP nz-number SP uid-set
/// ```
///
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
pub(crate) fn resp_text_code_copy_uid(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(
preceded(
tag_no_case(b"COPYUID "),
tuple((number, tag(" "), uid_set, tag(" "), uid_set)),
),
|(fst, _, snd, _, trd)| ResponseCode::CopyUid(fst, snd, trd),
)(i)
}
/// Extends resp-text-code as follows:
///
/// ```ignore
/// resp-text-code =/ "UIDNOTSTICKY"
/// ```
///
/// [RFC4315 - 3 Additional Response Codes](https://tools.ietf.org/html/rfc4315#section-3)
pub(crate) fn resp_text_code_uid_not_sticky(i: &[u8]) -> IResult<&[u8], ResponseCode> {
map(tag_no_case(b"UIDNOTSTICKY"), |_| ResponseCode::UidNotSticky)(i)
}
/// Parses the uid-set nonterminal:
///
/// ```ignore
/// uid-set = (uniqueid / uid-range) *("," uid-set)
/// ```
///
/// [RFC4315 - 4 Formal Syntax](https://tools.ietf.org/html/rfc4315#section-4)
fn uid_set(i: &[u8]) -> IResult<&[u8], Vec<UidSetMember>> {
separated_list1(tag(","), alt((uid_range, map(number, From::from))))(i)
}
/// Parses the uid-set nonterminal:
///
/// ```ignore
/// uid-range = (uniqueid ":" uniqueid)
/// ; two uniqueid values and all values
/// ; between these two regards of order.
/// ; Example: 2:4 and 4:2 are equivalent.
/// ```
///
/// [RFC4315 - 4 Formal Syntax](https://tools.ietf.org/html/rfc4315#section-4)
fn uid_range(i: &[u8]) -> IResult<&[u8], UidSetMember> {
map(
nom::sequence::separated_pair(number, tag(":"), number),
|(fst, snd)| if fst <= snd { fst..=snd } else { snd..=fst }.into(),
)(i)
}

36
src/parser/rfc4551.rs Normal file
View file

@ -0,0 +1,36 @@
//!
//! https://tools.ietf.org/html/rfc4551
//!
//! IMAP Extension for Conditional STORE Operation
//! or Quick Flag Changes Resynchronization
//!
use nom::{bytes::streaming::tag_no_case, sequence::tuple, IResult};
use crate::{
parser::core::{number_64, paren_delimited},
types::*,
};
// The highest mod-sequence value of all messages in the mailbox.
// Extends resp-test-code defined in rfc3501.
// [RFC4551 - 3.6 HIGHESTMODSEQ Status Data Items](https://tools.ietf.org/html/rfc4551#section-3.6)
// [RFC4551 - 4. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc4551#section-4)
pub(crate) fn resp_text_code_highest_mod_seq(i: &[u8]) -> IResult<&[u8], ResponseCode> {
let (i, (_, num)) = tuple((tag_no_case("HIGHESTMODSEQ "), number_64))(i)?;
Ok((i, ResponseCode::HighestModSeq(num)))
}
// Extends status-att/status-att-list defined in rfc3501
// [RFC4551 - 3.6 - HIGHESTMODSEQ Status Data Items](https://tools.ietf.org/html/rfc4551#section-3.6)
// [RFC4551 - 4. Formal Syntax - status-att-val](https://tools.ietf.org/html/rfc4551#section-4)
pub(crate) fn status_att_val_highest_mod_seq(i: &[u8]) -> IResult<&[u8], StatusAttribute> {
let (i, (_, num)) = tuple((tag_no_case("HIGHESTMODSEQ "), number_64))(i)?;
Ok((i, StatusAttribute::HighestModSeq(num)))
}
// [RFC4551 - 4. Formal Syntax - fetch-mod-resp](https://tools.ietf.org/html/rfc4551#section-4)
pub(crate) fn msg_att_mod_seq(i: &[u8]) -> IResult<&[u8], AttributeValue> {
let (i, (_, num)) = tuple((tag_no_case("MODSEQ "), paren_delimited(number_64)))(i)?;
Ok((i, AttributeValue::ModSeq(num)))
}

37
src/parser/rfc5161.rs Normal file
View file

@ -0,0 +1,37 @@
//!
//! https://tools.ietf.org/html/rfc5161
//!
//! The IMAP ENABLE Extension
//!
use nom::{
bytes::streaming::tag_no_case,
character::streaming::char,
combinator::map,
multi::many0,
sequence::{preceded, tuple},
IResult,
};
use std::borrow::Cow;
use crate::parser::core::atom;
use crate::types::*;
// The ENABLED response lists capabilities that were enabled in response
// to a ENABLE command.
// [RFC5161 - 3.2 The ENABLED Response](https://tools.ietf.org/html/rfc5161#section-3.2)
pub(crate) fn resp_enabled(i: &[u8]) -> IResult<&[u8], Response> {
map(enabled_data, Response::Capabilities)(i)
}
fn enabled_data(i: &[u8]) -> IResult<&[u8], Vec<Capability>> {
let (i, (_, capabilities)) = tuple((
tag_no_case("ENABLED"),
many0(preceded(char(' '), capability)),
))(i)?;
Ok((i, capabilities))
}
fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
map(map(atom, Cow::Borrowed), Capability::Atom)(i)
}

46
src/parser/rfc5256.rs Normal file
View file

@ -0,0 +1,46 @@
//!
//! https://tools.ietf.org/html/rfc5256
//!
//! SORT extension
//!
use nom::{
bytes::streaming::{tag, tag_no_case},
combinator::{map, opt},
multi::many0,
sequence::{preceded, terminated},
IResult,
};
use crate::{parser::core::number, types::MailboxDatum};
/// BASE.7.2.SORT. SORT Response
///
/// Data: zero or more numbers
///
/// The SORT response occurs as a result of a SORT or UID SORT
/// command. The number(s) refer to those messages that match the
/// search criteria. For SORT, these are message sequence numbers;
/// for UID SORT, these are unique identifiers. Each number is
/// delimited by a space.
///
/// Example:
///
/// ```ignore
/// S: * SORT 2 3 6
/// ```
///
/// [RFC5256 - 4 Additional Responses](https://tools.ietf.org/html/rfc5256#section-4)
pub(crate) fn mailbox_data_sort(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
map(
// Technically, trailing whitespace is not allowed for the SEARCH command,
// but multiple email servers in the wild seem to have it anyway (see #34, #108).
// Since the SORT command extends the SEARCH command, the trailing whitespace
// is exceptionnaly allowed here (as for the SEARCH command).
terminated(
preceded(tag_no_case(b"SORT"), many0(preceded(tag(" "), number))),
opt(tag(" ")),
),
MailboxDatum::Sort,
)(i)
}

386
src/parser/rfc5464.rs Normal file
View file

@ -0,0 +1,386 @@
//!
//! https://tools.ietf.org/html/rfc5464
//!
//! IMAP METADATA extension
//!
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::{map, map_opt},
multi::separated_list0,
sequence::tuple,
IResult,
};
use std::borrow::Cow;
use crate::{parser::core::*, types::*};
fn is_entry_component_char(c: u8) -> bool {
c < 0x80 && c > 0x19 && c != b'*' && c != b'%' && c != b'/'
}
enum EntryParseStage<'a> {
PrivateShared(usize),
Admin(usize),
VendorComment(usize),
Path(usize),
Done(usize),
Fail(nom::Err<&'a [u8]>),
}
fn check_private_shared(i: &[u8]) -> EntryParseStage {
if i.starts_with(b"/private") {
EntryParseStage::VendorComment(8)
} else if i.starts_with(b"/shared") {
EntryParseStage::Admin(7)
} else {
EntryParseStage::Fail(nom::Err::Error(
b"Entry Name doesn't start with /private or /shared",
))
}
}
fn check_admin(i: &[u8], l: usize) -> EntryParseStage {
if i[l..].starts_with(b"/admin") {
EntryParseStage::Path(l + 6)
} else {
EntryParseStage::VendorComment(l)
}
}
fn check_vendor_comment(i: &[u8], l: usize) -> EntryParseStage {
if i[l..].starts_with(b"/comment") {
EntryParseStage::Path(l + 8)
} else if i[l..].starts_with(b"/vendor") {
//make sure vendor name is present
if i.len() < l + 9 || i[l + 7] != b'/' || !is_entry_component_char(i[l + 8]) {
EntryParseStage::Fail(nom::Err::Incomplete(nom::Needed::Unknown))
} else {
EntryParseStage::Path(l + 7)
}
} else {
EntryParseStage::Fail(nom::Err::Error(
b"Entry name is not continued with /admin, /vendor or /comment",
))
}
}
fn check_path(i: &[u8], l: usize) -> EntryParseStage {
if i.len() == l || i[l] == b' ' || i[l] == b'\r' {
return EntryParseStage::Done(l);
} else if i[l] != b'/' {
return EntryParseStage::Fail(nom::Err::Error(b"Entry name path is corrupted"));
}
for j in 1..(i.len() - l) {
if !is_entry_component_char(i[l + j]) {
return EntryParseStage::Path(l + j);
}
}
EntryParseStage::Done(i.len())
}
fn check_entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
let mut stage = EntryParseStage::PrivateShared(0);
loop {
match stage {
EntryParseStage::PrivateShared(_) => {
stage = check_private_shared(i);
}
EntryParseStage::Admin(l) => {
stage = check_admin(i, l);
}
EntryParseStage::VendorComment(l) => {
stage = check_vendor_comment(i, l);
}
EntryParseStage::Path(l) => {
stage = check_path(i, l);
}
EntryParseStage::Done(l) => {
return Ok((&i[l..], &i[..l]));
}
EntryParseStage::Fail(nom::Err::Error(err_msg)) => {
return std::result::Result::Err(nom::Err::Error(nom::error::Error::new(
err_msg,
nom::error::ErrorKind::Verify,
)));
}
EntryParseStage::Fail(nom::Err::Incomplete(reason)) => {
return std::result::Result::Err(nom::Err::Incomplete(reason));
}
_ => panic!("Entry name verification failure"),
}
}
}
fn entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
let astring_res = astring(i)?;
check_entry_name(astring_res.1)?;
Ok(astring_res)
}
fn slice_to_str(i: &[u8]) -> &str {
std::str::from_utf8(i).unwrap()
}
fn nil_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
map_opt(tag_no_case("NIL"), |_| None)(i)
}
fn string_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
map(alt((quoted, literal)), |s| {
Some(slice_to_str(s).to_string())
})(i)
}
fn keyval_list(i: &[u8]) -> IResult<&[u8], Vec<Metadata>> {
parenthesized_nonempty_list(map(
tuple((
map(entry_name, slice_to_str),
tag(" "),
alt((nil_value, string_value)),
)),
|(key, _, value)| Metadata {
entry: key.to_string(),
value,
},
))(i)
}
fn entry_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<str>>> {
separated_list0(tag(" "), map(map(entry_name, slice_to_str), Cow::Borrowed))(i)
}
fn metadata_common(i: &[u8]) -> IResult<&[u8], &[u8]> {
let (i, (_, mbox, _)) = tuple((tag_no_case("METADATA "), quoted, tag(" ")))(i)?;
Ok((i, mbox))
}
// [RFC5464 - 4.4.1 METADATA Response with values]
pub(crate) fn metadata_solicited(i: &[u8]) -> IResult<&[u8], Response> {
let (i, (mailbox, values)) = tuple((metadata_common, keyval_list))(i)?;
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataSolicited {
mailbox: Cow::Borrowed(slice_to_str(mailbox)),
values,
}),
))
}
// [RFC5464 - 4.4.2 Unsolicited METADATA Response without values]
pub(crate) fn metadata_unsolicited(i: &[u8]) -> IResult<&[u8], Response> {
let (i, (mailbox, values)) = tuple((metadata_common, entry_list))(i)?;
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataUnsolicited {
mailbox: Cow::Borrowed(slice_to_str(mailbox)),
values,
}),
))
}
// There are any entries with values larger than the MAXSIZE limit given to GETMETADATA.
// Extends resp-test-code defined in rfc3501.
// [RFC5464 - 4.2.1 MAXSIZE GETMETADATA Command Option](https://tools.ietf.org/html/rfc5464#section-4.2.1)
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
pub(crate) fn resp_text_code_metadata_long_entries(i: &[u8]) -> IResult<&[u8], ResponseCode> {
let (i, (_, num)) = tuple((tag_no_case("METADATA LONGENTRIES "), number_64))(i)?;
Ok((i, ResponseCode::MetadataLongEntries(num)))
}
// Server is unable to set an annotation because the size of its value is too large.
// Extends resp-test-code defined in rfc3501.
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
pub(crate) fn resp_text_code_metadata_max_size(i: &[u8]) -> IResult<&[u8], ResponseCode> {
let (i, (_, num)) = tuple((tag_no_case("METADATA MAXSIZE "), number_64))(i)?;
Ok((i, ResponseCode::MetadataMaxSize(num)))
}
// Server is unable to set a new annotation because the maximum number of allowed annotations has already been reached.
// Extends resp-test-code defined in rfc3501.
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
pub(crate) fn resp_text_code_metadata_too_many(i: &[u8]) -> IResult<&[u8], ResponseCode> {
let (i, _) = tag_no_case("METADATA TOOMANY")(i)?;
Ok((i, ResponseCode::MetadataTooMany))
}
// Server is unable to set a new annotation because it does not support private annotations on one of the specified mailboxes.
// Extends resp-test-code defined in rfc3501.
// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
pub(crate) fn resp_text_code_metadata_no_private(i: &[u8]) -> IResult<&[u8], ResponseCode> {
let (i, _) = tag_no_case("METADATA NOPRIVATE")(i)?;
Ok((i, ResponseCode::MetadataNoPrivate))
}
#[cfg(test)]
mod tests {
use super::{metadata_solicited, metadata_unsolicited};
use crate::types::*;
use std::borrow::Cow;
#[test]
fn test_solicited_fail_1() {
match metadata_solicited(b"METADATA \"\" (/asdfg \"asdf\")\r\n") {
Err(_) => {}
_ => panic!("Error required when entry name is not starting with /private or /shared"),
}
}
#[test]
fn test_solicited_fail_2() {
match metadata_solicited(b"METADATA \"\" (/shared/asdfg \"asdf\")\r\n") {
Err(_) => {}
_ => panic!(
"Error required when in entry name /shared \
is not continued with /admin, /comment or /vendor"
),
}
}
#[test]
fn test_solicited_fail_3() {
match metadata_solicited(b"METADATA \"\" (/private/admin \"asdf\")\r\n") {
Err(_) => {}
_ => panic!(
"Error required when in entry name /private \
is not continued with /comment or /vendor"
),
}
}
#[test]
fn test_solicited_fail_4() {
match metadata_solicited(b"METADATA \"\" (/shared/vendor \"asdf\")\r\n") {
Err(_) => {}
_ => panic!("Error required when vendor name is not provided."),
}
}
#[test]
fn test_solicited_success() {
match metadata_solicited(
b"METADATA \"mbox\" (/shared/vendor/vendorname \"asdf\" \
/private/comment/a \"bbb\")\r\n",
) {
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
assert_eq!(mailbox, "mbox");
assert_eq!(i, b"\r\n");
assert_eq!(values.len(), 2);
assert_eq!(values[0].entry, "/shared/vendor/vendorname");
assert_eq!(
values[0]
.value
.as_ref()
.expect("None value is not expected"),
"asdf"
);
assert_eq!(values[1].entry, "/private/comment/a");
assert_eq!(
values[1]
.value
.as_ref()
.expect("None value is not expected"),
"bbb"
);
}
_ => panic!("Correct METADATA response is not parsed properly."),
}
}
#[test]
fn test_literal_success() {
// match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a \"AAA\")\r\n")
match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a {3}\r\nAAA)\r\n") {
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
assert_eq!(mailbox, "");
assert_eq!(i, b"\r\n");
assert_eq!(values.len(), 1);
assert_eq!(values[0].entry, "/shared/vendor/vendor.coi/a");
assert_eq!(
values[0]
.value
.as_ref()
.expect("None value is not expected"),
"AAA"
);
}
Err(e) => panic!("ERR: {:?}", e),
_ => panic!("Strange failure"),
}
}
#[test]
fn test_unsolicited_success() {
match metadata_unsolicited(b"METADATA \"theBox\" /shared/admin/qwe /private/comment/a\r\n")
{
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }),
)) => {
assert_eq!(i, b"\r\n");
assert_eq!(mailbox, "theBox");
assert_eq!(values.len(), 2);
assert_eq!(values[0], "/shared/admin/qwe");
assert_eq!(values[1], "/private/comment/a");
}
_ => panic!("Correct METADATA response is not parsed properly."),
}
}
#[test]
fn test_response_codes() {
use crate::parser::parse_response;
match parse_response(b"* OK [METADATA LONGENTRIES 123] Some entries omitted.\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::MetadataLongEntries(123)),
information: Some(Cow::Borrowed("Some entries omitted.")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [METADATA MAXSIZE 123] Annotation too large.\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::MetadataMaxSize(123)),
information: Some(Cow::Borrowed("Annotation too large.")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [METADATA TOOMANY] Too many annotations.\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::MetadataTooMany),
information: Some(Cow::Borrowed("Too many annotations.")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [METADATA NOPRIVATE] Private annotations not supported.\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::MetadataNoPrivate),
information: Some(Cow::Borrowed("Private annotations not supported.")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
}

36
src/parser/rfc7162.rs Normal file
View file

@ -0,0 +1,36 @@
//!
//!
//! https://tools.ietf.org/html/rfc7162
//!
//! The IMAP QRESYNC Extensions
//!
use nom::{
bytes::streaming::tag_no_case, character::streaming::space1, combinator::opt, sequence::tuple,
IResult,
};
use crate::parser::core::sequence_set;
use crate::types::*;
// The VANISHED response reports that the specified UIDs have been
// permanently removed from the mailbox. This response is similar to
// the EXPUNGE response (RFC3501); however, it can return information
// about multiple messages, and it returns UIDs instead of message
// numbers.
// [RFC7162 - VANISHED RESPONSE](https://tools.ietf.org/html/rfc7162#section-3.2.10)
pub(crate) fn resp_vanished(i: &[u8]) -> IResult<&[u8], Response> {
let (rest, (_, earlier, _, uids)) = tuple((
tag_no_case("VANISHED"),
opt(tuple((space1, tag_no_case("(EARLIER)")))),
space1,
sequence_set,
))(i)?;
Ok((
rest,
Response::Vanished {
earlier: earlier.is_some(),
uids,
},
))
}

568
src/parser/tests.rs Normal file
View file

@ -0,0 +1,568 @@
use super::{bodystructure::BodyStructParser, parse_response};
use crate::types::*;
use std::borrow::Cow;
use std::num::NonZeroUsize;
#[test]
fn test_mailbox_data_response() {
match parse_response(b"* LIST (\\HasNoChildren) \".\" INBOX.Tests\r\n") {
Ok((_, Response::MailboxData(_))) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_number_overflow() {
match parse_response(b"* 2222222222222222222222222222222222222222222C\r\n") {
Err(_) => {}
_ => panic!("error required for integer overflow"),
}
}
#[test]
fn test_unseen() {
match parse_response(b"* OK [UNSEEN 3] Message 3 is first unseen\r\n").unwrap() {
(
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Unseen(3)),
information: Some(Cow::Borrowed("Message 3 is first unseen")),
},
) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_body_text() {
match parse_response(b"* 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n") {
Ok((_, Response::Fetch(_, attrs))) => {
let body = &attrs[0];
assert_eq!(
body,
&AttributeValue::BodySection {
section: Some(SectionPath::Full(MessageSection::Text)),
index: None,
data: Some(Cow::Borrowed(b"foo")),
},
"body = {:?}",
body
);
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_body_structure() {
const RESPONSE: &[u8] = b"* 15 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"iso-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 1315 42 NIL NIL NIL NIL))\r\n";
match parse_response(RESPONSE) {
Ok((_, Response::Fetch(_, attrs))) => {
let body = &attrs[0];
assert!(
matches!(*body, AttributeValue::BodyStructure(_)),
"body = {:?}",
body
);
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_status() {
match parse_response(b"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n") {
Ok((_, Response::MailboxData(MailboxDatum::Status { mailbox, status }))) => {
assert_eq!(mailbox, "blurdybloop");
assert_eq!(
status,
[
StatusAttribute::Messages(231),
StatusAttribute::UidNext(44292),
]
);
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_notify() {
match parse_response(b"* 3501 EXPUNGE\r\n") {
Ok((_, Response::Expunge(3501))) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* 3501 EXISTS\r\n") {
Ok((_, Response::MailboxData(MailboxDatum::Exists(3501)))) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"+ idling\r\n") {
Ok((
_,
Response::Continue {
code: None,
information: Some(Cow::Borrowed("idling")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_search() {
// also allow trailing whitespace in SEARCH responses
for empty_response in &["* SEARCH\r\n", "* SEARCH \r\n"] {
match parse_response(empty_response.as_bytes()) {
Ok((_, Response::MailboxData(MailboxDatum::Search(ids)))) => {
assert!(ids.is_empty());
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
for response in &["* SEARCH 12345 67890\r\n", "* SEARCH 12345 67890 \r\n"] {
match parse_response(response.as_bytes()) {
Ok((_, Response::MailboxData(MailboxDatum::Search(ids)))) => {
assert_eq!(ids[0], 12345);
assert_eq!(ids[1], 67890);
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
}
#[test]
fn test_sort() {
// also allow trailing whitespace in SEARCH responses
for empty_response in &["* SORT\r\n", "* SORT \r\n"] {
match parse_response(empty_response.as_bytes()) {
Ok((_, Response::MailboxData(MailboxDatum::Sort(ids)))) => {
assert!(ids.is_empty());
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
for response in &["* SORT 12345 67890\r\n", "* SORT 12345 67890 \r\n"] {
match parse_response(response.as_bytes()) {
Ok((_, Response::MailboxData(MailboxDatum::Sort(ids)))) => {
assert_eq!(ids[0], 12345);
assert_eq!(ids[1], 67890);
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
}
#[test]
fn test_uid_fetch() {
match parse_response(b"* 4 FETCH (UID 71372 RFC822.HEADER {10275}\r\n") {
Err(nom::Err::Incomplete(nom::Needed::Size(size))) => {
assert_eq!(size, NonZeroUsize::new(10275).unwrap());
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_uid_fetch_extra_space() {
// DavMail inserts an extra space after RFC822.HEADER
match parse_response(b"* 4 FETCH (UID 71372 RFC822.HEADER {10275}\r\n") {
Err(nom::Err::Incomplete(nom::Needed::Size(size))) => {
assert_eq!(size, NonZeroUsize::new(10275).unwrap());
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_header_fields() {
const RESPONSE: &[u8] = b"* 1 FETCH (UID 1 BODY[HEADER.FIELDS (CHAT-VERSION)] {21}\r\nChat-Version: 1.0\r\n\r\n)\r\n";
match parse_response(RESPONSE) {
Ok((_, Response::Fetch(_, _))) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_response_codes() {
match parse_response(b"* OK [ALERT] Alert!\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Alert),
information: Some(Cow::Borrowed("Alert!")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [PARSE] Something\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::Parse),
information: Some(Cow::Borrowed("Something")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* OK [CAPABILITY IMAP4rev1 IDLE] Logged in\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Capabilities(c)),
information: Some(Cow::Borrowed("Logged in")),
},
)) => {
assert_eq!(c.len(), 2);
assert_eq!(c[0], Capability::Imap4rev1);
assert_eq!(c[1], Capability::Atom(Cow::Borrowed("IDLE")));
}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* OK [CAPABILITY UIDPLUS IMAP4rev1 IDLE] Logged in\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Capabilities(c)),
information: Some(Cow::Borrowed("Logged in")),
},
)) => {
assert_eq!(c.len(), 3);
assert_eq!(c[0], Capability::Atom(Cow::Borrowed("UIDPLUS")));
assert_eq!(c[1], Capability::Imap4rev1);
assert_eq!(c[2], Capability::Atom(Cow::Borrowed("IDLE")));
}
rsp => panic!("unexpected response {:?}", rsp),
}
// Missing IMAP4rev1
match parse_response(b"* OK [CAPABILITY UIDPLUS IDLE] Logged in\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: None,
information: Some(Cow::Borrowed("[CAPABILITY UIDPLUS IDLE] Logged in")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [BADCHARSET] error\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::BadCharset(None)),
information: Some(Cow::Borrowed("error")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [BADCHARSET (utf-8 latin1)] error\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::BadCharset(Some(v))),
information: Some(Cow::Borrowed("error")),
},
)) => {
assert_eq!(v.len(), 2);
assert_eq!(v[0], "utf-8");
assert_eq!(v[1], "latin1");
}
rsp => panic!("unexpected response {:?}", rsp),
}
match parse_response(b"* NO [BADCHARSET ()] error\r\n") {
Ok((
_,
Response::Data {
status: Status::No,
code: None,
information: Some(Cow::Borrowed("[BADCHARSET ()] error")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_incomplete_fetch() {
match parse_response(b"* 4644 FETCH (UID ") {
Err(nom::Err::Incomplete(_)) => {}
rsp => panic!("should be incomplete: {:?}", rsp),
}
}
#[test]
fn test_continuation() {
// regular RFC compliant
match parse_response(b"+ \r\n") {
Ok((
_,
Response::Continue {
code: None,
information: None,
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
// short version, sent by yandex
match parse_response(b"+\r\n") {
Ok((
_,
Response::Continue {
code: None,
information: None,
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_enabled() {
match parse_response(b"* ENABLED QRESYNC X-GOOD-IDEA\r\n") {
Ok((_, capabilities)) => assert_eq!(
capabilities,
Response::Capabilities(vec![
Capability::Atom(Cow::Borrowed("QRESYNC")),
Capability::Atom(Cow::Borrowed("X-GOOD-IDEA")),
])
),
rsp => panic!("Unexpected response: {:?}", rsp),
}
}
#[test]
fn test_flags() {
// Invalid response (FLAGS can't include \*) from Zoho Mail server.
//
// As a workaround, such response is parsed without error.
match parse_response(b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)\r\n") {
Ok((_, capabilities)) => assert_eq!(
capabilities,
Response::MailboxData(MailboxDatum::Flags(vec![
Cow::Borrowed("\\Answered"),
Cow::Borrowed("\\Flagged"),
Cow::Borrowed("\\Deleted"),
Cow::Borrowed("\\Seen"),
Cow::Borrowed("\\Draft"),
Cow::Borrowed("\\*")
]))
),
rsp => panic!("Unexpected response: {:?}", rsp),
}
}
#[test]
fn test_vanished() {
match parse_response(b"* VANISHED (EARLIER) 1,2,3:8\r\n") {
Ok((_, Response::Vanished { earlier, uids })) => {
assert!(earlier);
assert_eq!(uids.len(), 3);
let v = &uids[0];
assert_eq!(*v.start(), 1);
assert_eq!(*v.end(), 1);
let v = &uids[1];
assert_eq!(*v.start(), 2);
assert_eq!(*v.end(), 2);
let v = &uids[2];
assert_eq!(*v.start(), 3);
assert_eq!(*v.end(), 8);
}
rsp => panic!("Unexpected response: {:?}", rsp),
}
match parse_response(b"* VANISHED 1,2,3:8,10\r\n") {
Ok((_, Response::Vanished { earlier, uids })) => {
assert!(!earlier);
assert_eq!(uids.len(), 4);
}
rsp => panic!("Unexpected response: {:?}", rsp),
}
match parse_response(b"* VANISHED (EARLIER) 1\r\n") {
Ok((_, Response::Vanished { earlier, uids })) => {
assert!(earlier);
assert_eq!(uids.len(), 1);
assert_eq!(uids[0].clone().collect::<Vec<u32>>(), vec![1]);
}
rsp => panic!("Unexpected response: {:?}", rsp),
}
match parse_response(b"* VANISHED 1\r\n") {
Ok((_, Response::Vanished { earlier, uids })) => {
assert!(!earlier);
assert_eq!(uids.len(), 1);
}
rsp => panic!("Unexpected response: {:?}", rsp),
}
assert!(parse_response(b"* VANISHED \r\n").is_err());
assert!(parse_response(b"* VANISHED (EARLIER) \r\n").is_err());
}
#[test]
fn test_uidplus() {
match dbg!(parse_response(
b"* OK [APPENDUID 38505 3955] APPEND completed\r\n"
)) {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::AppendUid(38505, uid_set)),
information: Some(Cow::Borrowed("APPEND completed")),
},
)) if uid_set == [3955.into()] => {}
rsp => panic!("Unexpected response: {:?}", rsp),
}
match dbg!(parse_response(
b"* OK [COPYUID 38505 304,319:320 3956:3958] Done\r\n"
)) {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::CopyUid(38505, uid_set_src, uid_set_dst)),
information: Some(Cow::Borrowed("Done")),
},
)) if uid_set_src == [304.into(), (319..=320).into()]
&& uid_set_dst == [(3956..=3958).into()] => {}
rsp => panic!("Unexpected response: {:?}", rsp),
}
match dbg!(parse_response(
b"* NO [UIDNOTSTICKY] Non-persistent UIDs\r\n"
)) {
Ok((
_,
Response::Data {
status: Status::No,
code: Some(ResponseCode::UidNotSticky),
information: Some(Cow::Borrowed("Non-persistent UIDs")),
},
)) => {}
rsp => panic!("Unexpected response: {:?}", rsp),
}
}
#[test]
fn test_imap_body_structure() {
let test = b"\
* 1569 FETCH (\
BODYSTRUCTURE (\
(\
(\
(\
\"TEXT\" \"PLAIN\" \
(\"CHARSET\" \"ISO-8859-1\") NIL NIL \
\"QUOTED-PRINTABLE\" 833 30 NIL NIL NIL\
)\
(\
\"TEXT\" \"HTML\" \
(\"CHARSET\" \"ISO-8859-1\") NIL NIL \
\"QUOTED-PRINTABLE\" 3412 62 NIL \
(\"INLINE\" NIL) NIL\
) \
\"ALTERNATIVE\" (\"BOUNDARY\" \"2__=fgrths\") NIL NIL\
)\
(\
\"IMAGE\" \"GIF\" \
(\"NAME\" \"485039.gif\") \"<2__=lgkfjr>\" NIL \
\"BASE64\" 64 NIL (\"INLINE\" (\"FILENAME\" \"485039.gif\")) \
NIL\
) \
\"RELATED\" (\"BOUNDARY\" \"1__=fgrths\") NIL NIL\
)\
(\
\"APPLICATION\" \"PDF\" \
(\"NAME\" \"title.pdf\") \
\"<1__=lgkfjr>\" NIL \"BASE64\" 333980 NIL \
(\"ATTACHMENT\" (\"FILENAME\" \"title.pdf\")) NIL\
) \
\"MIXED\" (\"BOUNDARY\" \"0__=fgrths\") NIL NIL\
)\
)\r\n";
let (_, resp) = parse_response(test).unwrap();
match resp {
Response::Fetch(_, f) => {
let bodystructure = f
.iter()
.flat_map(|f| match f {
AttributeValue::BodyStructure(e) => Some(e),
_ => None,
})
.next()
.unwrap();
let parser = BodyStructParser::new(bodystructure);
let element = parser.search(|b: &BodyStructure| {
matches!(b, BodyStructure::Basic { ref common, .. } if common.ty.ty == "APPLICATION")
});
assert_eq!(element, Some(vec![2]));
}
_ => panic!("invalid FETCH command test"),
};
}
#[test]
fn test_parsing_of_quota_capability_in_login_response() {
match parse_response(b"* OK [CAPABILITY IMAP4rev1 IDLE QUOTA] Logged in\r\n") {
Ok((
_,
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Capabilities(c)),
information: Some(Cow::Borrowed("Logged in")),
},
)) => {
assert_eq!(c.len(), 3);
assert_eq!(c[0], Capability::Imap4rev1);
assert_eq!(c[1], Capability::Atom(Cow::Borrowed("IDLE")));
assert_eq!(c[2], Capability::Atom(Cow::Borrowed("QUOTA")));
}
rsp => panic!("unexpected response {:?}", rsp),
}
}
#[test]
fn test_parsing_of_bye_response() {
match parse_response(b"* BYE\r\n") {
Ok((
_,
Response::Data {
status: Status::Bye,
code: None,
information: None,
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
};
match parse_response(b"* BYE Autologout; idle for too long\r\n") {
Ok((
_,
Response::Data {
status: Status::Bye,
code: None,
information: Some(Cow::Borrowed("Autologout; idle for too long")),
},
)) => {}
rsp => panic!("unexpected response {:?}", rsp),
};
}

793
src/types.rs Normal file
View file

@ -0,0 +1,793 @@
use std::borrow::Cow;
use std::ops::RangeInclusive;
fn to_owned_cow<'a, T: ?Sized + ToOwned>(c: Cow<'a, T>) -> Cow<'static, T> {
Cow::Owned(c.into_owned())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Request<'a>(pub Cow<'a, [u8]>, pub Cow<'a, [u8]>);
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum AttrMacro {
All,
Fast,
Full,
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Response<'a> {
Capabilities(Vec<Capability<'a>>),
Continue {
code: Option<ResponseCode<'a>>,
information: Option<Cow<'a, str>>,
},
Done {
tag: RequestId,
status: Status,
code: Option<ResponseCode<'a>>,
information: Option<Cow<'a, str>>,
},
Data {
status: Status,
code: Option<ResponseCode<'a>>,
information: Option<Cow<'a, str>>,
},
Expunge(u32),
Vanished {
earlier: bool,
uids: Vec<std::ops::RangeInclusive<u32>>,
},
Fetch(u32, Vec<AttributeValue<'a>>),
MailboxData(MailboxDatum<'a>),
Quota(Quota<'a>),
QuotaRoot(QuotaRoot<'a>),
}
impl<'a> Response<'a> {
pub fn from_bytes(buf: &'a [u8]) -> crate::ParseResult {
crate::parser::parse_response(buf)
}
pub fn into_owned(self) -> Response<'static> {
match self {
Response::Capabilities(capabilities) => Response::Capabilities(
capabilities
.into_iter()
.map(Capability::into_owned)
.collect(),
),
Response::Continue { code, information } => Response::Continue {
code: code.map(ResponseCode::into_owned),
information: information.map(to_owned_cow),
},
Response::Done {
tag,
status,
code,
information,
} => Response::Done {
tag,
status,
code: code.map(ResponseCode::into_owned),
information: information.map(to_owned_cow),
},
Response::Data {
status,
code,
information,
} => Response::Data {
status,
code: code.map(ResponseCode::into_owned),
information: information.map(to_owned_cow),
},
Response::Expunge(seq) => Response::Expunge(seq),
Response::Vanished { earlier, uids } => Response::Vanished { earlier, uids },
Response::Fetch(seq, attrs) => Response::Fetch(
seq,
attrs.into_iter().map(AttributeValue::into_owned).collect(),
),
Response::MailboxData(datum) => Response::MailboxData(datum.into_owned()),
Response::Quota(quota) => Response::Quota(quota.into_owned()),
Response::QuotaRoot(quota_root) => Response::QuotaRoot(quota_root.into_owned()),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum Status {
Ok,
No,
Bad,
PreAuth,
Bye,
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ResponseCode<'a> {
Alert,
BadCharset(Option<Vec<Cow<'a, str>>>),
Capabilities(Vec<Capability<'a>>),
HighestModSeq(u64), // RFC 4551, section 3.1.1
Parse,
PermanentFlags(Vec<Cow<'a, str>>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(u32),
UidValidity(u32),
Unseen(u32),
AppendUid(u32, Vec<UidSetMember>),
CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
UidNotSticky,
MetadataLongEntries(u64), // RFC 5464, section 4.2.1
MetadataMaxSize(u64), // RFC 5464, section 4.3
MetadataTooMany, // RFC 5464, section 4.3
MetadataNoPrivate, // RFC 5464, section 4.3
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum UidSetMember {
UidRange(RangeInclusive<u32>),
Uid(u32),
}
impl From<RangeInclusive<u32>> for UidSetMember {
fn from(x: RangeInclusive<u32>) -> Self {
UidSetMember::UidRange(x)
}
}
impl From<u32> for UidSetMember {
fn from(x: u32) -> Self {
UidSetMember::Uid(x)
}
}
impl<'a> ResponseCode<'a> {
pub fn into_owned(self) -> ResponseCode<'static> {
match self {
ResponseCode::Alert => ResponseCode::Alert,
ResponseCode::BadCharset(v) => {
ResponseCode::BadCharset(v.map(|vs| vs.into_iter().map(to_owned_cow).collect()))
}
ResponseCode::Capabilities(v) => {
ResponseCode::Capabilities(v.into_iter().map(Capability::into_owned).collect())
}
ResponseCode::HighestModSeq(v) => ResponseCode::HighestModSeq(v),
ResponseCode::Parse => ResponseCode::Parse,
ResponseCode::PermanentFlags(v) => {
ResponseCode::PermanentFlags(v.into_iter().map(to_owned_cow).collect())
}
ResponseCode::ReadOnly => ResponseCode::ReadOnly,
ResponseCode::ReadWrite => ResponseCode::ReadWrite,
ResponseCode::TryCreate => ResponseCode::TryCreate,
ResponseCode::UidNext(v) => ResponseCode::UidNext(v),
ResponseCode::UidValidity(v) => ResponseCode::UidValidity(v),
ResponseCode::Unseen(v) => ResponseCode::Unseen(v),
ResponseCode::AppendUid(a, b) => ResponseCode::AppendUid(a, b),
ResponseCode::CopyUid(a, b, c) => ResponseCode::CopyUid(a, b, c),
ResponseCode::UidNotSticky => ResponseCode::UidNotSticky,
ResponseCode::MetadataLongEntries(v) => ResponseCode::MetadataLongEntries(v),
ResponseCode::MetadataMaxSize(v) => ResponseCode::MetadataMaxSize(v),
ResponseCode::MetadataTooMany => ResponseCode::MetadataTooMany,
ResponseCode::MetadataNoPrivate => ResponseCode::MetadataNoPrivate,
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub enum StatusAttribute {
HighestModSeq(u64), // RFC 4551
Messages(u32),
Recent(u32),
UidNext(u32),
UidValidity(u32),
Unseen(u32),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Metadata {
pub entry: String,
pub value: Option<String>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub enum MailboxDatum<'a> {
Exists(u32),
Flags(Vec<Cow<'a, str>>),
List {
flags: Vec<Cow<'a, str>>,
delimiter: Option<Cow<'a, str>>,
name: Cow<'a, str>,
},
Search(Vec<u32>),
Sort(Vec<u32>),
Status {
mailbox: Cow<'a, str>,
status: Vec<StatusAttribute>,
},
Recent(u32),
MetadataSolicited {
mailbox: Cow<'a, str>,
values: Vec<Metadata>,
},
MetadataUnsolicited {
mailbox: Cow<'a, str>,
values: Vec<Cow<'a, str>>,
},
}
impl<'a> MailboxDatum<'a> {
pub fn into_owned(self) -> MailboxDatum<'static> {
match self {
MailboxDatum::Exists(seq) => MailboxDatum::Exists(seq),
MailboxDatum::Flags(flags) => {
MailboxDatum::Flags(flags.into_iter().map(to_owned_cow).collect())
}
MailboxDatum::List {
flags,
delimiter,
name,
} => MailboxDatum::List {
flags: flags.into_iter().map(to_owned_cow).collect(),
delimiter: delimiter.map(to_owned_cow),
name: to_owned_cow(name),
},
MailboxDatum::Search(seqs) => MailboxDatum::Search(seqs),
MailboxDatum::Sort(seqs) => MailboxDatum::Sort(seqs),
MailboxDatum::Status { mailbox, status } => MailboxDatum::Status {
mailbox: to_owned_cow(mailbox),
status,
},
MailboxDatum::Recent(seq) => MailboxDatum::Recent(seq),
MailboxDatum::MetadataSolicited { mailbox, values } => {
MailboxDatum::MetadataSolicited {
mailbox: to_owned_cow(mailbox),
values,
}
}
MailboxDatum::MetadataUnsolicited { mailbox, values } => {
MailboxDatum::MetadataUnsolicited {
mailbox: to_owned_cow(mailbox),
values: values.into_iter().map(to_owned_cow).collect(),
}
}
}
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Capability<'a> {
Imap4rev1,
Auth(Cow<'a, str>),
Atom(Cow<'a, str>),
}
impl<'a> Capability<'a> {
pub fn into_owned(self) -> Capability<'static> {
match self {
Capability::Imap4rev1 => Capability::Imap4rev1,
Capability::Auth(v) => Capability::Auth(to_owned_cow(v)),
Capability::Atom(v) => Capability::Atom(to_owned_cow(v)),
}
}
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Attribute {
Body,
Envelope,
Flags,
InternalDate,
ModSeq, // RFC 4551, section 3.3.2
Rfc822,
Rfc822Size,
Rfc822Text,
Uid,
}
#[derive(Debug, Eq, PartialEq)]
pub enum MessageSection {
Header,
Mime,
Text,
}
#[derive(Debug, Eq, PartialEq)]
pub enum SectionPath {
Full(MessageSection),
Part(Vec<u32>, Option<MessageSection>),
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum AttributeValue<'a> {
BodySection {
section: Option<SectionPath>,
index: Option<u32>,
data: Option<Cow<'a, [u8]>>,
},
BodyStructure(BodyStructure<'a>),
Envelope(Box<Envelope<'a>>),
Flags(Vec<Cow<'a, str>>),
InternalDate(Cow<'a, str>),
ModSeq(u64), // RFC 4551, section 3.3.2
Rfc822(Option<Cow<'a, [u8]>>),
Rfc822Header(Option<Cow<'a, [u8]>>),
Rfc822Size(u32),
Rfc822Text(Option<Cow<'a, [u8]>>),
Uid(u32),
}
impl<'a> AttributeValue<'a> {
pub fn into_owned(self) -> AttributeValue<'static> {
match self {
AttributeValue::BodySection {
section,
index,
data,
} => AttributeValue::BodySection {
section,
index,
data: data.map(to_owned_cow),
},
AttributeValue::BodyStructure(body) => AttributeValue::BodyStructure(body.into_owned()),
AttributeValue::Envelope(e) => AttributeValue::Envelope(Box::new(e.into_owned())),
AttributeValue::Flags(v) => {
AttributeValue::Flags(v.into_iter().map(to_owned_cow).collect())
}
AttributeValue::InternalDate(v) => AttributeValue::InternalDate(to_owned_cow(v)),
AttributeValue::ModSeq(v) => AttributeValue::ModSeq(v),
AttributeValue::Rfc822(v) => AttributeValue::Rfc822(v.map(to_owned_cow)),
AttributeValue::Rfc822Header(v) => AttributeValue::Rfc822Header(v.map(to_owned_cow)),
AttributeValue::Rfc822Size(v) => AttributeValue::Rfc822Size(v),
AttributeValue::Rfc822Text(v) => AttributeValue::Rfc822Text(v.map(to_owned_cow)),
AttributeValue::Uid(v) => AttributeValue::Uid(v),
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Eq, PartialEq)]
pub enum BodyStructure<'a> {
Basic {
common: BodyContentCommon<'a>,
other: BodyContentSinglePart<'a>,
extension: Option<BodyExtension<'a>>,
},
Text {
common: BodyContentCommon<'a>,
other: BodyContentSinglePart<'a>,
lines: u32,
extension: Option<BodyExtension<'a>>,
},
Message {
common: BodyContentCommon<'a>,
other: BodyContentSinglePart<'a>,
envelope: Envelope<'a>,
body: Box<BodyStructure<'a>>,
lines: u32,
extension: Option<BodyExtension<'a>>,
},
Multipart {
common: BodyContentCommon<'a>,
bodies: Vec<BodyStructure<'a>>,
extension: Option<BodyExtension<'a>>,
},
}
impl<'a> BodyStructure<'a> {
pub fn into_owned(self) -> BodyStructure<'static> {
match self {
BodyStructure::Basic {
common,
other,
extension,
} => BodyStructure::Basic {
common: common.into_owned(),
other: other.into_owned(),
extension: extension.map(|v| v.into_owned()),
},
BodyStructure::Text {
common,
other,
lines,
extension,
} => BodyStructure::Text {
common: common.into_owned(),
other: other.into_owned(),
lines,
extension: extension.map(|v| v.into_owned()),
},
BodyStructure::Message {
common,
other,
envelope,
body,
lines,
extension,
} => BodyStructure::Message {
common: common.into_owned(),
other: other.into_owned(),
envelope: envelope.into_owned(),
body: Box::new(body.into_owned()),
lines,
extension: extension.map(|v| v.into_owned()),
},
BodyStructure::Multipart {
common,
bodies,
extension,
} => BodyStructure::Multipart {
common: common.into_owned(),
bodies: bodies.into_iter().map(|v| v.into_owned()).collect(),
extension: extension.map(|v| v.into_owned()),
},
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct BodyContentCommon<'a> {
pub ty: ContentType<'a>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<Cow<'a, str>>>,
pub location: Option<Cow<'a, str>>,
}
impl<'a> BodyContentCommon<'a> {
pub fn into_owned(self) -> BodyContentCommon<'static> {
BodyContentCommon {
ty: self.ty.into_owned(),
disposition: self.disposition.map(|v| v.into_owned()),
language: self
.language
.map(|v| v.into_iter().map(to_owned_cow).collect()),
location: self.location.map(to_owned_cow),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct BodyContentSinglePart<'a> {
pub id: Option<Cow<'a, str>>,
pub md5: Option<Cow<'a, str>>,
pub description: Option<Cow<'a, str>>,
pub transfer_encoding: ContentEncoding<'a>,
pub octets: u32,
}
impl<'a> BodyContentSinglePart<'a> {
pub fn into_owned(self) -> BodyContentSinglePart<'static> {
BodyContentSinglePart {
id: self.id.map(to_owned_cow),
md5: self.md5.map(to_owned_cow),
description: self.description.map(to_owned_cow),
transfer_encoding: self.transfer_encoding.into_owned(),
octets: self.octets,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct ContentType<'a> {
pub ty: Cow<'a, str>,
pub subtype: Cow<'a, str>,
pub params: BodyParams<'a>,
}
impl<'a> ContentType<'a> {
pub fn into_owned(self) -> ContentType<'static> {
ContentType {
ty: to_owned_cow(self.ty),
subtype: to_owned_cow(self.subtype),
params: body_param_owned(self.params),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct ContentDisposition<'a> {
pub ty: Cow<'a, str>,
pub params: BodyParams<'a>,
}
impl<'a> ContentDisposition<'a> {
pub fn into_owned(self) -> ContentDisposition<'static> {
ContentDisposition {
ty: to_owned_cow(self.ty),
params: body_param_owned(self.params),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum ContentEncoding<'a> {
SevenBit,
EightBit,
Binary,
Base64,
QuotedPrintable,
Other(Cow<'a, str>),
}
impl<'a> ContentEncoding<'a> {
pub fn into_owned(self) -> ContentEncoding<'static> {
match self {
ContentEncoding::SevenBit => ContentEncoding::SevenBit,
ContentEncoding::EightBit => ContentEncoding::EightBit,
ContentEncoding::Binary => ContentEncoding::Binary,
ContentEncoding::Base64 => ContentEncoding::Base64,
ContentEncoding::QuotedPrintable => ContentEncoding::QuotedPrintable,
ContentEncoding::Other(v) => ContentEncoding::Other(to_owned_cow(v)),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum BodyExtension<'a> {
Num(u32),
Str(Option<Cow<'a, str>>),
List(Vec<BodyExtension<'a>>),
}
impl<'a> BodyExtension<'a> {
pub fn into_owned(self) -> BodyExtension<'static> {
match self {
BodyExtension::Num(v) => BodyExtension::Num(v),
BodyExtension::Str(v) => BodyExtension::Str(v.map(to_owned_cow)),
BodyExtension::List(v) => {
BodyExtension::List(v.into_iter().map(|v| v.into_owned()).collect())
}
}
}
}
pub type BodyParams<'a> = Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>;
fn body_param_owned<'a>(v: BodyParams<'a>) -> BodyParams<'static> {
v.map(|v| {
v.into_iter()
.map(|(k, v)| (to_owned_cow(k), to_owned_cow(v)))
.collect()
})
}
/// An RFC 2822 envelope
///
/// See https://datatracker.ietf.org/doc/html/rfc2822#section-3.6 for more details.
#[derive(Debug, Eq, PartialEq)]
pub struct Envelope<'a> {
pub date: Option<Cow<'a, [u8]>>,
pub subject: Option<Cow<'a, [u8]>>,
/// Author of the message; mailbox responsible for writing the message
pub from: Option<Vec<Address<'a>>>,
/// Mailbox of the agent responsible for the message's transmission
pub sender: Option<Vec<Address<'a>>>,
/// Mailbox that the author of the message suggests replies be sent to
pub reply_to: Option<Vec<Address<'a>>>,
pub to: Option<Vec<Address<'a>>>,
pub cc: Option<Vec<Address<'a>>>,
pub bcc: Option<Vec<Address<'a>>>,
pub in_reply_to: Option<Cow<'a, [u8]>>,
pub message_id: Option<Cow<'a, [u8]>>,
}
impl<'a> Envelope<'a> {
pub fn into_owned(self) -> Envelope<'static> {
Envelope {
date: self.date.map(to_owned_cow),
subject: self.subject.map(to_owned_cow),
from: self
.from
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
sender: self
.sender
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
reply_to: self
.reply_to
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
to: self
.to
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
cc: self
.cc
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
bcc: self
.bcc
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
in_reply_to: self.in_reply_to.map(to_owned_cow),
message_id: self.message_id.map(to_owned_cow),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Address<'a> {
pub name: Option<Cow<'a, [u8]>>,
pub adl: Option<Cow<'a, [u8]>>,
pub mailbox: Option<Cow<'a, [u8]>>,
pub host: Option<Cow<'a, [u8]>>,
}
impl<'a> Address<'a> {
pub fn into_owned(self) -> Address<'static> {
Address {
name: self.name.map(to_owned_cow),
adl: self.adl.map(to_owned_cow),
mailbox: self.mailbox.map(to_owned_cow),
host: self.host.map(to_owned_cow),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestId(pub String);
impl RequestId {
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum State {
NotAuthenticated,
Authenticated,
Selected,
Logout,
}
// Body Structure
pub struct BodyFields<'a> {
pub param: BodyParams<'a>,
pub id: Option<Cow<'a, str>>,
pub description: Option<Cow<'a, str>>,
pub transfer_encoding: ContentEncoding<'a>,
pub octets: u32,
}
impl<'a> BodyFields<'a> {
pub fn into_owned(self) -> BodyFields<'static> {
BodyFields {
param: body_param_owned(self.param),
id: self.id.map(to_owned_cow),
description: self.description.map(to_owned_cow),
transfer_encoding: self.transfer_encoding.into_owned(),
octets: self.octets,
}
}
}
pub struct BodyExt1Part<'a> {
pub md5: Option<Cow<'a, str>>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<Cow<'a, str>>>,
pub location: Option<Cow<'a, str>>,
pub extension: Option<BodyExtension<'a>>,
}
impl<'a> BodyExt1Part<'a> {
pub fn into_owned(self) -> BodyExt1Part<'static> {
BodyExt1Part {
md5: self.md5.map(to_owned_cow),
disposition: self.disposition.map(|v| v.into_owned()),
language: self
.language
.map(|v| v.into_iter().map(to_owned_cow).collect()),
location: self.location.map(to_owned_cow),
extension: self.extension.map(|v| v.into_owned()),
}
}
}
pub struct BodyExtMPart<'a> {
pub param: BodyParams<'a>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<Cow<'a, str>>>,
pub location: Option<Cow<'a, str>>,
pub extension: Option<BodyExtension<'a>>,
}
impl<'a> BodyExtMPart<'a> {
pub fn into_owned(self) -> BodyExtMPart<'static> {
BodyExtMPart {
param: body_param_owned(self.param),
disposition: self.disposition.map(|v| v.into_owned()),
language: self
.language
.map(|v| v.into_iter().map(to_owned_cow).collect()),
location: self.location.map(to_owned_cow),
extension: self.extension.map(|v| v.into_owned()),
}
}
}
// IMAP4 QUOTA extension (rfc2087)
/// https://tools.ietf.org/html/rfc2087#section-3
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub enum QuotaResourceName<'a> {
/// Sum of messages' RFC822.SIZE, in units of 1024 octets
Storage,
/// Number of messages
Message,
Atom(Cow<'a, str>),
}
impl<'a> QuotaResourceName<'a> {
pub fn into_owned(self) -> QuotaResourceName<'static> {
match self {
QuotaResourceName::Message => QuotaResourceName::Message,
QuotaResourceName::Storage => QuotaResourceName::Storage,
QuotaResourceName::Atom(v) => QuotaResourceName::Atom(to_owned_cow(v)),
}
}
}
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct QuotaResource<'a> {
pub name: QuotaResourceName<'a>,
/// current usage of the resource
pub usage: u64,
/// resource limit
pub limit: u64,
}
impl<'a> QuotaResource<'a> {
pub fn into_owned(self) -> QuotaResource<'static> {
QuotaResource {
name: self.name.into_owned(),
usage: self.usage,
limit: self.limit,
}
}
}
/// 5.1. QUOTA Response (https://tools.ietf.org/html/rfc2087#section-5.1)
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct Quota<'a> {
/// quota root name
pub root_name: Cow<'a, str>,
pub resources: Vec<QuotaResource<'a>>,
}
impl<'a> Quota<'a> {
pub fn into_owned(self) -> Quota<'static> {
Quota {
root_name: to_owned_cow(self.root_name),
resources: self.resources.into_iter().map(|r| r.into_owned()).collect(),
}
}
}
/// 5.2. QUOTAROOT Response (https://tools.ietf.org/html/rfc2087#section-5.2)
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct QuotaRoot<'a> {
/// mailbox name
pub mailbox_name: Cow<'a, str>,
/// zero or more quota root names
pub quota_root_names: Vec<Cow<'a, str>>,
}
impl<'a> QuotaRoot<'a> {
pub fn into_owned(self) -> QuotaRoot<'static> {
QuotaRoot {
mailbox_name: to_owned_cow(self.mailbox_name),
quota_root_names: self
.quota_root_names
.into_iter()
.map(to_owned_cow)
.collect(),
}
}
}