wasm-demo/melib/src/email/parser.rs

2467 lines
86 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* meli - parser module
*
* Copyright 2017 - 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::error::{MeliError, Result, ResultIntoMeliError};
use nom::{
branch::alt,
bytes::complete::{is_a, is_not, tag, take_until, take_while},
character::is_hex_digit,
combinator::peek,
combinator::{map, opt},
error::{context, ErrorKind},
multi::{many0, many1, separated_list, separated_nonempty_list},
number::complete::le_u8,
sequence::{delimited, pair, preceded, separated_pair, terminated},
};
use smallvec::SmallVec;
use std::borrow::Cow;
macro_rules! to_str {
($l:expr) => {{
unsafe { std::str::from_utf8_unchecked($l) }
}};
}
#[derive(Eq, PartialEq)]
pub struct ParsingError<I> {
input: I,
error: Cow<'static, str>,
}
impl core::fmt::Debug for ParsingError<&'_ [u8]> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt.debug_struct("ParsingError")
.field("input", &to_str!(&self.input))
.field("error", &self.error)
.finish()
}
}
struct DebugOkWrapper<'r, I, R: AsRef<[u8]>>(&'r IResult<I, R>);
impl<R: AsRef<[u8]> + core::fmt::Debug> core::fmt::Debug for DebugOkWrapper<'_, &'_ [u8], R> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
if let Ok((a, b)) = self.0 {
write!(fmt, "Ok({}, {})", &to_str!(a), &to_str!(b.as_ref()))
} else {
write!(fmt, "{:?}", self.0)
}
}
}
pub type IResult<I, O, E = ParsingError<I>> = std::result::Result<(I, O), nom::Err<E>>;
impl<'i> ParsingError<&'i str> {
pub fn as_bytes(self) -> ParsingError<&'i [u8]> {
ParsingError {
input: self.input.as_bytes(),
error: self.error,
}
}
}
impl<I> From<(I, &'static str)> for ParsingError<I> {
fn from((input, error): (I, &'static str)) -> Self {
Self {
input,
error: error.into(),
}
}
}
impl<I> From<(I, String)> for ParsingError<I> {
fn from((input, error): (I, String)) -> Self {
Self {
input,
error: error.into(),
}
}
}
impl<I> nom::error::ParseError<I> for ParsingError<I> {
fn from_error_kind(input: I, kind: ErrorKind) -> Self {
Self {
input,
error: kind.description().to_string().into(),
}
}
fn append(input: I, kind: ErrorKind, other: Self) -> Self {
Self {
input,
error: format!("{}, {}", kind.description(), other.error).into(),
}
}
}
impl<'i> From<ParsingError<&'i [u8]>> for MeliError {
fn from(val: ParsingError<&'i [u8]>) -> MeliError {
MeliError::new("Parsing error").set_summary(format!(
r#"In input: "{}...",
Error: {}"#,
String::from_utf8_lossy(val.input)
.chars()
.take(30)
.collect::<String>(),
val.error
))
}
}
impl<'i> From<ParsingError<&'i str>> for MeliError {
fn from(val: ParsingError<&'i str>) -> MeliError {
MeliError::new("Parsing error").set_summary(format!(
r#"In input: "{}...",
Error: {}"#,
val.input.chars().take(30).collect::<String>(),
val.error
))
}
}
impl<'i> From<nom::Err<ParsingError<&'i [u8]>>> for MeliError {
fn from(val: nom::Err<ParsingError<&'i [u8]>>) -> MeliError {
match val {
nom::Err::Incomplete(_) => MeliError::new("Parsing Error: Incomplete"),
nom::Err::Error(err) | nom::Err::Failure(err) => err.into(),
}
}
}
impl<'i> From<nom::Err<ParsingError<&'i str>>> for MeliError {
fn from(val: nom::Err<ParsingError<&'i str>>) -> MeliError {
match val {
nom::Err::Incomplete(_) => MeliError::new("Parsing Error: Incomplete"),
nom::Err::Error(err) | nom::Err::Failure(err) => err.into(),
}
}
}
macro_rules! is_ctl_or_space {
($var:ident) => {
/* <any ASCII control character and DEL> */
$var < 33 || $var == 127
};
($var:expr) => {
/* <any ASCII control character and DEL> */
$var < 33 || $var == 127
};
}
macro_rules! is_whitespace {
($var:ident) => {
$var == b' ' || $var == b'\t' || $var == b'\n' || $var == b'\r'
};
($var:expr) => {
$var == b' ' || $var == b'\t' || $var == b'\n' || $var == b'\r'
};
}
pub trait BytesExt {
fn rtrim(&self) -> &Self;
fn ltrim(&self) -> &Self;
fn trim(&self) -> &Self;
fn find(&self, needle: &[u8]) -> Option<usize>;
fn rfind(&self, needle: &[u8]) -> Option<usize>;
fn replace(&self, from: &[u8], to: &[u8]) -> Vec<u8>;
fn is_quoted(&self) -> bool;
}
impl BytesExt for [u8] {
fn rtrim(&self) -> &Self {
if let Some(last) = self.iter().rposition(|b| !is_whitespace!(*b)) {
&self[..=last]
} else {
&[]
}
}
fn ltrim(&self) -> &Self {
if let Some(first) = self.iter().position(|b| !is_whitespace!(*b)) {
&self[first..]
} else {
&[]
}
}
fn trim(&self) -> &[u8] {
self.rtrim().ltrim()
}
// https://stackoverflow.com/a/35907071
fn find(&self, needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return None;
}
self.windows(needle.len())
.position(|window| window == needle)
}
fn rfind(&self, needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return None;
}
self.windows(needle.len())
.rposition(|window| window == needle)
}
fn replace(&self, from: &[u8], to: &[u8]) -> Vec<u8> {
let mut ret = self.to_vec();
if let Some(idx) = self.find(from) {
ret.splice(idx..(idx + from.len()), to.iter().cloned());
}
ret
}
fn is_quoted(&self) -> bool {
self.starts_with(b"\"") && self.ends_with(b"\"") && self.len() > 1
}
}
pub trait BytesIterExt {
fn join(&mut self, sep: u8) -> Vec<u8>;
}
impl<'a, P: for<'r> FnMut(&'r u8) -> bool> BytesIterExt for std::slice::Split<'a, u8, P> {
fn join(&mut self, sep: u8) -> Vec<u8> {
self.fold(vec![], |mut acc, el| {
if !acc.is_empty() {
acc.push(sep);
}
acc.extend(el.iter());
acc
})
}
}
//fn parser(input: I) -> IResult<I, O, E>;
pub fn mail(input: &[u8]) -> Result<(Vec<(&[u8], &[u8])>, &[u8])> {
let (rest, result) = alt((
separated_pair(
headers::headers,
alt((tag(b"\n"), tag(b"\r\n"))),
take_while(|_| true),
),
pair(headers::headers, generic::eof),
))(input)
.chain_err_summary(|| "Could not parse mail")?;
if !rest.is_empty() {
return Err(MeliError::new("Got leftover bytes after parsing mail"));
}
Ok(result)
}
pub mod generic {
use super::*;
pub fn angle_bracket_delimeted_list(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
separated_nonempty_list(is_a(","), delimited(tag("<"), take_until(">"), tag(">")))(
input.rtrim(),
)
// separated_nonempty_list!(complete!(is_a!(",")), ws!(complete!(complete!(delimited!(tag!("<"), take_until1!(">"), tag!(">")))))));
}
pub fn date(input: &[u8]) -> Result<crate::datetime::UnixTimestamp> {
let (_, mut parsed_result) = encodings::phrase(&eat_comments(input), false)?;
if let Some(pos) = parsed_result.find(b"-0000") {
parsed_result[pos] = b'+';
}
crate::datetime::rfc822_to_timestamp(parsed_result.trim())
}
///`%x21-7E`
fn vchar(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "vchar(): empty input").into()));
}
if input[0] >= 0x21 && input[0] <= 0x7e {
Ok((&input[1..], input[0]))
} else {
Err(nom::Err::Error((input, "vchar(): out of range").into()))
}
}
///`quoted-pair = ("\" (VCHAR / WSP)) / obs-qp`
fn quoted_pair(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, byte) = preceded(tag("\\"), alt((vchar, wsp)))(input)?;
Ok((input, vec![byte].into()))
}
///```text
///ctext = %d33-39 / ; Printable US-ASCII
/// %d42-91 / ; characters not including
/// %d93-126 / ; "(", ")", or "\"
/// obs-ctext
///```
fn ctext(input: &[u8]) -> IResult<&[u8], ()> {
if input.is_empty() {
return Err(nom::Err::Error((input, "ctext(): empty input").into()));
}
if (input[0] >= 33 && input[0] <= 39)
|| (input[0] >= 42 && input[0] <= 91)
|| (input[0] >= 93 && input[0] <= 126)
{
Ok((&input[1..], ()))
} else {
Err(nom::Err::Error((input, "ctext(): out of range").into()))
}
}
///```text
///ctext = %d33-39 / ; Printable US-ASCII
/// %d42-91 / ; characters not including
/// %d93-126 / ; "(", ")", or "\"
/// obs-ctext
///ccontent = ctext / quoted-pair / comment
///comment = "(" *([FWS] ccontent) [FWS] ")"
///```
pub fn comment(input: &[u8]) -> IResult<&[u8], ()> {
if !input.starts_with(b"(") {
return Err(nom::Err::Error(
(input, "comment(): not starting with '('").into(),
));
}
let mut input = &input[1..];
let mut comment_level = 1;
while comment_level > 0 {
if input.is_empty() {
return Err(nom::Err::Error(
(input, "comment(): unclosed comment").into(),
));
}
input = context("comment()", opt(fws))(input)?.0;
while let Ok((_input, _)) =
context("comment()", alt((ctext, map(quoted_pair, |_| ()))))(input)
{
input = _input;
}
if input.starts_with(b")") {
comment_level -= 1;
input = &input[1..];
} else if input.starts_with(b"(") {
comment_level += 1;
input = &input[1..];
} else {
input = context("comment()", opt(fws))(input)?.0;
}
}
Ok((input, ()))
}
#[test]
fn test_parser_comment() {
let s = b"(recursive (comment) block)";
assert_eq!(comment(s), Ok((&b""[..], ())));
}
///`FWS = ([*WSP CRLF] 1*WSP) / obs-FWS`
pub fn fws(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
if let Ok((rest, ws)) = terminated(many0(wsp), crlf)(input) {
let mut v: Vec<u8> = ws.into_iter().fold(vec![], |mut acc, x| {
acc.push(x);
acc
});
let mut width = 0;
let mut input = rest;
while let Ok((input_, w)) = wsp(input) {
v.push(w);
width += 1;
input = input_;
}
if width == 0 {
Err(nom::Err::Error((input, "fws(): no WSP").into()))
} else {
Ok((input, Cow::Owned(v)))
}
} else {
let orig_input = input;
let mut input = input;
let mut width = 0;
while let Ok((input_, _)) = wsp(input) {
width += 1;
input = input_;
}
if width == 0 {
Err(nom::Err::Error((input, "fws(): no WSP").into()))
} else {
Ok((input, Cow::Borrowed(&orig_input[..width])))
}
}
}
///`WSP = SP / HTAB ; white space`
pub fn wsp(input: &[u8]) -> IResult<&[u8], u8> {
if input.starts_with(b" ") || input.starts_with(b"\t") {
Ok((&input[1..], input[0]))
} else {
Err(nom::Err::Error((input, "wsp(): not whitespace").into()))
}
}
pub fn crlf(input: &[u8]) -> IResult<&[u8], ()> {
if input.starts_with(b"\n") {
Ok((&input[1..], ()))
} else if input.starts_with(b"\r\n") {
Ok((&input[2..], ()))
} else {
Err(nom::Err::Error((input, "crlf(): not whitespace").into()))
}
}
///`CFWS = (1*([FWS] comment) [FWS]) / FWS`
pub fn cfws(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
alt((
|input| {
let (input, pr) = many1(terminated(opt(fws), comment))(input)?;
let (input, end) = opt(fws)(input)?;
let mut pr = pr.into_iter().filter_map(|s| s).fold(vec![], |mut acc, x| {
acc.extend_from_slice(&x);
acc
});
if pr.is_empty() {
Ok((input, end.unwrap_or((&b""[..]).into())))
} else {
if let Some(end) = end {
pr.extend_from_slice(&end);
}
Ok((input, pr.into()))
}
},
fws,
))(input)
}
fn eat_comments(input: &[u8]) -> Vec<u8> {
let mut in_comment = false;
input
.iter()
.fold(Vec::with_capacity(input.len()), |mut acc, x| {
if *x == b'(' && !in_comment {
in_comment = true;
acc
} else if *x == b')' && in_comment {
in_comment = false;
acc
} else if in_comment {
acc
} else {
acc.push(*x);
acc
}
})
}
///`unstructured = (*([FWS] VCHAR) *WSP) / obs-unstruct`
pub fn unstructured(input: &[u8]) -> Result<String> {
let (input, r): (_, Vec<(Option<Cow<'_, [u8]>>, u8)>) =
many0(pair(opt(fws), vchar))(input)?;
let (input, rest_wsp): (_, Vec<u8>) = many0(wsp)(input)?;
let mut ret_s = Vec::new();
for (opt_slice, b) in r {
if let Some(slice) = opt_slice {
ret_s.extend_from_slice(&slice);
}
ret_s.push(b);
}
ret_s.extend_from_slice(&rest_wsp);
let ret_s = String::from_utf8_lossy(&ret_s).into_owned();
if !input.is_empty() {
Err(MeliError::from(format!(
"unstructured(): unmatched input: {} while result is {}",
to_str!(input),
ret_s
)))
} else {
Ok(ret_s)
}
}
use crate::email::address::Address;
use crate::email::mailto::Mailto;
pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> {
if !input.starts_with(b"mailto:") {
return Err(nom::Err::Error(
(input, "mailto(): input doesn't start with `mailto:`").into(),
));
}
input = &input[b"mailto:".len()..];
let end = input.iter().position(|e| *e == b'?').unwrap_or(input.len());
let address: Address;
if let Ok((_, addr)) = crate::email::parser::address::address(&input[..end]) {
address = addr;
input = if input[end..].is_empty() {
&input[end..]
} else {
&input[end + 1..]
};
} else {
return Err(nom::Err::Error(
(input, "mailto(): address not found in input").into(),
));
}
let mut subject = None;
let mut cc = None;
let mut bcc = None;
let mut body = None;
while !input.is_empty() {
let tag = if let Some(tag_pos) = input.iter().position(|e| *e == b'=') {
let ret = &input[0..tag_pos];
input = &input[tag_pos + 1..];
ret
} else {
return Err(nom::Err::Error(
(input, "mailto(): extra characters found in input").into(),
));
};
let value_end = input.iter().position(|e| *e == b'&').unwrap_or(input.len());
let value = String::from_utf8_lossy(&input[..value_end]).to_string();
match tag {
b"subject" if subject.is_none() => {
subject = Some(value);
}
b"cc" if cc.is_none() => {
cc = Some(value);
}
b"bcc" if bcc.is_none() => {
bcc = Some(value);
}
b"body" if body.is_none() => {
/* FIXME:
* Parse escaped characters properly.
*/
body = Some(value.replace("%20", " ").replace("%0A", "\n"));
}
_ => {
return Err(nom::Err::Error(
(input, "mailto(): unknown tag in input").into(),
));
}
}
if input[value_end..].is_empty() {
break;
}
input = &input[value_end + 1..];
}
Ok((
input,
Mailto {
address,
subject,
cc,
bcc,
body,
},
))
}
pub struct HeaderIterator<'a>(pub &'a [u8]);
impl<'a> Iterator for HeaderIterator<'a> {
type Item = (&'a [u8], &'a [u8]);
fn next(&mut self) -> Option<(&'a [u8], &'a [u8])> {
if self.0.is_empty() {
return None;
}
match super::headers::header(self.0) {
Ok((rest, value)) => {
self.0 = rest;
Some(value)
}
_ => {
self.0 = &[];
None
}
}
}
}
pub fn eof(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.is_empty() {
Ok((input, input))
} else {
Err(nom::Err::Error((input, "expected EOF").into()))
}
}
#[test]
fn test_parser_cfws() {
let s = r#"This
is a test"#;
assert_eq!(&unstructured(s.as_bytes()).unwrap(), "This is a test",);
assert_eq!(&unstructured(s.as_bytes()).unwrap(), "This is a test",);
let s = "this is\n\ta folded name";
assert_eq!(
&unstructured(s.as_bytes()).unwrap(),
"this is a\tfolded name",
);
}
}
pub mod headers {
use super::*;
pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], &[u8])>> {
many1(header)(input)
}
pub fn header(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
alt((header_without_val, header_with_val))(input)
}
pub fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
if input.is_empty() {
return Err(nom::Err::Error(
(input, "header_without_val(): input is empty").into(),
));
} else if input.starts_with(b"\n") || input.starts_with(b"\r\n") {
return Err(nom::Err::Error(
(
input,
"header_without_val(): input starts with folding whitespace",
)
.into(),
));
}
let mut ptr = 0;
let mut name: &[u8] = &[];
let mut has_colon = false;
/* field-name = 1*<any CHAR, excluding CTLs, SPACE, and ":"> */
for (i, x) in input.iter().enumerate() {
if input[i..].starts_with(b"\r\n") {
name = &input[0..i];
ptr = i + 2;
break;
} else if *x == b':' {
name = &input[0..i];
has_colon = true;
ptr = i;
break;
} else if *x == b'\n' {
name = &input[0..i];
ptr = i;
break;
} else if is_ctl_or_space!(*x) {
return Err(nom::Err::Error((
input,
r#"header_without_val(): field-name should contain "any CHAR, excluding CTLs, SPACE, and ":""#,
).into()));
}
}
if name.is_empty() || input.len() <= ptr {
return Err(nom::Err::Error(
(input, "header_without_val(): not enough input").into(),
));
}
if input[ptr] == b':' {
ptr += 1;
has_colon = true;
if ptr >= input.len() {
return Err(nom::Err::Error(
(input, "header_without_val(): EOF after colon").into(),
));
}
}
if !has_colon {
return Err(nom::Err::Error(
(input, "header_without_val(): no colon found").into(),
));
}
while input[ptr] == b' ' {
ptr += 1;
if ptr >= input.len() {
return Err(nom::Err::Error(
(
input,
"header_without_val(): expected start of next field, found EOF",
)
.into(),
));
}
}
if input[ptr..].starts_with(b"\n") {
ptr += 1;
if ptr >= input.len() {
return Err(nom::Err::Error(
(
input,
"header_without_val(): expected folding whitespace, found EOF",
)
.into(),
));
}
if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' {
Ok((&input[ptr..], (name, b"")))
} else {
Err(nom::Err::Error(
(
input,
"header_without_val(): expected folding whitespace, found EOF",
)
.into(),
))
}
} else if input[ptr..].starts_with(b"\r\n") {
ptr += 2;
if ptr > input.len() {
return Err(nom::Err::Error(
(
input,
"header_without_val(): expected folding whitespace, found EOF",
)
.into(),
));
}
if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' {
Ok((&input[ptr..], (name, b"")))
} else {
Err(nom::Err::Error(
(
&input[ptr..],
"header_without_val(): expected folding whitespace, found EOF",
)
.into(),
))
}
} else {
Err(nom::Err::Error(
(
&input[ptr..],
"header_without_val(): expected folding whitespace (newline)",
)
.into(),
))
}
}
/* A header can span multiple lines, eg:
*
* Received: from -------------------- (-------------------------)
* by --------------------- (--------------------- [------------------]) (-----------------------)
* with ESMTP id ------------ for <------------------->;
* Tue, 5 Jan 2016 21:30:44 +0100 (CET)
*/
pub fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
let input_len = input.len();
for (i, x) in input.iter().enumerate() {
if *x == b'\n'
&& (((i + 1) < input_len && input[i + 1] != b' ' && input[i + 1] != b'\t')
|| i + 1 == input_len)
{
return Ok((&input[(i + 1)..], &input[0..i]));
} else if input[i..].starts_with(b"\r\n")
&& (((i + 2) < input_len && input[i + 2] != b' ' && input[i + 2] != b'\t')
|| i + 2 == input_len)
{
return Ok((&input[(i + 2)..], &input[0..i]));
}
}
Err(nom::Err::Error(
(
input,
"header_value(): expected new line after header value",
)
.into(),
))
}
/* Parse a single header as a tuple */
pub fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
if input.is_empty() {
return Err(nom::Err::Error(
(input, "header_with_val(): empty input").into(),
));
} else if input.starts_with(b"\n") || input.starts_with(b"\r\n") {
return Err(nom::Err::Error(
(input, "header_with_val(): field name starts with new line").into(),
));
}
let mut ptr = 0;
let mut name: &[u8] = &[];
/* field-name = 1*<any CHAR, excluding CTLs, SPACE, and ":"> */
for (i, x) in input.iter().enumerate() {
if *x == b':' {
name = &input[0..i];
ptr = i + 1;
break;
} else if is_ctl_or_space!(*x) {
return Err(nom::Err::Error(
(
&input[i..],
format!("header_with_val(): invalid character: {:?}", *x as char),
)
.into(),
));
}
}
if name.is_empty() {
return Err(nom::Err::Error(
(input, "header_with_val(): found empty header name ").into(),
));
}
if ptr >= input.len() {
return Err(nom::Err::Error(
(input, "header_with_val(): found EOF").into(),
));
}
if input[ptr] == b'\n' {
ptr += 1;
if ptr >= input.len() {
return Err(nom::Err::Error(
(input, "header_with_val(): found EOF").into(),
));
}
} else if input[ptr..].starts_with(b"\r\n") {
ptr += 2;
if ptr > input.len() {
return Err(nom::Err::Error(
(input, "header_with_val(): found EOF").into(),
));
}
}
if ptr >= input.len() {
return Err(nom::Err::Error(
(input, "header_with_val(): found EOF").into(),
));
}
while input[ptr] == b' ' || input[ptr] == b'\t' {
ptr += 1;
if ptr >= input.len() {
return Err(nom::Err::Error(
(input, "header_with_val(): found EOF").into(),
));
}
}
header_value(&input[ptr..]).map(|(rest, value)| (rest, (name, value)))
}
pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.is_empty() {
return Err(nom::Err::Error(
(input, "headers_raw(): input is empty").into(),
));
}
for i in 0..input.len() {
if input[i..].starts_with(b"\n\n") {
return Ok((&input[(i + 1)..], &input[0..=i]));
} else if input[i..].starts_with(b"\r\n\r\n") {
return Ok((&input[(i + 2)..], &input[0..=i]));
}
}
Err(nom::Err::Error(
(input, "headers_raw(): got EOF while looking for new line").into(),
))
}
}
pub mod attachments {
use super::*;
use crate::email::address::*;
use crate::email::attachment_types::{ContentDisposition, ContentDispositionKind};
pub fn attachment(input: &[u8]) -> IResult<&[u8], (std::vec::Vec<(&[u8], &[u8])>, &[u8])> {
alt((
separated_pair(
many0(headers::header),
alt((tag(b"\n"), tag(b"\r\n"))),
take_while(|_| true),
),
pair(headers::headers, generic::eof),
))(input)
}
pub fn multipart_parts<'a>(
input: &'a [u8],
boundary: &[u8],
) -> IResult<&'a [u8], Vec<StrBuilder>> {
let mut ret: Vec<_> = Vec::new();
let mut input = input;
let mut offset = 0;
loop {
let b_start = if let Some(v) = input.find(boundary) {
v
} else {
return Err(nom::Err::Error(
(input, "multipart_parts(): could not find starting boundary").into(),
));
};
if b_start < 2 {
return Err(nom::Err::Error(
(input, "multipart_parts(): malformed boundary").into(),
));
}
offset += b_start - 2;
input = &input[b_start - 2..];
if &input[0..2] == b"--" {
offset += 2 + boundary.len();
input = &input[2 + boundary.len()..];
if input[0] == b'\n' {
offset += 1;
input = &input[1..];
} else if input[0..].starts_with(b"\r\n") {
offset += 2;
input = &input[2..];
} else {
continue;
}
break;
}
}
loop {
if input.len() < boundary.len() + 4 {
return Err(nom::Err::Error(
(input, "multipart_parts(): found EOF").into(),
));
}
if let Some(end) = input.find(boundary) {
if &input[end - 2..end] != b"--" {
return Err(nom::Err::Error(
(input, "multipart_parts(): malformed boundary").into(),
));
}
if input[..end - 2].ends_with(b"\r\n") {
ret.push(StrBuilder {
offset,
length: end - 4,
});
} else {
ret.push(StrBuilder {
offset,
length: end - 3,
});
}
offset += end + boundary.len();
input = &input[end + boundary.len()..];
if input.len() < 2 || input[0] != b'\n' || &input[0..2] == b"--" {
break;
}
if input[0] == b'\n' {
offset += 1;
input = &input[1..];
} else if input[0..].starts_with(b"\r\n") {
offset += 2;
input = &input[2..];
}
} else {
ret.push(StrBuilder {
offset,
length: input.len(),
});
break;
}
}
Ok((input, ret))
}
fn parts_f(boundary: &[u8]) -> impl Fn(&[u8]) -> IResult<&[u8], Vec<&[u8]>> + '_ {
move |input: &[u8]| -> IResult<&[u8], Vec<&[u8]>> {
let mut ret: Vec<&[u8]> = Vec::new();
let mut input = input;
loop {
let b_start = if let Some(v) = input.find(boundary) {
v
} else {
return Err(nom::Err::Error(
(input, "parts_f(): could not find starting boundary").into(),
));
};
if b_start < 2 {
return Err(nom::Err::Error(
(input, "parts_f(): malformed boundary").into(),
));
}
input = &input[b_start - 2..];
if &input[0..2] == b"--" {
input = &input[2 + boundary.len()..];
if input[0] == b'\n' {
input = &input[1..];
} else if input[0..].starts_with(b"\r\n") {
input = &input[2..];
} else {
continue;
}
break;
}
}
loop {
if input.len() < boundary.len() + 4 {
return Err(nom::Err::Error((input, "parts_f(): found EOF").into()));
}
if let Some(end) = input.find(boundary) {
if &input[end - 2..end] != b"--" {
return Err(nom::Err::Error((input, "parts_f(): found EOF").into()));
}
if input[..end - 2].ends_with(b"\r\n") {
ret.push(&input[..end - 4]);
} else {
ret.push(&input[..end - 3]);
}
input = &input[end + boundary.len()..];
if input.len() < 2
|| (input[0] != b'\n' && &input[0..2] != b"\r\n")
|| &input[0..2] == b"--"
{
break;
}
if input[0] == b'\n' {
input = &input[1..];
} else if input[0..].starts_with(b"\r\n") {
input = &input[2..];
}
} else {
ret.push(input);
break;
}
}
Ok((input, ret))
}
}
pub fn parts<'a>(input: &'a [u8], boundary: &[u8]) -> IResult<&'a [u8], Vec<&'a [u8]>> {
alt((
parts_f(boundary),
|input: &'a [u8]| -> IResult<&'a [u8], Vec<&'a [u8]>> {
let (input, _) = take_until(&b"--"[..])(input)?;
let (input, _) = take_until(boundary)(input)?;
Ok((input, Vec::<&[u8]>::new()))
},
))(input)
/*
alt_complete!(call!(parts_f, boundary) | do_parse!(
take_until_and_consume!(&b"--"[..]) >>
take_until_and_consume!(boundary) >>
( { Vec::<&[u8]>::new() } ))
));
*/
}
/* Caution: values should be passed through phrase() */
pub fn content_type_parameter(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
let (input, _) = tag(";")(input.ltrim())?;
let (input, name) = terminated(take_until("="), tag("="))(input.ltrim())?;
let (input, value) = alt((
delimited(tag("\""), take_until("\""), tag("\"")),
is_not(";"),
))(input.ltrim())?;
Ok((input, (name, value)))
}
pub fn content_type(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8], Vec<(&[u8], &[u8])>)> {
let (input, _type) = take_until("/")(input.ltrim())?;
let (input, _) = tag("/")(input)?;
let (input, _subtype) = is_not(";")(input)?;
let (input, parameters) = many0(content_type_parameter)(input)?;
Ok((input, (_type, _subtype, parameters)))
/*
do_parse!(
_type: take_until!("/") >>
tag!("/") >>
_subtype: is_not!(";") >>
parameters: many0!(complete!(content_type_parameter)) >>
( {
(_type, _subtype, parameters)
} )
));
*/
}
/* Caution: values should be passed through phrase() */
pub fn content_disposition_parameter(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
let (input, _) = tag(";")(input.ltrim())?;
let (input, name) = terminated(take_until("="), tag("="))(input.ltrim())?;
let (input, value) = alt((
delimited(tag("\""), take_until("\""), tag("\"")),
is_not(";"),
))(input.ltrim())?;
Ok((input, (name, value)))
}
pub fn content_disposition(input: &[u8]) -> IResult<&[u8], ContentDisposition> {
let (input, kind) = alt((take_until(";"), take_while(|_| true)))(input.trim())?;
let mut ret = ContentDisposition {
kind: if kind.trim().eq_ignore_ascii_case(b"attachment") {
ContentDispositionKind::Attachment
} else {
ContentDispositionKind::Inline
},
..ContentDisposition::default()
};
if input.is_empty() {
return Ok((input, ret));
}
let (input, parameters) = many0(content_disposition_parameter)(input.ltrim())?;
for (k, v) in parameters {
if k.eq_ignore_ascii_case(b"filename") {
ret.filename =
Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
} else if k.eq_ignore_ascii_case(b"size") {
ret.size =
Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
} else if k.eq_ignore_ascii_case(b"creation-date") {
ret.creation_date =
Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
} else if k.eq_ignore_ascii_case(b"modification-date") {
ret.modification_date =
Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
} else if k.eq_ignore_ascii_case(b"read-date") {
ret.read_date =
Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
} else {
ret.parameter
.push(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into());
}
}
Ok((input, ret))
}
}
pub mod encodings {
use super::*;
use crate::email::attachment_types::Charset;
use data_encoding::BASE64_MIME;
use encoding::all::*;
use encoding::{DecoderTrap, Encoding};
pub fn quoted_printable_byte(input: &[u8]) -> IResult<&[u8], u8> {
if input.len() < 3 {
Err(nom::Err::Error(
(
input,
"quoted_printable_byte(): input too short to be quoted_printable",
)
.into(),
))
} else if input[0] == b'=' && is_hex_digit(input[1]) && is_hex_digit(input[2]) {
let a = if input[1] < b':' {
input[1] - 48
} else if input[1] < b'[' {
input[1] - 55
} else {
input[1] - 87
};
let b = if input[2] < b':' {
input[2] - 48
} else if input[2] < b'[' {
input[2] - 55
} else {
input[2] - 87
};
Ok((&input[3..], a * 16 + b))
} else if input.starts_with(b"\r\n") {
Ok((&input[2..], b'\n'))
} else {
Err(nom::Err::Error(
(input, "quoted_printable_byte(): invalid input").into(),
))
}
}
/* Encoded words
*"=?charset?encoding?encoded text?=".
*/
fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
if input.is_empty() {
return Ok((&[], Vec::with_capacity(0)));
}
if input.len() < 5 {
return Err(nom::Err::Error(
(input, "encoded_word(): input too short to be encoded_word").into(),
));
} else if input[0] != b'=' || input[1] != b'?' {
return Err(nom::Err::Error(
(input, "encoded_word(): invalid input").into(),
));
}
/* find end of Charset tag:
* =?charset?encoding?encoded text?=
* ---------^
*/
let mut tag_end_idx = None;
for (idx, b) in input[2..].iter().enumerate() {
if *b == b'?' {
tag_end_idx = Some(idx + 2);
break;
}
}
if tag_end_idx.is_none() {
return Err(nom::Err::Error(
(input, "encoded_word(): expected end tag").into(),
));
}
let tag_end_idx = tag_end_idx.unwrap();
if tag_end_idx + 2 >= input.len() || input[2 + tag_end_idx] != b'?' {
return Err(nom::Err::Error(
(input, "encoded_word(): expected valid end tag").into(),
));
}
/* See if input ends with "?=" and get ending index
* =?charset?encoding?encoded text?=
* -------------------------------^
*/
let mut encoded_end_idx = None;
for i in (3 + tag_end_idx)..input.len() {
if input[i] == b'?' && i + 1 < input.len() && input[i + 1] == b'=' {
encoded_end_idx = Some(i);
break;
}
}
if encoded_end_idx.is_none() {
return Err(nom::Err::Error(
(input, "encoded_word(): expected input after end tag").into(),
));
}
let encoded_end_idx = encoded_end_idx.unwrap();
let encoded_text = &input[3 + tag_end_idx..encoded_end_idx];
let s: Vec<u8> = match input[tag_end_idx + 1] {
b'b' | b'B' => match BASE64_MIME.decode(encoded_text) {
Ok(v) => v,
Err(_) => encoded_text.to_vec(),
},
b'q' | b'Q' => match quoted_printable_bytes_header(encoded_text) {
Ok((b"", s)) => s,
_ => {
return Err(nom::Err::Error(
(input, "encoded_word(): invalid quoted_printable").into(),
))
}
},
_ => {
return Err(nom::Err::Error(
(input, "encoded_word(): expected 'b|q'").into(),
))
}
};
let charset = Charset::from(&input[2..tag_end_idx]);
if let Charset::UTF8 = charset {
Ok((&input[encoded_end_idx + 2..], s))
} else {
match decode_charset(&s, charset) {
Ok(v) => Ok((&input[encoded_end_idx + 2..], v.into_bytes())),
_ => Err(nom::Err::Error(
(
input,
format!("encoded_word(): unknown charset {:?}", charset),
)
.into(),
)),
}
}
}
pub fn decode_charset(s: &[u8], charset: Charset) -> Result<String> {
match charset {
Charset::UTF8 | Charset::Ascii => Ok(String::from_utf8_lossy(s).to_string()),
Charset::ISO8859_1 => Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_2 => Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_7 => Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_15 => Ok(ISO_8859_15.decode(s, DecoderTrap::Strict)?),
Charset::GBK => Ok(GBK.decode(s, DecoderTrap::Strict)?),
Charset::Windows1250 => Ok(WINDOWS_1250.decode(s, DecoderTrap::Strict)?),
Charset::Windows1251 => Ok(WINDOWS_1251.decode(s, DecoderTrap::Strict)?),
Charset::Windows1252 => Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?),
Charset::Windows1253 => Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?),
// Unimplemented:
Charset::GB2312 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::UTF16 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::BIG5 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::ISO2022JP => Ok(String::from_utf8_lossy(s).to_string()),
}
}
fn quoted_printable_soft_break(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.len() < 2 {
Err(nom::Err::Error(
(input, "quoted_printable_soft_break(): found EOF").into(),
))
} else if input[0] == b'=' && input[1] == b'\n' {
Ok((&input[2..], &input[0..2])) // `=\n` is an escaped space character.
} else if input.len() > 3 && input.starts_with(b"=\r\n") {
Ok((&input[3..], &input[0..3])) // `=\r\n` is an escaped space character.
} else {
Err(nom::Err::Error(
(input, "quoted_printable_soft_break(): invalid input").into(),
))
}
}
pub fn qp_underscore_header(input: &[u8]) -> IResult<&[u8], u8> {
let (rest, _) = tag(b"_")(input)?;
Ok((rest, 0x20))
}
// With MIME, headers in quoted printable format can contain underscores that represent spaces.
// In non-header context, an underscore is just a plain underscore.
pub fn quoted_printable_bytes_header(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
many0(alt((quoted_printable_byte, qp_underscore_header, le_u8)))(input)
}
// For atoms in Header values.
pub fn quoted_printable_bytes(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
many0(alt((
preceded(quoted_printable_soft_break, quoted_printable_byte),
preceded(quoted_printable_soft_break, le_u8),
quoted_printable_byte,
le_u8,
)))(input)
}
pub fn space(input: &[u8]) -> IResult<&[u8], ()> {
let (rest, _) =
take_while(|c: u8| c == b' ' || c == b'\t' || c == b'\r' || c == b'\n')(input)?;
Ok((rest, ()))
//eat_separator!());
}
pub fn encoded_word_list(input: &[u8]) -> IResult<&[u8], SmallVec<[u8; 64]>> {
let (input, list) = separated_nonempty_list(space, encoded_word)(input)?;
let list_len = list.iter().fold(0, |mut acc, x| {
acc += x.len();
acc
});
Ok((
input,
list.iter()
.fold(SmallVec::with_capacity(list_len), |mut acc, x| {
acc.extend(x.into_iter().cloned());
acc
}),
))
}
pub fn ascii_token(input: &[u8]) -> IResult<&[u8], SmallVec<[u8; 64]>> {
let (input, word) = alt((
terminated(take_until(" =?"), peek(preceded(tag(b" "), encoded_word))),
take_while(|_| true),
))(input)?;
Ok((input, SmallVec::from(word)))
}
pub fn phrase(
input: &[u8],
multiline: /* preserve newlines */ bool,
) -> IResult<&[u8], Vec<u8>> {
if input.is_empty() {
return Ok((&[], Vec::with_capacity(0)));
}
let mut input = input.ltrim();
let mut acc: Vec<u8> = Vec::new();
let mut ptr = 0;
while ptr < input.len() {
let mut flag = false;
// Check if word is encoded.
while let Ok((rest, v)) = encoded_word(&input[ptr..]) {
flag = true;
input = rest;
ptr = 0;
acc.extend(v);
// consume whitespace
while ptr < input.len() && (is_whitespace!(input[ptr])) {
ptr += 1;
}
if ptr >= input.len() {
break;
}
}
if flag && ptr < input.len() && ptr != 0 {
acc.push(b' ');
}
let end = input[ptr..].find(b"=?");
let end = end.unwrap_or_else(|| input.len() - ptr) + ptr;
let ascii_s = ptr;
let mut ascii_e = 0;
while ptr < end && !(is_whitespace!(input[ptr])) {
ptr += 1;
}
if !multiline {
ascii_e = ptr;
}
while ptr < input.len() && (is_whitespace!(input[ptr])) {
ptr += 1;
}
if multiline {
ascii_e = ptr;
}
if ptr >= input.len() {
acc.extend(ascii_token(&input[ascii_s..ascii_e])?.1);
break;
}
if ascii_s >= ascii_e {
/* We have the start of an encoded word but not the end, so parse it as ascii */
ascii_e = input[ascii_s..]
.find(b" ")
.unwrap_or_else(|| ascii_s + input[ascii_s..].len());
ptr = ascii_e;
}
if ascii_s >= ascii_e {
return Err(nom::Err::Error(
(input, "phrase(): start of an encoded word but no end").into(),
));
}
acc.extend(ascii_token(&input[ascii_s..ascii_e])?.1);
if ptr != ascii_e {
acc.push(b' ');
}
}
Ok((&input[ptr..], acc))
}
}
pub mod address {
use super::*;
use crate::email::address::*;
use crate::email::parser::generic::cfws;
pub fn display_addr(input: &[u8]) -> IResult<&[u8], Address> {
if input.is_empty() || input.len() < 3 {
Err(nom::Err::Error((input, "display_addr(): EOF").into()))
} else if !is_whitespace!(input[0]) {
let mut display_name = StrBuilder {
offset: 0,
length: 0,
};
let mut flag = false;
for (i, b) in input[0..].iter().enumerate() {
if *b == b'<' {
display_name.length = i.saturating_sub(1); // if i != 0 { i - 1 } else { 0 };
flag = true;
break;
}
}
if !flag {
let (rest, output) = match super::encodings::phrase(input, false) {
Ok(v) => v,
_ => {
return Err(nom::Err::Error(
(input, "display_addr(): no '<' found").into(),
))
}
};
if output.contains(&b'<') {
let (_, address) = match display_addr(&output) {
Ok(v) => v,
_ => {
return Err(nom::Err::Error(
(input, "display_addr(): invalid input").into(),
))
}
};
return Ok((rest, address));
}
return Err(nom::Err::Error(
(input, "display_addr(): invalid input").into(),
));
}
let mut end = input.len();
let mut at_flag = false;
let mut flag = false;
for (i, b) in input[display_name.length + 2..].iter().enumerate() {
match *b {
b'@' => at_flag = true,
b'>' => {
end = i;
flag = true;
break;
}
_ => {}
}
}
if at_flag && flag {
let (_, raw) =
super::encodings::phrase(&input[0..end + display_name.length + 3], false)?;
let display_name_end = raw.find(b"<").unwrap();
display_name.length = raw[0..display_name_end].trim().len();
let address_spec = if display_name_end == 0 {
StrBuilder {
offset: 1,
length: end + 1,
}
} else {
StrBuilder {
offset: display_name_end + 1,
length: end,
}
};
if display_name.display(&raw).as_bytes().is_quoted() {
display_name.offset += 1;
display_name.length -= 2;
}
let rest_start = if input.len() > end + display_name.length + 2 {
end + display_name.length + 3
} else {
end + display_name.length + 2
};
Ok((
input.get(rest_start..).unwrap_or_default(),
Address::Mailbox(MailboxAddress {
raw,
display_name,
address_spec,
}),
))
} else {
Err(nom::Err::Error(
(input, "display_addr(): did not find both '@' and '>'").into(),
))
}
} else {
Err(nom::Err::Error(
(input, "display_addr(): unexpected whitespace").into(),
))
}
}
///`angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr`
fn angle_addr(input: &[u8]) -> IResult<&[u8], Address> {
let (input, _) = opt(cfws)(input)?;
let (input, _) = tag("<")(input)?;
let (input, addr_spec) = addr_spec(input)?;
let (input, _) = tag(">")(input)?;
let (input, _) = opt(cfws)(input)?;
Ok((input, addr_spec))
}
///`atom = [CFWS] 1*atext [CFWS]`
fn atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
let mut i = 0;
while i < input.len() {
//&& !input[i].is_ascii_whitespace() {
match input[i] {
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b':' | b';' | b'@' | b'\\' | b','
| b'.' | b'\r' | b'\n' | b'"' => break,
_ => {}
}
i += 1;
}
if i == 0 {
return Err(nom::Err::Error(
(input, "atom(): starts with whitespace or empty").into(),
));
}
while i + 1 > 0 {
if input[i - 1] == b' ' || input[i - 1] == b'\t' {
i -= 1;
} else {
break;
}
}
let (rest, opt_space2) = opt(cfws)(&input[i..])?;
let ret = if opt_space.is_some() || opt_space2.is_some() {
let mut ret = Vec::with_capacity(i + 2);
if let Some(opt_space) = opt_space {
ret.extend_from_slice(&opt_space);
}
ret.extend_from_slice(&input[..i]);
if let Some(opt_space) = opt_space2 {
ret.extend_from_slice(&opt_space);
}
Cow::Owned(ret)
} else {
Cow::Borrowed(&input[..i])
};
Ok((rest, ret))
}
///`quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]`
fn quoted_string(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
if !input.starts_with(b"\"") {
return Err(nom::Err::Error(
(input, "quoted_string(): doesn't start with DQUOTE").into(),
));
}
let input = &input[1..];
let mut i = 0;
while i < input.len() && input[i] != b'"' {
if opt_space.is_some() || (input[i..].starts_with(b"\\") && i + 1 < input.len()) {
let mut ret = if let Some(opt_space) = opt_space {
let mut r = Vec::with_capacity(2 * i);
r.extend_from_slice(&opt_space);
r
} else {
Vec::with_capacity(2 * i)
};
ret.extend_from_slice(&input[..i]);
i += 1;
ret.push(input[i]);
i += 1;
while i < input.len() && input[i] != b'"' {
if input[i..].starts_with(b"\\") && i + 1 < input.len() {
i += 1;
}
ret.push(input[i]);
i += 1;
}
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
ret.extend_from_slice(&opt_sp);
}
let ret = Cow::Owned(ret);
return Ok((rest, ret));
}
i += 1;
}
let ret = Cow::Borrowed(&input[..i]);
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
let mut ret = ret.to_vec();
ret.extend_from_slice(&opt_sp);
Ok((rest, Cow::Owned(ret)))
} else {
Ok((rest, ret))
}
}
///`word = atom / quoted-string`
fn word(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
alt((quoted_string, atom))(input)
}
///`phrase = 1*word / obs-phrase`
fn phrase(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (rest, words) = many1(word)(input)?;
let len = words.iter().map(|v| v.len()).sum::<usize>();
let mut ret = words
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, el| {
acc.extend_from_slice(&el);
acc
});
let right_wsp_padding = ret.len() - ret.rtrim().len();
for _ in 0..right_wsp_padding {
ret.pop();
}
Ok((rest, ret))
}
#[test]
fn test_phrase() {
let s = b"\"Jeffrey \\\"fejj\\\" Stedfast\""; // <fejj@helixcode.com>"
assert_eq!(to_str!(&phrase(s).unwrap().1), "Jeffrey \"fejj\" Stedfast");
}
///`addr-spec = local-part "@" domain`
pub fn addr_spec(input: &[u8]) -> IResult<&[u8], Address> {
///`atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"`
fn atext(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "atext(): empty input").into()));
}
if input[0].is_ascii_alphanumeric()
|| [
b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'/', b'=', b'?', b'^',
b'_', b'`', b'{', b'|', b'}', b'~',
]
.contains(&input[0])
{
Ok((&input[1..], input[0]))
} else {
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
}
}
///dot-atom-text = 1*atext *("." 1*atext)
///dot-atom = [CFWS] dot-atom-text [CFWS]
fn dot_atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (mut input, _) = opt(cfws)(input)?;
let mut ret = vec![];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): starts with at least one atext").into(),
));
}
loop {
if !input.starts_with(b".") {
break;
}
ret.push(b'.');
input = &input[1..];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): DOT followed with at least one atext").into(),
));
}
}
let (input, _) = opt(cfws)(input)?;
Ok((input, ret.into()))
}
///`obs-domain = atom *("." atom)`
fn obs_domain(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (mut input, atom_) = context("obs_domain", atom)(input)?;
let mut ret: Vec<u8> = atom_.into();