diff --git a/crates/kumod/src/http_server/inject_v1.rs b/crates/kumod/src/http_server/inject_v1.rs index 3a3e9d61..775db3e7 100644 --- a/crates/kumod/src/http_server/inject_v1.rs +++ b/crates/kumod/src/http_server/inject_v1.rs @@ -713,7 +713,7 @@ impl InjectV1Request { let opts = mailparsing::AttachmentOptions { file_name: a.file_name.clone().map(|s| s.into()), inline: a.content_id.is_some(), - content_id: a.content_id.clone(), + content_id: a.content_id.clone().map(Into::into), }; let decoded_data; diff --git a/crates/mailparsing/src/header.rs b/crates/mailparsing/src/header.rs index 52d7c106..5e19e9b7 100644 --- a/crates/mailparsing/src/header.rs +++ b/crates/mailparsing/src/header.rs @@ -184,57 +184,57 @@ impl<'a> Header<'a> { } pub fn as_content_transfer_encoding(&self) -> Result { - Parser::parse_content_transfer_encoding_header(self.get_raw_value_string()?) + Parser::parse_content_transfer_encoding_header(self.get_raw_value()) } pub fn as_content_disposition(&self) -> Result { - Parser::parse_content_transfer_encoding_header(self.get_raw_value_string()?) + Parser::parse_content_transfer_encoding_header(self.get_raw_value()) } pub fn as_content_type(&self) -> Result { - Parser::parse_content_type_header(self.get_raw_value_string()?) + Parser::parse_content_type_header(self.get_raw_value()) } /// Parse the header into a mailbox-list (as defined in /// RFC 5322), which is how the `From` and `Resent-From`, /// headers are defined. pub fn as_mailbox_list(&self) -> Result { - Parser::parse_mailbox_list_header(self.get_raw_value_string()?) + Parser::parse_mailbox_list_header(self.get_raw_value()) } /// Parse the header into a mailbox (as defined in /// RFC 5322), which is how the `Sender` and `Resent-Sender` /// headers are defined. pub fn as_mailbox(&self) -> Result { - Parser::parse_mailbox_header(self.get_raw_value_string()?) + Parser::parse_mailbox_header(self.get_raw_value()) } pub fn as_address_list(&self) -> Result { - Parser::parse_address_list_header(self.get_raw_value_string()?) + Parser::parse_address_list_header(self.get_raw_value()) } pub fn as_message_id(&self) -> Result { - Parser::parse_msg_id_header(self.get_raw_value_string()?) + Parser::parse_msg_id_header(self.get_raw_value()) } pub fn as_content_id(&self) -> Result { - Parser::parse_content_id_header(self.get_raw_value_string()?) + Parser::parse_content_id_header(self.get_raw_value()) } pub fn as_message_id_list(&self) -> Result> { - Parser::parse_msg_id_header_list(self.get_raw_value_string()?) + Parser::parse_msg_id_header_list(self.get_raw_value()) } pub fn as_unstructured(&self) -> Result { - Parser::parse_unstructured_header(self.get_raw_value_string()?) + Parser::parse_unstructured_header(self.get_raw_value()) } pub fn as_authentication_results(&self) -> Result { - Parser::parse_authentication_results_header(self.get_raw_value_string()?) + Parser::parse_authentication_results_header(self.get_raw_value()) } pub fn as_arc_authentication_results(&self) -> Result { - Parser::parse_arc_authentication_results_header(self.get_raw_value_string()?) + Parser::parse_arc_authentication_results_header(self.get_raw_value()) } pub fn as_date(&self) -> Result> { diff --git a/crates/mailparsing/src/mimepart.rs b/crates/mailparsing/src/mimepart.rs index 87676256..8dbdc03f 100644 --- a/crates/mailparsing/src/mimepart.rs +++ b/crates/mailparsing/src/mimepart.rs @@ -834,7 +834,7 @@ impl<'a> MimePart<'a> { headers.set_content_disposition(cd)?; if let Some(id) = &opts.content_id { - headers.set_content_id(MessageID(id.to_string()))?; + headers.set_content_id(MessageID(id.clone()))?; } } @@ -1142,7 +1142,7 @@ impl<'a> MimePart<'a> { if to_fix.contains(MessageConformance::MISSING_MESSAGE_ID_HEADER) { if let Some(message_id) = &settings.message_id { msg.headers_mut() - .set_message_id(MessageID(message_id.clone()))?; + .set_message_id(MessageID(message_id.clone().into()))?; } } @@ -1243,7 +1243,7 @@ pub struct AttachmentOptions { #[serde(default)] pub inline: bool, #[serde(default)] - pub content_id: Option, + pub content_id: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/mailparsing/src/nom_utils.rs b/crates/mailparsing/src/nom_utils.rs index 64270d61..8d9ef671 100644 --- a/crates/mailparsing/src/nom_utils.rs +++ b/crates/mailparsing/src/nom_utils.rs @@ -1,11 +1,12 @@ +use bstr::ByteSlice; use nom::error::{ContextError, ErrorKind}; use nom_locate::LocatedSpan; use std::fmt::{Debug, Write}; -pub(crate) type Span<'a> = LocatedSpan<&'a str>; +pub(crate) type Span<'a> = LocatedSpan<&'a [u8]>; pub(crate) type IResult<'a, A, B> = nom::IResult>>; -pub fn make_span(s: &'_ str) -> Span<'_> { +pub fn make_span(s: &'_ [u8]) -> Span<'_> { Span::new(s) } @@ -81,20 +82,22 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { match err { nom::Err::Error(e) => { let mut result = String::new(); - for (i, (span, kind)) in e.errors.iter().enumerate() { + let mut lines_shown = vec![]; + + for (span, kind) in e.errors.iter() { if input.is_empty() { match kind { ParseErrorKind::Char(c) => { - write!(&mut result, "{i}: expected '{c}', got empty input\n\n") + write!(&mut result, "Error expected '{c}', got empty input\n\n") } ParseErrorKind::Context(s) => { - write!(&mut result, "{i}: in {s}, got empty input\n\n") + write!(&mut result, "Error in {s}, got empty input\n\n") } ParseErrorKind::External { kind, reason } => { - write!(&mut result, "{i}: {reason} {kind:?}, got empty input\n\n") + write!(&mut result, "Error {reason} {kind:?}, got empty input\n\n") } ParseErrorKind::Nom(e) => { - write!(&mut result, "{i}: in {e:?}, got empty input\n\n") + write!(&mut result, "Error in {e:?}, got empty input\n\n") } } .ok(); @@ -102,21 +105,33 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } let line_number = span.location_line(); - let line = std::str::from_utf8(span.get_line_beginning()) - .unwrap_or(""); + let input_line = span.get_line_beginning(); // Remap \t in particular, because it can render as multiple // columns and defeat the column number calculation provided // by the Span type - let line: String = line - .chars() - .map(|c| match c { + let mut line = String::new(); + for (start, end, c) in input_line.char_indices() { + let c = match c { '\t' => '\u{2409}', '\r' => '\u{240d}', '\n' => '\u{240a}', - _ => c, - }) - .collect(); + c => c, + }; + + if c == std::char::REPLACEMENT_CHARACTER { + let bytes = &input_line[start..end]; + for b in bytes.iter() { + line.push_str(&format!("\\x{b:02X}")); + } + } else { + line.push(c); + } + } + let column = span.get_utf8_column(); + + lines_shown.push(line_number); + let mut caret = " ".repeat(column.saturating_sub(1)); caret.push('^'); for _ in 1..span.fragment().len() { @@ -128,7 +143,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { if let Some(actual) = span.fragment().chars().next() { write!( &mut result, - "{i}: at line {line_number}:\n\ + "Error at line {line_number}:\n\ {line}\n\ {caret}\n\ expected '{expected}', found {actual}\n\n", @@ -136,7 +151,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } else { write!( &mut result, - "{i}: at line {line_number}:\n\ + "Error at line {line_number}:\n\ {line}\n\ {caret}\n\ expected '{expected}', got end of input\n\n", @@ -144,17 +159,12 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } } ParseErrorKind::Context(context) => { - write!( - &mut result, - "{i}: at line {line_number}, in {context}:\n\ - {line}\n\ - {caret}\n\n", - ) + write!(&mut result, "while parsing {context}\n") } - ParseErrorKind::External { kind, reason } => { + ParseErrorKind::External { kind: _, reason } => { write!( &mut result, - "{i}: at line {line_number}, {reason} {kind:?}:\n\ + "Error at line {line_number}, {reason}:\n\ {line}\n\ {caret}\n\n", ) @@ -162,7 +172,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { ParseErrorKind::Nom(nom_err) => { write!( &mut result, - "{i}: at line {line_number}, in {nom_err:?}:\n\ + "Error at line {line_number}, in {nom_err:?}:\n\ {line}\n\ {caret}\n\n", ) diff --git a/crates/mailparsing/src/rfc5322_parser.rs b/crates/mailparsing/src/rfc5322_parser.rs index 00d6a019..82e16c76 100644 --- a/crates/mailparsing/src/rfc5322_parser.rs +++ b/crates/mailparsing/src/rfc5322_parser.rs @@ -4,16 +4,16 @@ use crate::{MailParsingError, Result, SharedString}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use charset_normalizer_rs::Encoding; use nom::branch::alt; -use nom::bytes::complete::{tag, take_while, take_while1}; -use nom::character::complete::{char, satisfy}; +use nom::bytes::complete::{take_while, take_while1, take_while_m_n}; use nom::combinator::{all_consuming, map, opt, recognize}; use nom::error::context; use nom::multi::{many0, many1, separated_list1}; use nom::sequence::{delimited, preceded, separated_pair, terminated}; -use nom::Parser as _; +use nom::{Compare, Input, Parser as _}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::Debug; +use std::marker::PhantomData; impl MailParsingError { pub fn from_nom(input: Span, err: nom::Err>>) -> Self { @@ -21,55 +21,119 @@ impl MailParsingError { } } -fn is_utf8_non_ascii(c: char) -> bool { - let c = c as u32; +/// Like nom::bytes::complete::tag, except that we print what the tag +/// was expecting if there was an error. +/// I feel like this should be the default behavior TBH. +fn tag(tag: &'static str) -> TagParser { + TagParser { + tag, + e: PhantomData, + } +} + +/// Struct to support displaying better errors for tag() +struct TagParser { + tag: &'static str, + e: PhantomData, +} + +/// All this fuss to show what we expected for the TagParser impl +impl + nom::error::FromExternalError> nom::Parser + for TagParser +where + I: Input + Compare<&'static str> + nom::AsBytes, +{ + type Output = I; + type Error = Error; + + fn process( + &mut self, + i: I, + ) -> nom::PResult { + use nom::error::ErrorKind; + use nom::{CompareResult, Err, Mode}; + + let tag_len = self.tag.input_len(); + + match i.compare(self.tag) { + CompareResult::Ok => Ok((i.take_from(tag_len), OM::Output::bind(|| i.take(tag_len)))), + CompareResult::Incomplete => Err(Err::Error(OM::Error::bind(|| { + Error::from_external_error( + i, + ErrorKind::Fail, + format!( + "expected \"{}\" but ran out of input", + self.tag.escape_debug() + ), + ) + }))), + + CompareResult::Error => { + let available = i.take(i.input_len().min(tag_len)); + Err(Err::Error(OM::Error::bind(|| { + Error::from_external_error( + i, + ErrorKind::Fail, + format!( + "expected \"{}\" but found {:?}", + self.tag.escape_debug(), + BStr::new(available.as_bytes()) + ), + ) + }))) + } + } + } +} + +fn is_utf8_non_ascii(c: u8) -> bool { c == 0 || c >= 0x80 } // ctl = { '\u{00}'..'\u{1f}' | "\u{7f}" } -fn is_ctl(c: char) -> bool { +fn is_ctl(c: u8) -> bool { match c { - '\u{00}'..='\u{1f}' | '\u{7f}' => true, + b'\x00'..=b'\x1f' | b'\x7f' => true, _ => false, } } -fn not_angle(c: char) -> bool { +fn not_angle(c: u8) -> bool { match c { - '<' | '>' => false, + b'<' | b'>' => false, _ => true, } } // char = { '\u{01}'..'\u{7f}' } -fn is_char(c: char) -> bool { +fn is_char(c: u8) -> bool { match c { - '\u{01}'..='\u{ff}' => true, + 0x01..=0xff => true, _ => false, } } -fn is_especial(c: char) -> bool { +fn is_especial(c: u8) -> bool { match c { - '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '/' | '[' | ']' | '?' | '.' | '=' => true, + b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'/' | b'[' | b']' | b'?' + | b'.' | b'=' => true, _ => false, } } -fn is_token(c: char) -> bool { - is_char(c) && c != ' ' && !is_especial(c) && !is_ctl(c) +fn is_token(c: u8) -> bool { + is_char(c) && c != b' ' && !is_especial(c) && !is_ctl(c) } // vchar = { '\u{21}'..'\u{7e}' | utf8_non_ascii } -fn is_vchar(c: char) -> bool { - let u = c as u32; - (0x21..=0x7e).contains(&u) || is_utf8_non_ascii(c) +fn is_vchar(c: u8) -> bool { + (0x21..=0x7e).contains(&c) || is_utf8_non_ascii(c) } -fn is_atext(c: char) -> bool { +fn is_atext(c: u8) -> bool { match c { - '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' - | '`' | '{' | '|' | '}' | '~' => true, + b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?' + | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' => true, c => c.is_ascii_alphanumeric() || is_utf8_non_ascii(c), } } @@ -78,64 +142,64 @@ fn atext(input: Span) -> IResult { context("atext", take_while1(is_atext)).parse(input) } -fn is_obs_no_ws_ctl(c: char) -> bool { +fn is_obs_no_ws_ctl(c: u8) -> bool { match c { - '\u{01}'..='\u{08}' | '\u{0b}'..='\u{0c}' | '\u{0e}'..='\u{1f}' | '\u{7f}' => true, + 0x01..=0x08 | 0x0b..=0x0c | 0x0e..=0x1f | 0x7f => true, _ => false, } } -fn is_obs_ctext(c: char) -> bool { +fn is_obs_ctext(c: u8) -> bool { is_obs_no_ws_ctl(c) } // ctext = { '\u{21}'..'\u{27}' | '\u{2a}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_ctext | utf8_non_ascii } -fn is_ctext(c: char) -> bool { +fn is_ctext(c: u8) -> bool { match c { - '\u{21}'..='\u{27}' | '\u{2a}'..='\u{5b}' | '\u{5d}'..='\u{7e}' => true, + 0x21..=0x27 | 0x2a..=0x5b | 0x5d..=0x7e => true, c => is_obs_ctext(c) || is_utf8_non_ascii(c), } } // dtext = { '\u{21}'..'\u{5a}' | '\u{5e}'..'\u{7e}' | obs_dtext | utf8_non_ascii } // obs_dtext = { obs_no_ws_ctl | quoted_pair } -fn is_dtext(c: char) -> bool { +fn is_dtext(c: u8) -> bool { match c { - '\u{21}'..='\u{5a}' | '\u{5e}'..='\u{7e}' => true, + 0x21..=0x5a | 0x5e..=0x7e => true, c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c), } } // qtext = { "\u{21}" | '\u{23}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_qtext | utf8_non_ascii } // obs_qtext = { obs_no_ws_ctl } -fn is_qtext(c: char) -> bool { +fn is_qtext(c: u8) -> bool { match c { - '\u{21}' | '\u{23}'..='\u{5b}' | '\u{5d}'..='\u{7e}' => true, + 0x21 | 0x23..=0x5b | 0x5d..=0x7e => true, c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c), } } -fn is_tspecial(c: char) -> bool { +fn is_tspecial(c: u8) -> bool { match c { - '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' - | '=' => true, + b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'\\' | b'"' | b'/' | b'[' + | b']' | b'?' | b'=' => true, _ => false, } } -fn is_attribute_char(c: char) -> bool { +fn is_attribute_char(c: u8) -> bool { match c { - ' ' | '*' | '\'' | '%' => false, + b' ' | b'*' | b'\'' | b'%' => false, _ => is_char(c) && !is_ctl(c) && !is_tspecial(c), } } fn wsp(input: Span) -> IResult { - context("wsp", take_while1(|c| c == ' ' || c == '\t')).parse(input) + context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input) } fn newline(input: Span) -> IResult { - context("newline", recognize(preceded(opt(char('\r')), char('\n')))).parse(input) + context("newline", recognize(preceded(opt(tag("\r")), tag("\n")))).parse(input) } // fws = { ((wsp* ~ "\r"? ~ "\n")* ~ wsp+) | obs_fws } @@ -163,7 +227,7 @@ fn obs_fws(input: Span) -> IResult { fn mailbox_list(input: Span) -> IResult { let (loc, mailboxes) = context( "mailbox_list", - alt((separated_list1(char(','), mailbox), obs_mbox_list)), + alt((separated_list1(tag(","), mailbox), obs_mbox_list)), ) .parse(input)?; Ok((loc, MailboxList(mailboxes))) @@ -174,11 +238,11 @@ fn obs_mbox_list(input: Span) -> IResult> { let (loc, entries) = context( "obs_mbox_list", many1(preceded( - many0(preceded(opt(cfws), char(','))), + many0(preceded(opt(cfws), tag(","))), ( mailbox, many0(preceded( - char(','), + tag(","), alt((map(mailbox, Some), map(cfws, |_| None))), )), ), @@ -221,7 +285,7 @@ fn address_list(input: Span) -> IResult { context( "address_list", alt(( - map(separated_list1(char(','), address), AddressList), + map(separated_list1(tag(","), address), AddressList), obs_address_list, )), ) @@ -233,11 +297,11 @@ fn obs_address_list(input: Span) -> IResult { let (loc, entries) = context( "obs_address_list", many1(preceded( - many0(preceded(opt(cfws), char(','))), + many0(preceded(opt(cfws), tag(","))), ( address, many0(preceded( - char(','), + tag(","), alt((map(address, Some), map(cfws, |_| None))), )), ), @@ -269,7 +333,7 @@ fn group(input: Span) -> IResult { let (loc, (name, _, group_list, _)) = context( "group", terminated( - (display_name, char(':'), opt(group_list), char(';')), + (display_name, tag(":"), opt(group_list), tag(";")), opt(cfws), ), ) @@ -301,7 +365,7 @@ fn obs_group_list(input: Span) -> IResult { context( "obs_group_list", map( - terminated(many1(preceded(opt(cfws), char(','))), opt(cfws)), + terminated(many1(preceded(opt(cfws), tag(","))), opt(cfws)), |_| MailboxList(vec![]), ), ) @@ -336,7 +400,7 @@ fn phrase(input: Span) -> IResult { map(cfws, |_| None), map(encoded_word, Option::Some), map(word, Option::Some), - map(char('.'), |_dot| Some(BString::from("."))), + map(tag("."), |_dot| Some(BString::from("."))), ))), ), ) @@ -358,7 +422,7 @@ fn angle_addr(input: Span) -> IResult { alt(( delimited( opt(cfws), - delimited(char('<'), addr_spec, char('>')), + delimited(tag("<"), addr_spec, tag(">")), opt(cfws), ), obs_angle_addr, @@ -373,7 +437,7 @@ fn obs_angle_addr(input: Span) -> IResult { "obs_angle_addr", delimited( opt(cfws), - delimited(char('<'), preceded(obs_route, addr_spec), char('>')), + delimited(tag("<"), preceded(obs_route, addr_spec), tag(">")), opt(cfws), ), ) @@ -387,12 +451,12 @@ fn obs_route(input: Span) -> IResult { "obs_route", recognize(terminated( ( - many0(alt((cfws, recognize(char(','))))), - recognize(char('@')), + many0(alt((cfws, recognize(tag(","))))), + recognize(tag("@")), recognize(domain), - many0((char(','), opt(cfws), opt((char('@'), domain)))), + many0((tag(","), opt(cfws), opt((tag("@"), domain)))), ), - char(':'), + tag(":"), )), ) .parse(input) @@ -401,7 +465,7 @@ fn obs_route(input: Span) -> IResult { // addr_spec = { local_part ~ "@" ~ domain } fn addr_spec(input: Span) -> IResult { let (loc, (local_part, domain)) = - context("addr_spec", separated_pair(local_part, char('@'), domain)).parse(input)?; + context("addr_spec", separated_pair(local_part, tag("@"), domain)).parse(input)?; Ok(( loc, AddrSpec { @@ -411,7 +475,7 @@ fn addr_spec(input: Span) -> IResult { )) } -fn parse_with<'a, R, F>(text: &'a str, parser: F) -> Result +fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result where F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>, { @@ -426,7 +490,7 @@ where #[test] fn test_addr_spec() { k9::snapshot!( - parse_with("darth.vader@a.galaxy.far.far.away", addr_spec), + parse_with("darth.vader@a.galaxy.far.far.away".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -438,7 +502,10 @@ Ok( ); k9::snapshot!( - parse_with("\"darth.vader\"@a.galaxy.far.far.away", addr_spec), + parse_with( + "\"darth.vader\"@a.galaxy.far.far.away".as_bytes(), + addr_spec + ), r#" Ok( AddrSpec { @@ -450,19 +517,18 @@ Ok( ); k9::snapshot!( - parse_with("\"darth\".vader@a.galaxy.far.far.away", addr_spec), + parse_with( + "\"darth\".vader@a.galaxy.far.far.away".as_bytes(), + addr_spec + ), r#" Err( HeaderParse( - "0: at line 1: + "Error at line 1, expected "@" but found ".": "darth".vader@a.galaxy.far.far.away ^___________________________ -expected '@', found . - -1: at line 1, in addr_spec: -"darth".vader@a.galaxy.far.far.away -^__________________________________ +while parsing addr_spec ", ), ) @@ -470,7 +536,7 @@ expected '@', found . ); k9::snapshot!( - parse_with("a@[127.0.0.1]", addr_spec), + parse_with("a@[127.0.0.1]".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -482,7 +548,7 @@ Ok( ); k9::snapshot!( - parse_with("a@[IPv6::1]", addr_spec), + parse_with("a@[IPv6::1]".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -508,11 +574,11 @@ fn word(input: Span) -> IResult { // obs_local_part = { word ~ (dot ~ word)* } fn obs_local_part(input: Span) -> IResult { let (loc, (word, dotted_words)) = - context("obs_local_part", (word, many0((char('.'), word)))).parse(input)?; + context("obs_local_part", (word, many0((tag("."), word)))).parse(input)?; let mut result = word; - for (dot, w) in dotted_words { - result.push_char(dot); + for (_dot, w) in dotted_words { + result.push(b'.'); result.push_str(&w); } @@ -532,11 +598,11 @@ fn domain(input: Span) -> IResult { // obs_domain = { atom ~ ( dot ~ atom)* } fn obs_domain(input: Span) -> IResult { let (loc, (atom, dotted_atoms)) = - context("obs_domain", (atom, many0((char('.'), atom)))).parse(input)?; + context("obs_domain", (atom, many0((tag("."), atom)))).parse(input)?; let mut result = atom; - for (dot, w) in dotted_atoms { - result.push_char(dot); + for (_dot, w) in dotted_atoms { + result.push(b'.'); result.push_str(&w); } @@ -550,12 +616,12 @@ fn domain_literal(input: Span) -> IResult { delimited( opt(cfws), delimited( - char('['), + tag("["), ( - many0((opt(fws), alt((satisfy(is_dtext), quoted_pair)))), + many0((opt(fws), alt((take_while_m_n(1, 1, is_dtext), quoted_pair)))), opt(fws), ), - char(']'), + tag("]"), ), opt(cfws), ), @@ -568,7 +634,7 @@ fn domain_literal(input: Span) -> IResult { if let Some(a) = a { result.push_str(&a); } - result.push_char(b); + result.push_str(b); } if let Some(t) = trailer { result.push_str(&t); @@ -580,7 +646,7 @@ fn domain_literal(input: Span) -> IResult { // dot_atom_text = @{ atext ~ ("." ~ atext)* } fn dot_atom_text(input: Span) -> IResult { let (loc, (a, b)) = - context("dot_atom_text", (atext, many0(preceded(char('.'), atext)))).parse(input)?; + context("dot_atom_text", (atext, many0(preceded(tag("."), atext)))).parse(input)?; let mut result: BString = (*a).into(); for item in b { result.push(b'.'); @@ -599,7 +665,7 @@ fn dot_atom(input: Span) -> IResult { #[test] fn test_dot_atom() { k9::snapshot!( - parse_with("hello", dot_atom), + parse_with("hello".as_bytes(), dot_atom), r#" Ok( "hello", @@ -608,7 +674,7 @@ Ok( ); k9::snapshot!( - parse_with("hello.there", dot_atom), + parse_with("hello.there".as_bytes(), dot_atom), r#" Ok( "hello.there", @@ -617,11 +683,11 @@ Ok( ); k9::snapshot!( - parse_with("hello.", dot_atom), + parse_with("hello.".as_bytes(), dot_atom), r#" Err( HeaderParse( - "0: at line 1, in Eof: + "Error at line 1, in Eof: hello. ^ @@ -632,7 +698,7 @@ hello. ); k9::snapshot!( - parse_with("(wat)hello", dot_atom), + parse_with("(wat)hello".as_bytes(), dot_atom), r#" Ok( "hello", @@ -657,7 +723,7 @@ fn cfws(input: Span) -> IResult { fn comment(input: Span) -> IResult { context( "comment", - recognize((char('('), many0((opt(fws), ccontent)), opt(fws), char(')'))), + recognize((tag("("), many0((opt(fws), ccontent)), opt(fws), tag(")"))), ) .parse(input) } @@ -666,17 +732,8 @@ fn comment(input: Span) -> IResult { #[test] fn test_comment() { k9::snapshot!( - parse_with("(wat)", comment), - r#" -Ok( - LocatedSpan { - offset: 0, - line: 1, - fragment: "(wat)", - extra: (), - }, -) -"# + BStr::new(&parse_with("(wat)".as_bytes(), comment).unwrap()), + "(wat)" ); } @@ -685,7 +742,7 @@ fn ccontent(input: Span) -> IResult { context( "ccontent", recognize(alt(( - recognize(satisfy(is_ctext)), + recognize(take_while_m_n(1, 1, is_ctext)), recognize(quoted_pair), comment, recognize(encoded_word), @@ -694,17 +751,21 @@ fn ccontent(input: Span) -> IResult { .parse(input) } -fn is_quoted_pair(c: char) -> bool { +fn is_quoted_pair(c: u8) -> bool { match c { - '\u{00}' | '\r' | '\n' | ' ' => true, + 0x00 | b'\r' | b'\n' | b' ' => true, c => is_obs_no_ws_ctl(c) || is_vchar(c), } } // quoted_pair = { ( "\\" ~ (vchar | wsp)) | obs_qp } // obs_qp = { "\\" ~ ( "\u{00}" | obs_no_ws_ctl | "\r" | "\n") } -fn quoted_pair(input: Span) -> IResult { - context("quoted_pair", preceded(char('\\'), satisfy(is_quoted_pair))).parse(input) +fn quoted_pair(input: Span) -> IResult { + context( + "quoted_pair", + preceded(tag("\\"), take_while_m_n(1, 1, is_quoted_pair)), + ) + .parse(input) } // encoded_word = { "=?" ~ charset ~ ("*" ~ language)? ~ "?" ~ encoding ~ "?" ~ encoded_text ~ "?=" } @@ -715,10 +776,10 @@ fn encoded_word(input: Span) -> IResult { tag("=?"), ( charset, - opt(preceded(char('*'), language)), - char('?'), + opt(preceded(tag("*"), language)), + tag("?"), encoding, - char('?'), + tag("?"), encoded_text, ), tag("?="), @@ -727,7 +788,7 @@ fn encoded_word(input: Span) -> IResult { .parse(input)?; let bytes = match *encoding.fragment() { - "B" | "b" => data_encoding::BASE64_MIME + b"B" | b"b" => data_encoding::BASE64_MIME .decode(text.as_bytes()) .map_err(|err| { make_context_error( @@ -735,12 +796,12 @@ fn encoded_word(input: Span) -> IResult { format!("encoded_word: base64 decode failed: {err:#}"), ) })?, - "Q" | "q" => { + b"Q" | b"q" => { // for rfc2047 header encoding, _ can be used to represent a space let munged = text.replace("_", " "); // The quoted_printable crate will unhelpfully strip trailing space // from the decoded input string, and we must track and restore it - let had_trailing_space = munged.ends_with(' '); + let had_trailing_space = munged.ends_with_str(" "); let mut decoded = quoted_printable::decode(munged, quoted_printable::ParseMode::Robust) .map_err(|err| { make_context_error( @@ -754,6 +815,7 @@ fn encoded_word(input: Span) -> IResult { decoded } encoding => { + let encoding = BStr::new(encoding); return Err(make_context_error( input, format!( @@ -763,17 +825,27 @@ fn encoded_word(input: Span) -> IResult { } }; - let charset = Encoding::by_name(&*charset).ok_or_else(|| { + let charset_name = charset.to_str().map_err(|err| { make_context_error( input, - format!("encoded_word: unsupported charset '{charset}'"), + format!( + "encoded_word: charset {} is not UTF-8: {err}", + BStr::new(*charset) + ), + ) + })?; + + let charset = Encoding::by_name(&*charset_name).ok_or_else(|| { + make_context_error( + input, + format!("encoded_word: unsupported charset '{charset_name}'"), ) })?; let decoded = charset.decode_simple(&bytes).map_err(|err| { make_context_error( input, - format!("encoded_word: failed to decode as '{charset}': {err}"), + format!("encoded_word: failed to decode as '{charset_name}': {err}"), ) })?; @@ -782,24 +854,24 @@ fn encoded_word(input: Span) -> IResult { // charset = @{ (!"*" ~ token)+ } fn charset(input: Span) -> IResult { - context("charset", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("charset", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // language = @{ token+ } fn language(input: Span) -> IResult { - context("language", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("language", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // encoding = @{ token+ } fn encoding(input: Span) -> IResult { - context("encoding", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("encoding", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // encoded_text = @{ (!( " " | "?") ~ vchar)+ } fn encoded_text(input: Span) -> IResult { context( "encoded_text", - take_while1(|c| is_vchar(c) && c != ' ' && c != '?'), + take_while1(|c| is_vchar(c) && c != b' ' && c != b'?'), ) .parse(input) } @@ -811,9 +883,9 @@ fn quoted_string(input: Span) -> IResult { delimited( opt(cfws), delimited( - char('"'), + tag("\""), (many0((opt(fws), qcontent)), opt(fws)), - char('"'), + tag("\""), ), opt(cfws), ), @@ -825,7 +897,7 @@ fn quoted_string(input: Span) -> IResult { if let Some(a) = a { result.push_str(&a); } - result.push_char(b); + result.push_str(b); } if let Some(t) = trailer { result.push_str(&t); @@ -834,8 +906,12 @@ fn quoted_string(input: Span) -> IResult { } // qcontent = { qtext | quoted_pair } -fn qcontent(input: Span) -> IResult { - context("qcontent", alt((satisfy(is_qtext), quoted_pair))).parse(input) +fn qcontent(input: Span) -> IResult { + context( + "qcontent", + alt((take_while_m_n(1, 1, is_qtext), quoted_pair)), + ) + .parse(input) } fn content_id(input: Span) -> IResult { @@ -852,14 +928,19 @@ fn relaxed_msg_id(input: Span) -> IResult { let (loc, id) = context( "msg_id", delimited( - preceded(opt(cfws), char('<')), - many0(satisfy(not_angle)), - preceded(char('>'), opt(cfws)), + preceded(opt(cfws), tag("<")), + many0(take_while_m_n(1, 1, not_angle)), + preceded(tag(">"), opt(cfws)), ), ) .parse(input)?; - Ok((loc, MessageID(id.into_iter().collect()))) + let mut result = BString::default(); + for item in id.into_iter() { + result.push_str(*item); + } + + Ok((loc, MessageID(result))) } // msg_id_list = { msg_id+ } @@ -896,14 +977,18 @@ fn strict_msg_id(input: Span) -> IResult { let (loc, (left, _, right)) = context( "msg_id", delimited( - preceded(opt(cfws), char('<')), - (id_left, char('@'), id_right), - preceded(char('>'), opt(cfws)), + preceded(opt(cfws), tag("<")), + (id_left, tag("@"), id_right), + preceded(tag(">"), opt(cfws)), ), ) .parse(input)?; - Ok((loc, MessageID(format!("{left}@{right}")))) + let mut result: BString = left.into(); + result.push_char('@'); + result.push_str(right); + + Ok((loc, MessageID(result))) } // obs_unstruct = { (( "\r"* ~ "\n"* ~ ((encoded_word | obs_utext)~ "\r"* ~ "\n"*)+) | fws)+ } @@ -911,7 +996,7 @@ fn unstructured(input: Span) -> IResult { #[derive(Debug)] enum Word { Encoded(BString), - UText(char), + UText(BString), Fws, } @@ -919,13 +1004,13 @@ fn unstructured(input: Span) -> IResult { "unstructured", many0(alt(( preceded( - map(take_while(|c| c == '\r' || c == '\n'), |_| Word::Fws), + map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws), terminated( alt(( map(encoded_word, Word::Encoded), - map(obs_utext, Word::UText), + map(obs_utext, |s| Word::UText((*s).into())), )), - map(take_while(|c| c == '\r' || c == '\n'), |_| Word::Fws), + map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws), ), ), map(fws, |_| Word::Fws), @@ -959,7 +1044,7 @@ fn unstructured(input: Span) -> IResult { } } Word::UText(c) => match processed.last_mut() { - Some(ProcessedWord::Text(prior)) => prior.push_char(c), + Some(ProcessedWord::Text(prior)) => prior.push_str(c), _ => processed.push(ProcessedWord::Text(c.to_string().into())), }, } @@ -985,10 +1070,10 @@ fn arc_authentication_results(input: Span) -> IResult IResult { fn no_result(input: Span) -> IResult> { context( "no_result", - map((opt(cfws), char(';'), opt(cfws), tag("none")), |_| vec![]), + map((opt(cfws), tag(";"), opt(cfws), tag("none")), |_| vec![]), ) .parse(input) } @@ -1039,7 +1124,7 @@ fn resinfo(input: Span) -> IResult { map( ( opt(cfws), - char(';'), + tag(";"), methodspec, opt(preceded(cfws, reasonspec)), opt(many1(propspec)), @@ -1067,7 +1152,7 @@ fn methodspec(input: Span) -> IResult, BString)> { opt(cfws), (keyword, opt(methodversion)), opt(cfws), - char('='), + tag("="), opt(cfws), keyword, ), @@ -1083,7 +1168,7 @@ fn keyword(input: Span) -> IResult { context( "keyword", map( - take_while1(|c: char| c.is_ascii_alphanumeric() || c == '+' || c == '-'), + take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'+' || c == b'-'), |s: Span| (*s).into(), ), ) @@ -1094,7 +1179,7 @@ fn methodversion(input: Span) -> IResult { context( "methodversion", preceded( - (opt(cfws), char('/'), opt(cfws)), + (opt(cfws), tag("/"), opt(cfws)), nom::character::complete::u32, ), ) @@ -1105,7 +1190,7 @@ fn reasonspec(input: Span) -> IResult { context( "reason", map( - (tag("reason"), opt(cfws), char('='), opt(cfws), value), + (tag("reason"), opt(cfws), tag("="), opt(cfws), value), |(_, _, _, _, value)| value, ), ) @@ -1120,19 +1205,19 @@ fn propspec(input: Span) -> IResult { opt(cfws), keyword, opt(cfws), - char('.'), + tag("."), opt(cfws), keyword, opt(cfws), - char('='), + tag("="), opt(cfws), alt(( - map(preceded(char('@'), domain), |d| { + map(preceded(tag("@"), domain), |d| { let mut at_dom = BString::from("@"); at_dom.push_str(d); at_dom }), - map(separated_pair(local_part, char('@'), domain), |(u, d)| { + map(separated_pair(local_part, tag("@"), domain), |(u, d)| { let mut result: BString = u.into(); result.push(b'@'); result.push_str(d); @@ -1153,16 +1238,16 @@ fn propspec(input: Span) -> IResult { } // obs_utext = @{ "\u{00}" | obs_no_ws_ctl | vchar } -fn obs_utext(input: Span) -> IResult { +fn obs_utext(input: Span) -> IResult { context( "obs_utext", - satisfy(|c| c == '\u{00}' || is_obs_no_ws_ctl(c) || is_vchar(c)), + take_while_m_n(1, 1, |c| c == 0x00 || is_obs_no_ws_ctl(c) || is_vchar(c)), ) .parse(input) } -fn is_mime_token(c: char) -> bool { - is_char(c) && c != ' ' && !is_ctl(c) && !is_tspecial(c) +fn is_mime_token(c: u8) -> bool { + is_char(c) && c != b' ' && !is_ctl(c) && !is_tspecial(c) } // mime_token = { (!(" " | ctl | tspecials) ~ char)+ } @@ -1182,7 +1267,7 @@ fn content_type(input: Span) -> IResult { ( mime_token, opt(cfws), - char('/'), + tag("/"), opt(cfws), mime_token, opt(cfws), @@ -1194,7 +1279,7 @@ fn content_type(input: Span) -> IResult { // In the meantime, there are implementations that assume // that the `;` is optional, so we therefore allow them // to be optional here in our implementation - preceded(opt(char(';')), opt(cfws)), + preceded(opt(tag(";")), opt(cfws)), terminated(parameter, opt(cfws)), )), ), @@ -1202,14 +1287,11 @@ fn content_type(input: Span) -> IResult { ) .parse(input)?; - let value = format!("{mime_type}/{mime_subtype}"); - Ok(( - loc, - MimeParameters { - value: value.into(), - parameters, - }, - )) + let mut value: BString = (*mime_type).into(); + value.push_char('/'); + value.push_str(mime_subtype); + + Ok((loc, MimeParameters { value, parameters })) } fn content_transfer_encoding(input: Span) -> IResult { @@ -1228,7 +1310,7 @@ fn content_transfer_encoding(input: Span) -> IResult { // In the meantime, there are implementations that assume // that the `;` is optional, so we therefore allow them // to be optional here in our implementation - preceded(opt(char(';')), opt(cfws)), + preceded(opt(tag(";")), opt(cfws)), terminated(parameter, opt(cfws)), )), ), @@ -1268,7 +1350,7 @@ fn param_with_unquoted_rfc2047(input: Span) -> IResult { context( "param_with_unquoted_rfc2047", map( - (attribute, opt(cfws), char('='), opt(cfws), encoded_word), + (attribute, opt(cfws), tag("="), opt(cfws), encoded_word), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), value: value.as_bytes().into(), @@ -1289,9 +1371,9 @@ fn param_with_quoted_rfc2047(input: Span) -> IResult { ( attribute, opt(cfws), - char('='), + tag("="), opt(cfws), - delimited(char('"'), encoded_word, char('"')), + delimited(tag("\""), encoded_word, tag("\"")), ), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), @@ -1313,17 +1395,17 @@ fn extended_param_with_charset(input: Span) -> IResult { ( attribute, opt(section), - char('*'), + tag("*"), opt(cfws), - char('='), + tag("="), opt(cfws), opt(mime_charset), - char('\''), + tag("'"), opt(mime_language), - char('\''), + tag("'"), map( recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))), - |s: Span| s.to_string(), + |s: Span| (*s).into(), ), ), |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter { @@ -1332,7 +1414,7 @@ fn extended_param_with_charset(input: Span) -> IResult { mime_charset: mime_charset.map(|s| s.as_bytes().into()), mime_language: mime_language.map(|s| s.as_bytes().into()), encoding: MimeParameterEncoding::Rfc2231, - value: value.into(), + value, }, ), ) @@ -1346,15 +1428,15 @@ fn extended_param_no_charset(input: Span) -> IResult { ( attribute, opt(section), - opt(char('*')), + opt(tag("*")), opt(cfws), - char('='), + tag("="), opt(cfws), alt(( quoted_string, map( recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))), - |s: Span| s.to_string().into(), + |s: Span| (*s).into(), ), )), ), @@ -1378,7 +1460,7 @@ fn extended_param_no_charset(input: Span) -> IResult { fn mime_charset(input: Span) -> IResult { context( "mime_charset", - take_while1(|c| is_mime_token(c) && c != '\''), + take_while1(|c| is_mime_token(c) && c != b'\''), ) .parse(input) } @@ -1386,7 +1468,7 @@ fn mime_charset(input: Span) -> IResult { fn mime_language(input: Span) -> IResult { context( "mime_language", - take_while1(|c| is_mime_token(c) && c != '\''), + take_while1(|c| is_mime_token(c) && c != b'\''), ) .parse(input) } @@ -1395,9 +1477,8 @@ fn ext_octet(input: Span) -> IResult { context( "ext_octet", recognize(( - char('%'), - satisfy(|c| c.is_ascii_hexdigit()), - satisfy(|c| c.is_ascii_hexdigit()), + tag("%"), + take_while_m_n(2, 2, |b: u8| b.is_ascii_hexdigit()), )), ) .parse(input) @@ -1405,11 +1486,7 @@ fn ext_octet(input: Span) -> IResult { // section = { "*" ~ ASCII_DIGIT+ } fn section(input: Span) -> IResult { - context( - "section", - preceded(char('*'), nom::character::complete::u32), - ) - .parse(input) + context("section", preceded(tag("*"), nom::character::complete::u32)).parse(input) } // regular_parameter = { attribute ~ cfws? ~ "=" ~ cfws? ~ value } @@ -1417,7 +1494,7 @@ fn regular_parameter(input: Span) -> IResult { context( "regular_parameter", map( - (attribute, opt(cfws), char('='), opt(cfws), value), + (attribute, opt(cfws), tag("="), opt(cfws), value), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), value: value.as_bytes().into(), @@ -1448,47 +1525,49 @@ fn value(input: Span) -> IResult { pub struct Parser; impl Parser { - pub fn parse_mailbox_list_header(text: &str) -> Result { + pub fn parse_mailbox_list_header(text: &[u8]) -> Result { parse_with(text, mailbox_list) } - pub fn parse_mailbox_header(text: &str) -> Result { + pub fn parse_mailbox_header(text: &[u8]) -> Result { parse_with(text, mailbox) } - pub fn parse_address_list_header(text: &str) -> Result { + pub fn parse_address_list_header(text: &[u8]) -> Result { parse_with(text, address_list) } - pub fn parse_msg_id_header(text: &str) -> Result { + pub fn parse_msg_id_header(text: &[u8]) -> Result { parse_with(text, msg_id) } - pub fn parse_msg_id_header_list(text: &str) -> Result> { + pub fn parse_msg_id_header_list(text: &[u8]) -> Result> { parse_with(text, msg_id_list) } - pub fn parse_content_id_header(text: &str) -> Result { + pub fn parse_content_id_header(text: &[u8]) -> Result { parse_with(text, content_id) } - pub fn parse_content_type_header(text: &str) -> Result { + pub fn parse_content_type_header(text: &[u8]) -> Result { parse_with(text, content_type) } - pub fn parse_content_transfer_encoding_header(text: &str) -> Result { + pub fn parse_content_transfer_encoding_header(text: &[u8]) -> Result { parse_with(text, content_transfer_encoding) } - pub fn parse_unstructured_header(text: &str) -> Result { + pub fn parse_unstructured_header(text: &[u8]) -> Result { parse_with(text, unstructured) } - pub fn parse_authentication_results_header(text: &str) -> Result { + pub fn parse_authentication_results_header(text: &[u8]) -> Result { parse_with(text, authentication_results) } - pub fn parse_arc_authentication_results_header(text: &str) -> Result { + pub fn parse_arc_authentication_results_header( + text: &[u8], + ) -> Result { parse_with(text, arc_authentication_results) } } @@ -1549,7 +1628,7 @@ pub struct AuthenticationResults { /// Emits a value that was parsed by `value`, into target fn emit_value_token(value: &[u8], target: &mut Vec) { - let use_quoted_string = !value.iter().all(|&c| is_mime_token(c as char) || c == b'@'); + let use_quoted_string = !value.iter().all(|&c| is_mime_token(c) || c == b'@'); if use_quoted_string { target.push(b'"'); for (start, end, c) in value.char_indices() { @@ -1625,7 +1704,7 @@ impl AddrSpec { } pub fn parse(email: &str) -> Result { - parse_with(email, addr_spec) + parse_with(email.as_bytes(), addr_spec) } } @@ -1633,10 +1712,7 @@ impl EncodeHeaderValue for AddrSpec { fn encode_value(&self) -> SharedString<'static> { let mut result: Vec = vec![]; - let needs_quoting = !self - .local_part - .iter() - .all(|&c| is_atext(c as char) || c == b'.'); + let needs_quoting = !self.local_part.iter().all(|&c| is_atext(c) || c == b'.'); if needs_quoting { result.push(b'"'); // RFC5321 4.1.2 qtextSMTP: @@ -1714,22 +1790,28 @@ pub struct Mailbox { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct MessageID(pub String); +pub struct MessageID(pub BString); impl EncodeHeaderValue for MessageID { fn encode_value(&self) -> SharedString<'static> { - format!("<{}>", self.0).into() + let mut result = Vec::::with_capacity(self.0.len() + 2); + result.push(b'<'); + result.push_str(&self.0); + result.push(b'>'); + result.into() } } impl EncodeHeaderValue for Vec { fn encode_value(&self) -> SharedString<'static> { - let mut result = String::new(); + let mut result = BString::default(); for id in self { if !result.is_empty() { result.push_str("\r\n\t"); } - result.push_str(&format!("<{}>", id.0)); + result.push(b'<'); + result.push_str(&id.0); + result.push(b'>'); } result.into() } @@ -1960,12 +2042,12 @@ impl EncodeHeaderValue for MimeParameters { result.push_str(&format!(";\r\n\t{name}=\"{encoded}\"")); } MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => { - let needs_encoding = value.chars().any(|c| !is_mime_token(c) || !c.is_ascii()); + let needs_encoding = value.iter().any(|&c| !is_mime_token(c) || !c.is_ascii()); // Prefer to use quoted_string representation when possible, as it doesn't // require any RFC 2231 encoding let use_quoted_string = value - .chars() - .all(|c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii()); + .iter() + .all(|&c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii()); let mut params = vec![]; let mut chars = value.char_indices().peekable(); @@ -1994,7 +2076,10 @@ impl EncodeHeaderValue for MimeParameters { encoded.push(b'\\'); } encoded.push_str(s); - } else if is_mime_token(c) && (!needs_encoding || c != '%') { + } else if (c as u32) <= 0xff + && is_mime_token(c as u32 as u8) + && (!needs_encoding || c != '%') + { encoded.push_str(s); } else { for b in s.bytes() { @@ -2175,12 +2260,17 @@ fn test_qp_encode() { /// string is enclosed in quotes. fn quote_string(s: impl AsRef<[u8]>) -> BString { let s = s.as_ref(); - if s.chars().any(|c| !is_atext(c)) { + + if s.iter().any(|&c| !is_atext(c)) { let mut result = Vec::::with_capacity(s.len() + 4); result.push(b'"'); for (start, end, c) in s.char_indices() { - if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) { - result.push(b'\\'); + let c = c as u32; + if c <= 0xff { + let c = c as u8; + if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) { + result.push(b'\\'); + } } result.push_str(&s[start..end]); } @@ -2357,19 +2447,12 @@ Some( InvalidHeaderValueDuringGet { header_name: "Sender", error: HeaderParse( - "0: at line 1: + "Error at line 1, expected "@" but found ".": hello..there@docomo.ne.jp ^___________________ -expected '@', found . - -1: at line 1, in addr_spec: -hello..there@docomo.ne.jp -^________________________ - -2: at line 1, in mailbox: -hello..there@docomo.ne.jp -^________________________ +while parsing addr_spec +while parsing mailbox ", ), } diff --git a/crates/mailparsing/src/strings.rs b/crates/mailparsing/src/strings.rs index a8f09daa..ff965bd4 100644 --- a/crates/mailparsing/src/strings.rs +++ b/crates/mailparsing/src/strings.rs @@ -107,11 +107,6 @@ impl SharedString<'_> { String::from_utf8_lossy(self.as_bytes()) } - #[deprecated = "use either as_bytes() or to_str() instead"] - pub fn as_str(&self) -> &str { - self.to_str().expect("SharedString is not UTF-8") - } - pub fn len(&self) -> usize { match self { Self::Owned(s) => s.len(),