diff --git a/meli/src/mail/view/envelope.rs b/meli/src/mail/view/envelope.rs index b2944e72..631bcde1 100644 --- a/meli/src/mail/view/envelope.rs +++ b/meli/src/mail/view/envelope.rs @@ -1092,33 +1092,36 @@ impl Component for EnvelopeView { ViewMode::Source(Source::Decoded) => { let text = { /* Decode each header value */ - let mut ret = melib::email::parser::headers::headers(self.mail.bytes()) + let mut ret = String::new(); + match melib::email::parser::headers::headers(self.mail.bytes()) .map(|(_, v)| v) - .map_err(|err| err.into()) - .and_then(|headers| { - Ok(headers - .into_iter() - .map(|(h, v)| { - melib::email::parser::encodings::phrase(v, true) - .map(|(_, v)| { - let mut h = h.to_vec(); - h.push(b':'); - h.push(b' '); - h.extend(v.into_iter()); - h - }) - .map_err(|err| err.into()) - }) - .collect::>>>()? - .join(&b"\n"[..])) - }) - .map(|v| String::from_utf8_lossy(&v).into_owned()) - .unwrap_or_else(|err: Error| err.to_string()); + { + Ok(headers) => { + for (h, v) in headers { + _ = match melib::email::parser::encodings::phrase(v, true) { + Ok((_, v)) => ret.write_fmt(format_args!( + "{h}: {}\n", + String::from_utf8_lossy(&v) + )), + Err(err) => ret.write_fmt(format_args!("{h}: {err}\n")), + }; + } + } + Err(err) => { + _ = write!(&mut ret, "{err}"); + } + } if !ret.ends_with("\n\n") { + if ret.ends_with('\n') { + ret.pop(); + } ret.push_str("\n\n"); } ret.push_str(&self.body_text); if !ret.ends_with("\n\n") { + if ret.ends_with('\n') { + ret.pop(); + } ret.push_str("\n\n"); } // ret.push_str(&self.attachment_tree); diff --git a/melib/src/email.rs b/melib/src/email.rs index 63db2d08..b78575f2 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -104,7 +104,7 @@ pub mod mailto; pub mod parser; pub mod pgp; -use std::{borrow::Cow, convert::TryInto, ops::Deref}; +use std::{borrow::Cow, ops::Deref}; pub use address::{Address, MessageID, References, StrBuild, StrBuilder}; pub use attachments::{Attachment, AttachmentBuilder}; @@ -371,47 +371,55 @@ impl Envelope { } }; for (name, value) in headers { - let name: HeaderName = name.try_into()?; - if name == "to" { - let parse_result = parser::address::rfc2822address_list(value); - if let Ok((_, value)) = parse_result { - self.set_to(value); - }; - } else if name == "cc" { - let parse_result = parser::address::rfc2822address_list(value); - if let Ok((_, value)) = parse_result { - self.set_cc(value); - }; - } else if name == "bcc" { - let parse_result = parser::address::rfc2822address_list(value); - if let Ok((_, value)) = parse_result { - self.set_bcc(value.to_vec()); - }; - } else if name == "from" { - let parse_result = parser::address::rfc2822address_list(value); - if let Ok((_, value)) = parse_result { - self.set_from(value); + match name { + HeaderName::TO => { + let parse_result = parser::address::rfc2822address_list(value); + if let Ok((_, value)) = parse_result { + self.set_to(value); + }; } - } else if name == "subject" { - let parse_result = parser::encodings::phrase(value.trim(), false); - if let Ok((_, value)) = parse_result { - self.set_subject(value); - }; - } else if name == "message-id" { - self.set_message_id(value); - } else if name == "references" { - self.set_references(value); - } else if name == "in-reply-to" { - self.set_in_reply_to(value); - } else if name == "date" { - let parse_result = parser::encodings::phrase(value, false); - if let Ok((_, value)) = parse_result { - self.set_date(value.as_slice()); - } else { - self.set_date(value); + HeaderName::CC => { + let parse_result = parser::address::rfc2822address_list(value); + if let Ok((_, value)) = parse_result { + self.set_cc(value); + }; } - } else if name == "content-type" { - match parser::attachments::content_type(value) { + HeaderName::BCC => { + let parse_result = parser::address::rfc2822address_list(value); + if let Ok((_, value)) = parse_result { + self.set_bcc(value.to_vec()); + }; + } + HeaderName::FROM => { + let parse_result = parser::address::rfc2822address_list(value); + if let Ok((_, value)) = parse_result { + self.set_from(value); + } + } + HeaderName::SUBJECT => { + let parse_result = parser::encodings::phrase(value.trim(), false); + if let Ok((_, value)) = parse_result { + self.set_subject(value); + }; + } + HeaderName::MESSAGE_ID => { + self.set_message_id(value); + } + HeaderName::REFERENCES => { + self.set_references(value); + } + HeaderName::IN_REPLY_TO => { + self.set_in_reply_to(value); + } + HeaderName::DATE => { + let parse_result = parser::encodings::phrase(value, false); + if let Ok((_, value)) = parse_result { + self.set_date(value.as_slice()); + } else { + self.set_date(value); + } + } + HeaderName::CONTENT_TYPE => match parser::attachments::content_type(value) { Ok((_, (ct, cst, ref params))) if ct.eq_ignore_ascii_case(b"multipart") && cst.eq_ignore_ascii_case(b"mixed") => @@ -437,7 +445,8 @@ impl Envelope { } } _ => {} - } + }, + _ => {} } self.other_headers.insert( name, @@ -598,13 +607,13 @@ impl Envelope { builder.build() } - pub fn headers<'a>(&self, bytes: &'a [u8]) -> Result> { + pub fn headers<'a>(&self, bytes: &'a [u8]) -> Result> { let ret = parser::headers::headers(bytes)?.1; let len = ret.len(); ret.into_iter() .try_fold(Vec::with_capacity(len), |mut acc, (a, b)| { Ok({ - acc.push((std::str::from_utf8(a)?, std::str::from_utf8(b)?)); + acc.push((a, std::str::from_utf8(b)?)); acc }) }) diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index 39bb2cea..c99ec1b4 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -31,7 +31,7 @@ use crate::{ address::StrBuilder, attachment_types::*, parser::{self, BytesExt}, - Mail, + HeaderName, Mail, }, BytesDisplay, }; @@ -108,11 +108,11 @@ impl AttachmentBuilder { ..Default::default() }; for (name, value) in headers { - if name.eq_ignore_ascii_case(b"content-type") { + if name == HeaderName::CONTENT_TYPE { builder.set_content_type_from_bytes(value); - } else if name.eq_ignore_ascii_case(b"content-transfer-encoding") { + } else if name == HeaderName::CONTENT_TRANSFER_ENCODING { builder.set_content_transfer_encoding(ContentTransferEncoding::from(value)); - } else if name.eq_ignore_ascii_case(b"content-disposition") { + } else if name == HeaderName::CONTENT_DISPOSITION { builder.set_content_disposition(ContentDisposition::from(value)); } } @@ -321,13 +321,13 @@ impl AttachmentBuilder { length: body.len(), }; for (name, value) in headers { - if name.eq_ignore_ascii_case(b"content-type") { + if name == HeaderName::CONTENT_TYPE { builder.set_content_type_from_bytes(value); - } else if name.eq_ignore_ascii_case(b"content-transfer-encoding") { + } else if name == HeaderName::CONTENT_TRANSFER_ENCODING { builder.set_content_transfer_encoding(ContentTransferEncoding::from( value, )); - } else if name.eq_ignore_ascii_case(b"content-disposition") { + } else if name == HeaderName::CONTENT_DISPOSITION { builder.set_content_disposition(ContentDisposition::from(value)); } } @@ -547,10 +547,10 @@ impl Attachment { Err(_err) => return false, }; let headers = crate::email::parser::generic::HeaderIterator(headers) - .collect::>(); + .collect::>(); let disposition = headers .iter() - .find(|(n, _)| n.eq_ignore_ascii_case(b"content-disposition")) + .find(|(n, _)| n == HeaderName::CONTENT_DISPOSITION) .map(|(_, v)| ContentDisposition::from(*v)) .unwrap_or_default(); if disposition.kind.is_attachment() { @@ -558,7 +558,7 @@ impl Attachment { } if let Some(boundary) = headers .iter() - .find(|(n, _)| n.eq_ignore_ascii_case(b"content-type")) + .find(|(n, _)| n == HeaderName::CONTENT_TYPE) .and_then(|(_, v)| { if let Ok((_, (ct, _cst, params))) = parser::attachments::content_type(v) @@ -856,7 +856,7 @@ impl Attachment { Err(_) => return ret, }; for (name, value) in headers { - if name.eq_ignore_ascii_case(b"content-type") { + if name == HeaderName::CONTENT_TYPE { if let Ok((_, (_, _, params))) = parser::attachments::content_type(value) { ret = params; } diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index e649e1fd..ab64cfbf 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -92,8 +92,7 @@ impl FromStr for Draft { let mut ret = Self::default(); for (k, v) in headers { - ret.headers - .insert(k.try_into()?, String::from_utf8(v.to_vec())?); + ret.headers.insert(k, String::from_utf8(v.to_vec())?); } let body = Envelope::new(EnvelopeHash::default()).body_bytes(s.as_bytes()); @@ -107,7 +106,7 @@ impl Draft { pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result { let mut ret = Self::default(); for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) { - ret.headers.insert(k.try_into()?, v.into()); + ret.headers.insert(k, v.into()); } let body = envelope.body_bytes(bytes); diff --git a/melib/src/email/headers.rs b/melib/src/email/headers.rs index 5012610a..84e1b05f 100644 --- a/melib/src/email/headers.rs +++ b/melib/src/email/headers.rs @@ -57,7 +57,7 @@ impl Eq for dyn HeaderKey + '_ {} impl HeaderKey for HeaderName { fn to_key(&self) -> &[u8] { - self.as_bytes() + self.as_lowercase_bytes() } } diff --git a/melib/src/email/headers/names.rs b/melib/src/email/headers/names.rs index 6268ec67..deebd27d 100644 --- a/melib/src/email/headers/names.rs +++ b/melib/src/email/headers/names.rs @@ -44,6 +44,7 @@ bitflags! { const Mail = Self::None.bits() << 1; const NNTP = Self::Mail.bits() << 1; const MIME = Self::NNTP.bits() << 1; + const Mbox = Self::MIME.bits() << 1; } } @@ -141,13 +142,21 @@ impl HeaderName { /// The returned string will always be lower case. Use `Display` for a /// properly formatted representation. #[inline] - pub fn as_bytes(&self) -> &[u8] { + pub fn as_lowercase_bytes(&self) -> &[u8] { match self.inner { Repr::Standard(v) => v.as_str().as_bytes(), Repr::Custom(ref v) => v.0.as_ref(), } } + /// Converts a header into a `Vec`. + /// + /// The returned string will always be formatted. + #[inline] + pub fn into_bytes(self) -> Vec { + self.to_string().into_bytes() + } + pub fn from_bytes(src: &[u8]) -> Result { if let Some(std) = StandardHeader::from_bytes(src.trim()) { Ok(Self { @@ -413,9 +422,11 @@ const UPPERCASE_TOKENS: &[&str] = &[ "SIO", "SPF", "TLS", "VBR", ]; -impl std::fmt::Display for Custom { +struct HeaderNameFmt<'a>(&'a str); + +impl std::fmt::Display for HeaderNameFmt<'_> { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let as_str = self.as_str(); + let as_str = self.0; let len = as_str.len(); let mut bytes_count = 0; for chunk in as_str.split('-') { @@ -467,6 +478,12 @@ impl std::fmt::Display for Custom { } } +impl std::fmt::Display for Custom { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(fmt, "{}", HeaderNameFmt(self.as_str())) + } +} + // an iterator which alternates between Some and None struct AsciiIgnoreCaseCmp<'a, 'b> { ord: Ordering, diff --git a/melib/src/email/headers/standards.rs b/melib/src/email/headers/standards.rs index 6b53efa7..8c117db1 100644 --- a/melib/src/email/headers/standards.rs +++ b/melib/src/email/headers/standards.rs @@ -370,12 +370,13 @@ standard_headers! { (ResentMessageId, RESENT_MESSAGE_ID, "Resent-Message-ID", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); (ResentReplyTo, RESENT_REPLY_TO, "Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5322]); (ResentSender, RESENT_SENDER, "Resent-Sender", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); - (ResentTo, RESENT_TO, "Resent-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ResentTo, RESENT_TO, "Resent-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); (ReturnPath, RETURN_PATH, "Return-Path", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); (SeeAlso, SEE_ALSO, "See-Also", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]); (Sender, SENDER, "Sender", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); (Sensitivity, SENSITIVITY, "Sensitivity", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); (Solicitation, SOLICITATION, "Solicitation", None, Protocol::Mail, Status::None, &[Standard::RFC3865]); + (Status, STATUS, "Status", None, Protocol::Mbox, Status::None, &[]); (Summary, SUMMARY, "Summary", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); (Supersedes, SUPERSEDES, "Supersedes", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC5536, Standard::RFC2156]); (TlsReportDomain, TLS_REPORT_DOMAIN, "TLS-Report-Domain", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]); @@ -402,9 +403,11 @@ standard_headers! { (SioLabel, SIO_LABEL, "SIO-Label", None, Protocol::Mail, Status::None, &[Standard::RFC7444]); (SioLabelHistory, SIO_LABEL_HISTORY, "SIO-Label-History", None, Protocol::Mail, Status::None, &[Standard::RFC7444]); (XArchivedAt, X_ARCHIVED_AT, "X-Archived-At", Some("prov/x-archived-at"), Protocol::Mail | Protocol::NNTP, Status::Deprecated, &[Standard::RFC5064]); + (XKeywords, X_KEYWORDS, "X-Keywords", None, Protocol::Mbox, Status::None, &[]); (XMittente, X_MITTENTE, "X-Mittente", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); (XRicevuta, X_RICEVUTA, "X-Ricevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); (XRiferimentoMessageId, X_RIFERIMENTO_MESSAGE_ID, "X-Riferimento-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XStatus, X_STATUS, "X-Status", None, Protocol::Mbox, Status::None, &[]); (XTiporicevuta, X_TIPORICEVUTA, "X-TipoRicevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); (XTrasporto, X_TRASPORTO, "X-Trasporto", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); (XVerificasicurezza, X_VERIFICASICUREZZA, "X-VerificaSicurezza", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index 9412b70a..02a13dbe 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -309,7 +309,7 @@ impl<'a, P: for<'r> FnMut(&'r u8) -> bool> BytesIterExt for std::slice::Split<'a } //fn parser(input: I) -> IResult; -pub fn mail(input: &[u8]) -> Result<(Vec<(&[u8], &[u8])>, &[u8])> { +pub fn mail(input: &[u8]) -> Result<(Vec<(HeaderName, &[u8])>, &[u8])> { let (rest, result) = alt(( separated_pair( headers::headers, @@ -1097,8 +1097,9 @@ pub mod generic { 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])> { + type Item = (HeaderName, &'a [u8]); + + fn next(&mut self) -> Option<(HeaderName, &'a [u8])> { if self.0.is_empty() { return None; } @@ -1425,15 +1426,15 @@ pub mod headers { //! Email headers. use super::*; - pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], &[u8])>> { + pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(HeaderName, &[u8])>> { many1(header)(input) } - pub fn header(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { + pub fn header(input: &[u8]) -> IResult<&[u8], (HeaderName, &[u8])> { alt((header_without_val, header_with_val))(input) } - pub fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { + pub fn header_without_val(input: &[u8]) -> IResult<&[u8], (HeaderName, &[u8])> { if input.is_empty() { return Err(nom::Err::Error( (input, "header_without_val(): input is empty").into(), @@ -1477,6 +1478,11 @@ pub mod headers { (input, "header_without_val(): not enough input").into(), )); } + let Ok(name) = HeaderName::try_from(name) else { + return Err(nom::Err::Error( + (input, "header_without_val(): invalid header name").into(), + )); + }; if input[ptr] == b':' { ptr += 1; has_colon = true; @@ -1592,8 +1598,9 @@ pub mod headers { )) } - /* Parse a single header as a tuple */ - pub fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { + /// Parse a single header as a ([`HeaderName`](crate::email::HeaderName), + /// [`&[u8]`]) tuple. + pub fn header_with_val(input: &[u8]) -> IResult<&[u8], (HeaderName, &[u8])> { if input.is_empty() { return Err(nom::Err::Error( (input, "header_with_val(): empty input").into(), @@ -1660,6 +1667,11 @@ pub mod headers { )); } } + let Ok(name) = HeaderName::try_from(name) else { + return Err(nom::Err::Error( + (input, "header_with_val(): invalid header name").into(), + )); + }; header_value(&input[ptr..]).map(|(rest, value)| (rest, (name, value))) } @@ -1689,7 +1701,8 @@ pub mod attachments { address::*, attachment_types::{ContentDisposition, ContentDispositionKind}, }; - pub fn attachment(input: &[u8]) -> IResult<&[u8], (std::vec::Vec<(&[u8], &[u8])>, &[u8])> { + + pub fn attachment(input: &[u8]) -> IResult<&[u8], (std::vec::Vec<(HeaderName, &[u8])>, &[u8])> { alt(( separated_pair( many0(headers::header), diff --git a/melib/src/imap/protocol_parser.rs b/melib/src/imap/protocol_parser.rs index d045fa2e..2d1acc3b 100644 --- a/melib/src/imap/protocol_parser.rs +++ b/melib/src/imap/protocol_parser.rs @@ -42,8 +42,11 @@ use crate::{ }, }, error::ResultIntoError, + utils::parsec::CRLF, }; +const UNTAGGED_PREFIX: &[u8] = b"* "; + bitflags! { #[derive(Default, Serialize, Deserialize)] pub struct RequiredResponses: u64 { @@ -75,10 +78,10 @@ bitflags! { impl RequiredResponses { pub fn check(&self, line: &[u8]) -> bool { - if !line.starts_with(b"* ") { + if !line.starts_with(UNTAGGED_PREFIX) { return false; } - let line = &line[b"* ".len()..]; + let line = &line[UNTAGGED_PREFIX.len()..]; let mut ret = false; if self.intersects(Self::CAPABILITY) { ret |= line.starts_with(b"CAPABILITY"); @@ -356,7 +359,7 @@ impl<'a> Iterator for ImapLineIterator<'a> { let mut i = 0; loop { let cur_slice = &self.slice[i..]; - if let Some(pos) = cur_slice.find(b"\r\n") { + if let Some(pos) = cur_slice.find(CRLF) { /* Skip literal continuation line */ if cur_slice.get(pos.saturating_sub(1)) == Some(&b'}') { i += pos + 2; @@ -424,7 +427,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> { let (input, separator) = delimited(tag(b"\""), take(1_u32), tag(b"\""))(input)?; let (input, _) = take(1_u32)(input)?; let (input, path) = mailbox_token(input)?; - let (input, _) = tag("\r\n")(input)?; + let (input, _) = tag(CRLF)(input)?; Ok(( input, ({ @@ -495,7 +498,7 @@ pub struct FetchResponse<'a> { pub fn fetch_response(input: &[u8]) -> ImapParseResult> { macro_rules! should_start_with { - ($input:expr, $tag:literal) => { + ($input:expr, $tag:tt) => { if !$input.starts_with($tag) { return Err(Error::new(format!( "Expected `{}` but got `{:.50}`", @@ -505,9 +508,9 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult> { } }; } - should_start_with!(input, b"* "); + should_start_with!(input, UNTAGGED_PREFIX); - let mut i = b"* ".len(); + let mut i = UNTAGGED_PREFIX.len(); macro_rules! bounds { () => { if i == input.len() { @@ -707,7 +710,7 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult = None; - while input.starts_with(b"* ") { + while input.starts_with(UNTAGGED_PREFIX) { let next_response = fetch_response(input); match next_response { Ok((rest, el, el_alert)) => { @@ -784,8 +787,8 @@ pub fn capabilities(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> { let (input, _) = take_until("CAPABILITY ")(input)?; let (input, _) = tag("CAPABILITY ")(input)?; let (input, ret) = separated_list1(tag(" "), is_not(" ]\r\n"))(input)?; - let (input, _) = take_until("\r\n")(input)?; - let (input, _) = tag("\r\n")(input)?; + let (input, _) = take_until(CRLF)(input)?; + let (input, _) = tag(CRLF)(input)?; Ok((input, ret)) } @@ -860,14 +863,13 @@ pub enum UntaggedResponse<'s> { pub fn untagged_responses(input: &[u8]) -> ImapParseResult>> { let orig_input = input; - let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"* ")(input)?; + let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(UNTAGGED_PREFIX)(input)?; let (input, num) = map_res::<_, _, _, (&[u8], nom::error::ErrorKind), _, _, _>(digit1, |s| { ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })(input)?; let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b" ")(input)?; - let (input, _tag) = - take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?; - let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?; + let (input, _tag) = take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?; + let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?; debug!( "Parse untagged response from {:?}", String::from_utf8_lossy(orig_input) @@ -904,7 +906,7 @@ pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec> { ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }), )(input)?; - let (input, _) = tag("\r\n")(input)?; + let (input, _) = tag(CRLF)(input)?; Ok((input, list)) }, |input: &'a [u8]| -> IResult<&'a [u8], Vec> { @@ -918,8 +920,8 @@ pub fn search_results_raw<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> { alt(( |input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> { let (input, _) = tag("* SEARCH ")(input)?; - let (input, list) = take_until("\r\n")(input)?; - let (input, _) = tag("\r\n")(input)?; + let (input, list) = take_until(CRLF)(input)?; + let (input, _) = tag(CRLF)(input)?; Ok((input, list)) }, |input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> { @@ -971,13 +973,13 @@ pub fn select_response(input: &[u8]) -> Result { if input.contains_subsequence(b"* OK") { let mut ret = SelectResponse::default(); for l in input.split_rn() { - if l.starts_with(b"* ") && l.ends_with(b" EXISTS\r\n") { + if l.starts_with(UNTAGGED_PREFIX) && l.ends_with(b" EXISTS\r\n") { ret.exists = ImapNum::from_str(&String::from_utf8_lossy( - &l[b"* ".len()..l.len() - b" EXISTS\r\n".len()], + &l[UNTAGGED_PREFIX.len()..l.len() - b" EXISTS\r\n".len()], ))?; - } else if l.starts_with(b"* ") && l.ends_with(b" RECENT\r\n") { + } else if l.starts_with(UNTAGGED_PREFIX) && l.ends_with(b" RECENT\r\n") { ret.recent = ImapNum::from_str(&String::from_utf8_lossy( - &l[b"* ".len()..l.len() - b" RECENT\r\n".len()], + &l[UNTAGGED_PREFIX.len()..l.len() - b" RECENT\r\n".len()], ))?; } else if l.starts_with(b"* FLAGS (") { ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?; @@ -1110,28 +1112,29 @@ pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec)> { */ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> { + const WS: &[u8] = b"\r\n\t "; let (input, _) = tag("(")(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, date) = quoted_or_nil(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, subject) = quoted_or_nil(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, from) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, _sender) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, _reply_to) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, to) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, cc) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, bcc) = envelope_addresses(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, in_reply_to) = quoted_or_nil(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, message_id) = quoted_or_nil(input)?; - let (input, _) = opt(is_a("\r\n\t "))(input)?; + let (input, _) = opt(is_a(WS))(input)?; let (input, _) = tag(")")(input)?; Ok(( input, @@ -1215,13 +1218,14 @@ pub fn envelope_addresses<'a>( // Parse an address in the format of the ENVELOPE structure eg // ("Terry Gray" NIL "gray" "cac.washington.edu") pub fn envelope_address(input: &[u8]) -> IResult<&[u8], Address> { + const WS: &[u8] = b"\r\n\t "; let (input, name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?; - let (input, _) = is_a("\r\n\t ")(input)?; + let (input, _) = is_a(WS)(input)?; let (input, _) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?; - let (input, _) = is_a("\r\n\t ")(input)?; + let (input, _) = is_a(WS)(input)?; let (input, mailbox_name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?; let (input, host_name) = opt(preceded( - is_a("\r\n\t "), + is_a(WS), alt((quoted, map(tag("NIL"), |_| Vec::new()))), ))(input)?; Ok(( @@ -1369,7 +1373,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> { while !input.is_empty() { if input[0] == b' ' || input[0] == b'\n' || input[0] == b'\t' { input = &input[1..]; - } else if input.starts_with(b"\r\n") { + } else if input.starts_with(CRLF) { input = &input[2..]; } else { break; diff --git a/melib/src/mbox/write.rs b/melib/src/mbox/write.rs index f4e79c00..454d8725 100644 --- a/melib/src/mbox/write.rs +++ b/melib/src/mbox/write.rs @@ -20,7 +20,10 @@ */ use super::*; -use crate::utils::datetime; +use crate::utils::{ + datetime, + parsec::{CRLF, LF}, +}; impl MboxFormat { #[allow(clippy::too_many_arguments)] @@ -38,7 +41,7 @@ impl MboxFormat { if tags.iter().any(|t| t.contains(' ')) { return Err(Error::new("mbox tags/keywords can't contain spaces")); } - let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] }; + let line_ending: &'static [u8] = if crlf { CRLF } else { LF }; if !is_empty { writer.write_all(line_ending)?; writer.write_all(line_ending)?; @@ -62,21 +65,21 @@ impl MboxFormat { writer.write_all(line_ending)?; let (mut headers, body) = parser::mail(input)?; headers.retain(|(header_name, _)| { - !header_name.eq_ignore_ascii_case(b"Status") - && !header_name.eq_ignore_ascii_case(b"X-Status") - && !header_name.eq_ignore_ascii_case(b"X-Keywords") - && !header_name.eq_ignore_ascii_case(b"Content-Length") + header_name != HeaderName::STATUS + && header_name != HeaderName::X_STATUS + && header_name != HeaderName::X_KEYWORDS + && header_name != HeaderName::CONTENT_LENGTH }); let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| { let mut i = 0; if crlf { while i < bytes.len() { - if bytes[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\r', b'\n'])?; + if bytes[i..].starts_with(CRLF) { + writer.write_all(CRLF)?; i += 2; continue; } else if bytes[i] == b'\n' { - writer.write_all(&[b'\r', b'\n'])?; + writer.write_all(CRLF)?; } else { writer.write_all(&[bytes[i]])?; } @@ -84,8 +87,8 @@ impl MboxFormat { } } else { while i < bytes.len() { - if bytes[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\n'])?; + if bytes[i..].starts_with(CRLF) { + writer.write_all(LF)?; i += 2; } else { writer.write_all(&[bytes[i]])?; @@ -99,7 +102,7 @@ impl MboxFormat { MboxMetadata::CClient => { for (h, v) in { if flags.is_seen() { - Some((&b"Status"[..], "R".into())) + Some((HeaderName::STATUS, "R".into())) } else { None } @@ -113,7 +116,7 @@ impl MboxFormat { None } else { Some(( - &b"X-Status"[..], + HeaderName::X_STATUS, format!( "{flagged}{replied}{draft}{trashed}", flagged = if flags.is_flagged() { "F" } else { "" }, @@ -127,11 +130,10 @@ impl MboxFormat { .chain(if tags.is_empty() { None } else { - Some((&b"X-Keywords"[..], tags.as_slice().join(" "))) + Some((HeaderName::X_KEYWORDS, tags.as_slice().join(" "))) }) } { - writer.write_all(h)?; - writer.write_all(&b": "[..])?; + writer.write_fmt(format_args!("{}: ", h))?; writer.write_all(v.as_bytes())?; writer.write_all(line_ending)?; } @@ -144,17 +146,19 @@ impl MboxFormat { let mut len = body.len(); if crlf { let stray_lfs = body.iter().filter(|b| **b == b'\n').count() - - body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count(); + - body.windows(CRLF.len()).filter(|w| w == &CRLF).count(); len += stray_lfs; } else { - let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count(); + let crlfs = body.windows(CRLF.len()).filter(|w| w == &CRLF).count(); len -= crlfs; } len }; match self { - Self::MboxO | Self::MboxRd => Err(Error::new("Unimplemented.")), + Self::MboxO | Self::MboxRd => { + Err(Error::new("Not implemented.").set_kind(ErrorKind::NotImplemented)) + } Self::MboxCl => { let len = (body_len + body @@ -165,10 +169,9 @@ impl MboxFormat { .to_string(); for (h, v) in headers .into_iter() - .chain(Some((&b"Content-Length"[..], len.as_bytes()))) + .chain(Some((HeaderName::CONTENT_LENGTH, len.as_bytes()))) { - writer.write_all(h)?; - writer.write_all(&b": "[..])?; + writer.write_fmt(format_args!("{}: ", h))?; write_header_val_fn(writer, v)?; writer.write_all(line_ending)?; } @@ -181,14 +184,14 @@ impl MboxFormat { let mut i = 0; if crlf { while i < body.len() { - if body[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\r', b'\n'])?; + if body[i..].starts_with(CRLF) { + writer.write_all(CRLF)?; if body[i..].starts_with(b"\r\nFrom ") { writer.write_all(&[b'>'])?; } i += 2; } else if body[i] == b'\n' { - writer.write_all(&[b'\r', b'\n'])?; + writer.write_all(CRLF)?; if body[i..].starts_with(b"\nFrom ") { writer.write_all(&[b'>'])?; } @@ -200,8 +203,8 @@ impl MboxFormat { } } else { while i < body.len() { - if body[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\n'])?; + if body[i..].starts_with(CRLF) { + writer.write_all(LF)?; if body[i..].starts_with(b"\r\nFrom ") { writer.write_all(&[b'>'])?; } @@ -221,10 +224,9 @@ impl MboxFormat { let len = body_len.to_string(); for (h, v) in headers .into_iter() - .chain(Some((&b"Content-Length"[..], len.as_bytes()))) + .chain(Some((HeaderName::CONTENT_LENGTH, len.as_bytes()))) { - writer.write_all(h)?; - writer.write_all(&b": "[..])?; + writer.write_fmt(format_args!("{}: ", h))?; write_header_val_fn(writer, v)?; writer.write_all(line_ending)?; } @@ -233,12 +235,12 @@ impl MboxFormat { let mut i = 0; if crlf { while i < body.len() { - if body[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\r', b'\n'])?; + if body[i..].starts_with(CRLF) { + writer.write_all(CRLF)?; i += 2; continue; } else if body[i] == b'\n' { - writer.write_all(&[b'\r', b'\n'])?; + writer.write_all(CRLF)?; } else { writer.write_all(&[body[i]])?; } @@ -246,8 +248,8 @@ impl MboxFormat { } } else { while i < body.len() { - if body[i..].starts_with(b"\r\n") { - writer.write_all(&[b'\n'])?; + if body[i..].starts_with(CRLF) { + writer.write_all(LF)?; i += 2; } else { writer.write_all(&[body[i]])?; diff --git a/melib/src/utils/parsec.rs b/melib/src/utils/parsec.rs index 6ac4f4ff..39484aa0 100644 --- a/melib/src/utils/parsec.rs +++ b/melib/src/utils/parsec.rs @@ -25,6 +25,11 @@ use std::borrow::Cow; use crate::utils::datetime::{parse_timestamp_from_string, UnixTimestamp}; +pub const CRLF: &[u8] = b"\r\n"; +pub const LF: &[u8] = b"\n"; +pub const NULL: &[u8] = b"\0"; +pub const EMPTY: &[u8] = b""; + pub type Result<'a, Output> = std::result::Result<(&'a str, Output), &'a str>; pub trait Parser<'a, Output> {