starttls wtf

This commit is contained in:
Michael Zhang 2021-02-12 06:32:17 -06:00
parent b232bebed5
commit c94eb56e53
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
31 changed files with 3919 additions and 18 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
/target
/.env
/output.log

107
Cargo.lock generated
View file

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.38"
@ -12,6 +14,12 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]]
name = "assert_matches"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
[[package]]
name = "async-trait"
version = "0.1.42"
@ -50,6 +58,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bitvec"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "bufstream"
version = "0.1.4"
@ -226,6 +246,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.12"
@ -351,6 +377,14 @@ dependencies = [
"winutil",
]
[[package]]
name = "imap"
version = "0.12.2"
dependencies = [
"assert_matches",
"nom 6.1.0",
]
[[package]]
name = "inotify"
version = "0.7.1"
@ -438,7 +472,7 @@ dependencies = [
"hostname",
"log",
"native-tls",
"nom",
"nom 4.2.3",
"serde",
"serde_derive",
"serde_json",
@ -576,7 +610,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr",
"version_check",
"version_check 0.1.5",
]
[[package]]
name = "nom"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1"
dependencies = [
"bitvec",
"memchr",
"version_check 0.9.2",
]
[[package]]
@ -685,10 +730,12 @@ dependencies = [
"crossterm",
"fern",
"futures",
"imap",
"lettre",
"log",
"notify",
"pin-project",
"rustls-connector",
"tokio",
"tokio-rustls",
"tokio-util",
@ -795,6 +842,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.8.3"
@ -887,6 +940,30 @@ dependencies = [
"webpki",
]
[[package]]
name = "rustls-connector"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffaf21b0bac725875490d079bb503cf88684618310c2dff167640fa006217cb"
dependencies = [
"log",
"rustls",
"rustls-native-certs",
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
dependencies = [
"openssl-probe",
"rustls",
"schannel",
"security-framework",
]
[[package]]
name = "ryu"
version = "1.0.5"
@ -1039,6 +1116,12 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tap"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
[[package]]
name = "tempfile"
version = "3.2.0"
@ -1066,9 +1149,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6714d663090b6b0acb0fa85841c6d66233d150cdb2602c8f9b8abb03370beb3f"
checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a"
dependencies = [
"autocfg",
"bytes",
@ -1086,9 +1169,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494"
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57"
dependencies = [
"proc-macro2",
"quote",
@ -1146,6 +1229,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "walkdir"
version = "2.3.1"
@ -1308,6 +1397,12 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xdg"
version = "2.2.0"

View file

@ -4,6 +4,9 @@ version = "0.1.0"
authors = ["Michael Zhang <mail@mzhang.io>"]
edition = "2018"
[workspace]
members = ["imap"]
[dependencies]
anyhow = "1.0.38"
async-trait = "0.1.42"
@ -16,8 +19,11 @@ lettre = "0.9.5"
log = "0.4.14"
notify = "4.0.15"
pin-project = "1.0.4"
rustls-connector = "0.13.1"
tokio = { version = "1.1.1", features = ["full"] }
tokio-rustls = "0.22.0"
tokio-util = { version = "0.6.3", features = ["full"] }
webpki-roots = "0.21.0"
xdg = "2.2.0"
imap = { path = "imap" }

View file

@ -10,3 +10,8 @@ Goals:
- Hot-reload on-disk config.
- Submit notifications to gotify-shaped notification servers.
- Never have to actually close the application.
Credits
-------
IMAP library modified from [djc/tokio-imap](https://github.com/djc/tokio-imap), MIT licensed.

1
imap/.ignore Normal file
View file

@ -0,0 +1 @@
*

21
imap/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "imap"
version = "0.12.2"
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"
[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
imap/fuzz/.gitignore vendored Normal file
View file

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

21
imap/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);
});

View file

@ -0,0 +1,397 @@
use std::borrow::Cow;
use std::fmt;
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 starttls() -> Command {
let args = b"STARTTLS".to_vec();
Command {
args,
next_state: None,
}
}
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(),
}
}
}
#[derive(Debug)]
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
imap/src/builders/mod.rs Normal file
View file

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

6
imap/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
imap/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),
}
}
}

25
imap/src/parser/mod.rs Normal file
View file

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

View file

@ -0,0 +1,71 @@
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
character::streaming::char,
combinator::{map, opt},
multi::many0,
sequence::{delimited, preceded, tuple},
IResult,
};
use crate::{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,
},
)(i)
}

View file

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

View file

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

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)
}

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)))
}

View file

@ -0,0 +1,36 @@
//!
//! https://tools.ietf.org/html/rfc5161
//!
//! The IMAP ENABLE Extension
//!
use nom::{
bytes::streaming::tag_no_case,
character::streaming::char,
combinator::map,
multi::many0,
sequence::{preceded, tuple},
IResult,
};
use crate::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(atom, Capability::Atom)(i)
}

295
imap/src/parser/rfc5464.rs Normal file
View file

@ -0,0 +1,295 @@
//!
//! https://tools.ietf.org/html/rfc5464
//!
//! IMAP METADATA extension
//!
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::{map, map_opt},
multi::separated_list0,
sequence::tuple,
IResult,
};
use crate::{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<&str>> {
separated_list0(tag(" "), map(entry_name, slice_to_str))(i)
}
fn metadata_common(i: &[u8]) -> IResult<&[u8], &[u8]> {
let (i, (_, mbox, _)) = tuple((tag_no_case("METADATA "), quoted, tag(" ")))(i)?;
Ok((i, mbox))
}
// [RFC5464 - 4.4.1 METADATA Response with values]
pub(crate) fn metadata_solicited(i: &[u8]) -> IResult<&[u8], Response> {
let (i, (mailbox, values)) = tuple((metadata_common, keyval_list))(i)?;
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataSolicited {
mailbox: slice_to_str(mailbox),
values,
}),
))
}
// [RFC5464 - 4.4.2 Unsolicited METADATA Response without values]
pub(crate) fn metadata_unsolicited(i: &[u8]) -> IResult<&[u8], Response> {
let (i, (mailbox, values)) = tuple((metadata_common, entry_list))(i)?;
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataUnsolicited {
mailbox: slice_to_str(mailbox),
values,
}),
))
}
#[cfg(test)]
mod tests {
use super::{metadata_solicited, metadata_unsolicited};
use crate::types::*;
#[test]
fn test_solicited_fail_1() {
match metadata_solicited(b"METADATA \"\" (/asdfg \"asdf\")\r\n") {
Err(_) => {}
_ => panic!("Error required when entry name is not starting with /private or /shared"),
}
}
#[test]
fn test_solicited_fail_2() {
match metadata_solicited(b"METADATA \"\" (/shared/asdfg \"asdf\")\r\n") {
Err(_) => {}
_ => panic!(
"Error required when in entry name /shared \
is not continued with /admin, /comment or /vendor"
),
}
}
#[test]
fn test_solicited_fail_3() {
match metadata_solicited(b"METADATA \"\" (/private/admin \"asdf\")\r\n") {
Err(_) => {}
_ => panic!(
"Error required when in entry name /private \
is not continued with /comment or /vendor"
),
}
}
#[test]
fn test_solicited_fail_4() {
match metadata_solicited(b"METADATA \"\" (/shared/vendor \"asdf\")\r\n") {
Err(_) => {}
_ => panic!("Error required when vendor name is not provided."),
}
}
#[test]
fn test_solicited_success() {
match metadata_solicited(
b"METADATA \"mbox\" (/shared/vendor/vendorname \"asdf\" \
/private/comment/a \"bbb\")\r\n",
) {
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
assert_eq!(mailbox, "mbox");
assert_eq!(i, b"\r\n");
assert_eq!(values.len(), 2);
assert_eq!(values[0].entry, "/shared/vendor/vendorname");
assert_eq!(
values[0]
.value
.as_ref()
.expect("None value is not expected"),
"asdf"
);
assert_eq!(values[1].entry, "/private/comment/a");
assert_eq!(
values[1]
.value
.as_ref()
.expect("None value is not expected"),
"bbb"
);
}
_ => panic!("Correct METADATA response is not parsed properly."),
}
}
#[test]
fn test_literal_success() {
// match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a \"AAA\")\r\n")
match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a {3}\r\nAAA)\r\n") {
Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
assert_eq!(mailbox, "");
assert_eq!(i, b"\r\n");
assert_eq!(values.len(), 1);
assert_eq!(values[0].entry, "/shared/vendor/vendor.coi/a");
assert_eq!(
values[0]
.value
.as_ref()
.expect("None value is not expected"),
"AAA"
);
}
Err(e) => panic!("ERR: {:?}", e),
_ => panic!("Strange failure"),
}
}
#[test]
fn test_unsolicited_success() {
match metadata_unsolicited(b"METADATA \"theBox\" /shared/admin/qwe /private/comment/a\r\n")
{
Ok((
i,
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }),
)) => {
assert_eq!(i, b"\r\n");
assert_eq!(mailbox, "theBox");
assert_eq!(values.len(), 2);
assert_eq!(values[0], "/shared/admin/qwe");
assert_eq!(values[1], "/private/comment/a");
}
_ => panic!("Correct METADATA response is not parsed properly."),
}
}
}

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,
},
))
}

499
imap/src/parser/tests.rs Normal file
View file

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

329
imap/src/types.rs Normal file
View file

@ -0,0 +1,329 @@
use std::ops::RangeInclusive;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Request<'a>(pub &'a [u8], pub &'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<&'a str>,
},
Done {
tag: RequestId,
status: Status,
code: Option<ResponseCode<'a>>,
information: Option<&'a str>,
},
Data {
status: Status,
code: Option<ResponseCode<'a>>,
information: Option<&'a str>,
},
Expunge(u32),
Vanished {
earlier: bool,
uids: Vec<std::ops::RangeInclusive<u32>>,
},
Fetch(u32, Vec<AttributeValue<'a>>),
MailboxData(MailboxDatum<'a>),
}
impl<'a> Response<'a> {
pub fn from_bytes(buf: &'a [u8]) -> crate::ParseResult {
crate::parser::parse_response(buf)
}
}
#[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<&'a str>>),
Capabilities(Vec<Capability<'a>>),
HighestModSeq(u64), // RFC 4551, section 3.1.1
Parse,
PermanentFlags(Vec<&'a str>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(u32),
UidValidity(u32),
Unseen(u32),
AppendUid(u32, Vec<UidSetMember>),
CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
UidNotSticky,
}
#[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)
}
}
#[derive(Debug, Eq, PartialEq)]
#[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)]
pub enum MailboxDatum<'a> {
Exists(u32),
Flags(Vec<&'a str>),
List {
flags: Vec<&'a str>,
delimiter: Option<&'a str>,
name: &'a str,
},
Search(Vec<u32>),
Status {
mailbox: &'a str,
status: Vec<StatusAttribute>,
},
Recent(u32),
MetadataSolicited {
mailbox: &'a str,
values: Vec<Metadata>,
},
MetadataUnsolicited {
mailbox: &'a str,
values: Vec<&'a str>,
},
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Capability<'a> {
Imap4rev1,
Auth(&'a str),
Atom(&'a str),
}
#[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<&'a [u8]>,
},
BodyStructure(BodyStructure<'a>),
Envelope(Box<Envelope<'a>>),
Flags(Vec<&'a str>),
InternalDate(&'a str),
ModSeq(u64), // RFC 4551, section 3.3.2
Rfc822(Option<&'a [u8]>),
Rfc822Header(Option<&'a [u8]>),
Rfc822Size(u32),
Rfc822Text(Option<&'a [u8]>),
Uid(u32),
}
#[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>>,
},
}
#[derive(Debug, Eq, PartialEq)]
pub struct BodyContentCommon<'a> {
pub ty: ContentType<'a>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<&'a str>>,
pub location: Option<&'a str>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct BodyContentSinglePart<'a> {
pub id: Option<&'a str>,
pub md5: Option<&'a str>,
pub description: Option<&'a str>,
pub transfer_encoding: ContentEncoding<'a>,
pub octets: u32,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ContentType<'a> {
pub ty: &'a str,
pub subtype: &'a str,
pub params: BodyParams<'a>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ContentDisposition<'a> {
pub ty: &'a str,
pub params: BodyParams<'a>,
}
#[derive(Debug, Eq, PartialEq)]
pub enum ContentEncoding<'a> {
SevenBit,
EightBit,
Binary,
Base64,
QuotedPrintable,
Other(&'a str),
}
#[derive(Debug, Eq, PartialEq)]
pub enum BodyExtension<'a> {
Num(u32),
Str(Option<&'a str>),
List(Vec<BodyExtension<'a>>),
}
pub type BodyParams<'a> = Option<Vec<(&'a str, &'a str)>>;
#[derive(Debug, Eq, PartialEq)]
pub struct Envelope<'a> {
pub date: Option<&'a [u8]>,
pub subject: Option<&'a [u8]>,
pub from: Option<Vec<Address<'a>>>,
pub sender: Option<Vec<Address<'a>>>,
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<&'a [u8]>,
pub message_id: Option<&'a [u8]>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct Address<'a> {
pub name: Option<&'a [u8]>,
pub adl: Option<&'a [u8]>,
pub mailbox: Option<&'a [u8]>,
pub host: Option<&'a [u8]>,
}
#[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<&'a str>,
pub description: Option<&'a str>,
pub transfer_encoding: ContentEncoding<'a>,
pub octets: u32,
}
pub struct BodyExt1Part<'a> {
pub md5: Option<&'a str>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<&'a str>>,
pub location: Option<&'a str>,
pub extension: Option<BodyExtension<'a>>,
}
pub struct BodyExtMPart<'a> {
pub param: BodyParams<'a>,
pub disposition: Option<ContentDisposition<'a>>,
pub language: Option<Vec<&'a str>>,
pub location: Option<&'a str>,
pub extension: Option<BodyExtension<'a>>,
}

View file

@ -1,7 +0,0 @@
[2021-02-12][01:42:31][panorama][INFO] poggers
[2021-02-12][01:42:36][panorama][INFO] poggers
[2021-02-12][01:56:24][panorama][INFO] poggers
[2021-02-12][01:56:50][panorama][INFO] poggers
[2021-02-12][01:56:50][panorama::panorama][DEBUG] starting all apps...
[2021-02-12][02:04:53][panorama][INFO] poggers
[2021-02-12][02:04:53][panorama::panorama][DEBUG] starting all apps...

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
nightly

186
src/mail.rs Normal file
View file

@ -0,0 +1,186 @@
use std::collections::HashMap;
use std::fmt::Display;
use std::sync::Arc;
use anyhow::Result;
use futures::{
future::{self, Either, FutureExt},
pin_mut, select,
sink::{Sink, SinkExt},
stream::{Stream, StreamExt, TryStream},
};
use imap::{
builders::command::{Command, CommandBuilder},
parser::parse_response,
types::{Capability, RequestId, Response, ResponseCode, State, Status},
};
use tokio::{
net::TcpStream,
sync::{
mpsc::{self, Receiver},
oneshot,
},
};
use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector};
use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError};
pub async fn run_mail(server: impl AsRef<str>, port: u16) -> Result<()> {
let server = server.as_ref();
let client = TcpStream::connect((server, port)).await?;
let codec = LinesCodec::new();
let mut framed = codec.framed(client);
let mut state = State::NotAuthenticated;
let (sink, stream) = framed.split::<String>();
let result = listen_loop(&mut state, sink, stream).await?;
if let LoopExit::NegotiateTls(stream, sink) = result {
debug!("negotiating tls");
let mut config = ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
let config = TlsConnector::from(Arc::new(config));
let dnsname = DNSNameRef::try_from_ascii_str(server).unwrap();
// reconstruct the original stream
let stream = stream.reunite(sink)?.into_inner();
// let stream = TcpStream::connect((server, port)).await?;
let stream = config.connect(dnsname, stream).await?;
let codec = LinesCodec::new();
let mut framed = codec.framed(stream);
let (sink, stream) = framed.split::<String>();
listen_loop(&mut state, sink, stream).await?;
}
Ok(())
}
enum LoopExit<S, S2> {
NegotiateTls(S, S2),
Closed,
}
async fn listen_loop<S, S2>(st: &mut State, mut sink: S2, mut stream: S) -> Result<LoopExit<S, S2>>
where
S: Stream<Item = Result<String, LinesCodecError>> + Unpin,
S2: Sink<String> + Unpin,
S2::Error: Display,
{
let (tx, mut rx) = mpsc::unbounded_channel::<()>();
let mut cmd_mgr = CommandManager::new(sink);
loop {
let fut1 = stream.next();
let fut2 = rx.recv();
pin_mut!(fut1);
pin_mut!(fut2);
debug!("waiting for next select");
match future::select(fut1, fut2).await {
Either::Left((line, _)) => {
let mut line = match line {
Some(v) => v?,
None => break,
};
line += "\r\n";
let (_, resp) = match parse_response(line.as_bytes()) {
Ok(v) => v,
Err(e) => bail!(e.to_string()),
};
debug!("<<< {:?}", resp);
match st {
State::NotAuthenticated => match resp {
Response::Data {
status: Status::Ok,
code: Some(ResponseCode::Capabilities(caps)),
..
} => {
let mut has_starttls = false;
for cap in caps {
if let Capability::Atom("STARTTLS") = cap {
has_starttls = true;
}
}
if has_starttls {
let cmd = Command {
args: b"STARTTLS".to_vec(),
next_state: None,
};
let tx = tx.clone();
cmd_mgr
.send(cmd, move |_| {
tx.send(()).unwrap();
})
.await?;
}
}
Response::Done { tag, code, .. } => {
cmd_mgr.process_done(tag, code)?;
}
_ => {}
},
_ => {}
}
}
Either::Right((_, _)) => {
debug!("ENCOUNTERED EXIT");
let sink = cmd_mgr.decompose();
return Ok(LoopExit::NegotiateTls(stream, sink));
}
}
}
Ok(LoopExit::Closed)
}
struct CommandManager<S> {
tag_idx: usize,
in_flight: HashMap<String, Box<dyn Fn(Option<ResponseCode>) + Send>>,
sink: S,
}
impl<S> CommandManager<S>
where
S: Sink<String> + Unpin,
{
pub fn new(sink: S) -> Self {
CommandManager {
tag_idx: 0,
in_flight: HashMap::new(),
sink,
}
}
pub fn decompose(self) -> S {
self.sink
}
pub async fn send(
&mut self,
cmd: Command,
cb: impl Fn(Option<ResponseCode>) + Send + 'static,
) -> Result<()> {
let tag_idx = self.tag_idx;
self.tag_idx += 1;
let cb = Box::new(cb);
let tag_str = format!("t{}", tag_idx);
let cmd_str = std::str::from_utf8(&cmd.args)?;
let full_str = format!("{} {}", tag_str, cmd_str);
self.in_flight.insert(tag_str.clone(), cb);
self.sink
.send(full_str)
.await
.map_err(|_| anyhow!("failed to send command"))
}
pub fn process_done(&mut self, id: RequestId, code: Option<ResponseCode>) -> Result<()> {
let name = std::str::from_utf8(id.as_bytes())?;
if let Some(cb) = self.in_flight.remove(name) {
cb(code);
}
Ok(())
}
}

View file

@ -1,23 +1,51 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate crossterm;
#[macro_use]
extern crate log;
mod mail;
mod ui;
use anyhow::Result;
use lettre::SmtpClient;
use futures::future::TryFutureExt;
use tokio::sync::oneshot;
type ExitSender = oneshot::Sender<()>;
#[tokio::main]
async fn main() -> Result<()> {
SmtpClient::new_simple("");
setup_logger()?;
let (exit_tx, exit_rx) = oneshot::channel::<()>();
tokio::spawn(mail::run_mail("mzhang.io", 143).unwrap_or_else(report_err));
let stdout = std::io::stdout();
tokio::spawn(ui::run_ui(stdout, exit_tx));
tokio::spawn(ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err));
exit_rx.await?;
Ok(())
}
fn report_err(err: anyhow::Error) {
error!("error: {:?}", err);
}
fn setup_logger() -> Result<()> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(log::LevelFilter::Debug)
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}

View file

@ -12,7 +12,7 @@ use tokio::time;
use crate::ExitSender;
const FRAME: Duration = Duration::from_millis(16);
const FRAME: Duration = Duration::from_millis(33);
pub async fn run_ui(mut w: impl Write, exit: ExitSender) -> Result<()> {
execute!(w, cursor::Hide, terminal::EnterAlternateScreen)?;
@ -22,7 +22,7 @@ pub async fn run_ui(mut w: impl Write, exit: ExitSender) -> Result<()> {
execute!(w, cursor::MoveTo(0, 0))?;
let now = Local::now();
println!("shiet {}", now);
println!("time {}", now);
// approx 60fps
time::sleep(FRAME).await;