diff --git a/Cargo.lock b/Cargo.lock index ca63b73e..6cd7be5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4959,6 +4959,7 @@ name = "mod-mimepart" version = "0.1.0" dependencies = [ "anyhow", + "bstr", "config", "mailparsing", "mlua", diff --git a/crates/kumo-log-types/src/rfc3464.rs b/crates/kumo-log-types/src/rfc3464.rs index e2c42f5d..c6b83330 100644 --- a/crates/kumo-log-types/src/rfc3464.rs +++ b/crates/kumo-log-types/src/rfc3464.rs @@ -6,7 +6,7 @@ use crate::rfc5965::{ }; use crate::{JsonLogRecord, RecordType}; use anyhow::{anyhow, Context}; -use bstr::{BString, ByteSlice}; +use bstr::{BStr, BString, ByteSlice}; use chrono::{DateTime, Utc}; use mailparsing::MimePart; use serde::{Deserialize, Serialize}; @@ -362,7 +362,7 @@ pub struct Report { pub original_message: Option, } -pub(crate) fn content_type(part: &MimePart) -> Option { +pub(crate) fn content_type(part: &MimePart) -> Option { let ct = part.headers().content_type().ok()??; Some(ct.value) } @@ -376,7 +376,8 @@ impl Report { ) })?; - if content_type(&mail).as_deref() != Some("multipart/report") { + if content_type(&mail).as_ref().map(|b| b.as_bstr()) != Some(BStr::new("multipart/report")) + { return Ok(None); } @@ -384,8 +385,10 @@ impl Report { for part in mail.child_parts() { let ct = content_type(part); - let ct = ct.as_deref(); - if ct == Some("message/rfc822") || ct == Some("text/rfc822-headers") { + let ct = ct.as_ref().map(|b| b.as_bstr()); + if ct == Some(BStr::new("message/rfc822")) + || ct == Some(BStr::new("text/rfc822-headers")) + { original_message = Some(BString::new( part.raw_body().as_bytes().replace(b"\r\n", b"\n"), )); @@ -394,8 +397,9 @@ impl Report { for part in mail.child_parts() { let ct = content_type(part); - let ct = ct.as_deref(); - if ct == Some("message/delivery-status") || ct == Some("message/global-delivery-status") + let ct = ct.as_ref().map(|b| b.as_bstr()); + if ct == Some(BStr::new("message/delivery-status")) + || ct == Some(BStr::new("message/global-delivery-status")) { return Ok(Some(Self::parse_inner(part, original_message)?)); } @@ -567,7 +571,7 @@ impl Report { "multipart/report", parts, if params.stable_content { - Some("report-boundary") + Some(b"report-boundary") } else { None }, diff --git a/crates/kumo-log-types/src/rfc5965.rs b/crates/kumo-log-types/src/rfc5965.rs index fdd6a1c2..4b13bfff 100644 --- a/crates/kumo-log-types/src/rfc5965.rs +++ b/crates/kumo-log-types/src/rfc5965.rs @@ -1,7 +1,7 @@ //! ARF reports use crate::rfc3464::{content_type, RemoteMta}; use anyhow::anyhow; -use bstr::{BString, ByteSlice}; +use bstr::{BStr, BString, ByteSlice}; use chrono::{DateTime, Utc}; use mailparsing::{Header, HeaderParseResult, MimePart}; use rfc5321::parse_envelope_address; @@ -75,7 +75,8 @@ impl ARFReport { return Ok(None); } - if ct.get("report-type").as_deref() != Some("feedback-report") { + if ct.get("report-type").as_ref().map(|b| b.as_bstr()) != Some(BStr::new("feedback-report")) + { return Ok(None); } @@ -84,8 +85,10 @@ impl ARFReport { for part in mail.child_parts() { let ct = content_type(part); - let ct = ct.as_deref(); - if ct == Some("message/rfc822") || ct == Some("text/rfc822-headers") { + let ct = ct.as_ref().map(|b| b.as_bstr()); + if ct == Some(BStr::new("message/rfc822")) + || ct == Some(BStr::new("text/rfc822-headers")) + { if let Ok(HeaderParseResult { headers, .. }) = Header::parse_headers(part.raw_body()) { @@ -127,8 +130,8 @@ impl ARFReport { for part in mail.child_parts() { let ct = content_type(part); - let ct = ct.as_deref(); - if ct == Some("message/feedback-report") { + let ct = ct.as_ref().map(|b| b.as_bstr()); + if ct == Some(BStr::new("message/feedback-report")) { return Ok(Some(Self::parse_inner( part, original_message, diff --git a/crates/kumod/src/http_server/inject_v1.rs b/crates/kumod/src/http_server/inject_v1.rs index e5a7f4c6..16d90d96 100644 --- a/crates/kumod/src/http_server/inject_v1.rs +++ b/crates/kumod/src/http_server/inject_v1.rs @@ -711,7 +711,7 @@ impl InjectV1Request { let mut attached = vec![]; for a in attachments { let opts = mailparsing::AttachmentOptions { - file_name: a.file_name.clone(), + file_name: a.file_name.clone().map(|s| s.into()), inline: a.content_id.is_some(), content_id: a.content_id.clone(), }; diff --git a/crates/mailparsing/src/builder.rs b/crates/mailparsing/src/builder.rs index e77cbeb7..2124c7e7 100644 --- a/crates/mailparsing/src/builder.rs +++ b/crates/mailparsing/src/builder.rs @@ -86,7 +86,7 @@ impl<'a> MessageBuilder<'a> { "multipart/alternative", vec![t?, amp?, h?], if self.stable_content { - Some("ma-boundary") + Some(b"ma-boundary") } else { None }, @@ -97,7 +97,7 @@ impl<'a> MessageBuilder<'a> { "multipart/alternative", vec![first?, second?], if self.stable_content { - Some("ma-boundary") + Some(b"ma-boundary") } else { None }, @@ -124,7 +124,7 @@ impl<'a> MessageBuilder<'a> { "multipart/related", parts, if self.stable_content { - Some("mr-boundary") + Some(b"mr-boundary") } else { None }, @@ -141,7 +141,7 @@ impl<'a> MessageBuilder<'a> { "multipart/mixed", parts, if self.stable_content { - Some("mm-boundary") + Some(b"mm-boundary") } else { None }, @@ -304,7 +304,7 @@ Content-Transfer-Encoding: quoted-printable\r b"hello", Some(&AttachmentOptions { content_id: None, - file_name: Some("日本語の添付.txt".to_string()), + file_name: Some("日本語の添付.txt".into()), inline: false, }), ) diff --git a/crates/mailparsing/src/mimepart.rs b/crates/mailparsing/src/mimepart.rs index a8fd105a..7d6f8ac4 100644 --- a/crates/mailparsing/src/mimepart.rs +++ b/crates/mailparsing/src/mimepart.rs @@ -5,7 +5,7 @@ use crate::{ has_lone_cr_or_lf, Header, MailParsingError, MessageID, MimeParameterEncoding, MimeParameters, Result, SharedString, }; -use bstr::BStr; +use bstr::{BStr, BString, ByteSlice}; use charset_normalizer_rs::entity::NormalizerSettings; use charset_normalizer_rs::Encoding; use chrono::Utc; @@ -58,7 +58,12 @@ impl Rfc2045Info { fn new(headers: &HeaderMap) -> Self { let mut invalid_mime_headers = false; let encoding = match headers.content_transfer_encoding() { - Ok(Some(cte)) => match ContentTransferEncoding::from_str(&cte.value) { + Ok(Some(cte)) => match cte + .value + .to_str() + .map_err(|_| ()) + .and_then(|s| ContentTransferEncoding::from_str(s).map_err(|_| ())) + { Ok(encoding) => encoding, Err(_) => { invalid_mime_headers = true; @@ -87,10 +92,16 @@ impl Rfc2045Info { } else { None }; - let charset = charset.unwrap_or_else(|| "us-ascii".to_string()); + let charset = charset.unwrap_or_else(|| "us-ascii".into()); - let charset = Encoding::by_name(&*charset) - .ok_or_else(|| MailParsingError::BodyParse(format!("unsupported charset {charset}"))); + let charset = match charset.to_str() { + Ok(charset) => Encoding::by_name(&*charset).ok_or_else(|| { + MailParsingError::BodyParse(format!("unsupported charset {charset}")) + }), + Err(_) => Err(MailParsingError::BodyParse(format!( + "non-ascii charset name {charset}" + ))), + }; let (is_text, is_multipart) = if let Some(ct) = &content_type { (ct.is_text(), ct.is_multipart()) @@ -149,7 +160,7 @@ impl Rfc2045Info { pub fn content_type(&self) -> Option<&str> { self.content_type .as_ref() - .map(|params| params.value.as_str()) + .and_then(|params| params.value.to_str().ok()) } } @@ -497,16 +508,16 @@ impl<'a> MimePart<'a> { let ct = info .content_type .as_ref() - .map(|ct| ct.value.as_str()) - .unwrap_or("text/plain"); + .map(|ct| ct.value.as_bstr()) + .unwrap_or_else(|| BStr::new("text/plain")); Self::new_text(ct, text.as_bytes())? } DecodedBody::Binary(data) => { let ct = info .content_type .as_ref() - .map(|ct| ct.value.as_str()) - .unwrap_or("application/octet-stream"); + .map(|ct| ct.value.as_bstr()) + .unwrap_or_else(|| BStr::new("application/octet-stream")); Self::new_binary(ct, &data, info.attachment_options.as_ref())? } } @@ -516,7 +527,11 @@ impl<'a> MimePart<'a> { "multipart message has no content-type information!?".to_string(), ) })?; - Self::new_multipart(&ct.value, children, ct.get("boundary").as_deref())? + Self::new_multipart( + &ct.value, + children, + ct.get("boundary").as_deref().map(|b| b.as_bytes()), + )? }; for hdr in self.headers.iter() { @@ -640,7 +655,7 @@ impl<'a> MimePart<'a> { pub fn replace_text_body( &mut self, - content_type: &str, + content_type: impl AsRef<[u8]>, content: impl AsRef, ) -> Result<()> { let mut new_part = Self::new_text(content_type, content)?; @@ -657,7 +672,7 @@ impl<'a> MimePart<'a> { Ok(()) } - pub fn replace_binary_body(&mut self, content_type: &str, content: &[u8]) -> Result<()> { + pub fn replace_binary_body(&mut self, content_type: &[u8], content: &[u8]) -> Result<()> { let mut new_part = Self::new_binary(content_type, content, None)?; self.bytes = new_part.bytes; self.body_offset = new_part.body_offset; @@ -700,7 +715,7 @@ impl<'a> MimePart<'a> { /// Constructs a new part with textual utf8 content. /// quoted-printable transfer encoding will be applied, /// unless it is smaller to represent the text in base64 - pub fn new_text(content_type: &str, content: impl AsRef) -> Result { + pub fn new_text(content_type: impl AsRef<[u8]>, content: impl AsRef) -> Result { let content = content.as_ref(); // We'll probably use qp, so speculatively do the work let qp_encoded = quoted_printable::encode(content); @@ -760,9 +775,9 @@ impl<'a> MimePart<'a> { } pub fn new_multipart( - content_type: &str, + content_type: impl AsRef<[u8]>, parts: Vec, - boundary: Option<&str>, + boundary: Option<&[u8]>, ) -> Result { let mut headers = HeaderMap::default(); @@ -793,7 +808,7 @@ impl<'a> MimePart<'a> { } pub fn new_binary( - content_type: &str, + content_type: impl AsRef<[u8]>, content: &[u8], options: Option<&AttachmentOptions>, ) -> Result { @@ -990,7 +1005,7 @@ impl<'a> MimePart<'a> { } } - if ct.value.starts_with("multipart/") { + if ct.value.starts_with_str("multipart/") { let mut text_part = None; let mut html_part = None; let mut amp_html_part = None; @@ -1224,7 +1239,7 @@ pub struct SimplifiedStructure<'a> { #[serde(deny_unknown_fields)] pub struct AttachmentOptions { #[serde(default)] - pub file_name: Option, + pub file_name: Option, #[serde(default)] pub inline: bool, #[serde(default)] @@ -1618,14 +1633,14 @@ Ok( "application/octet-stream", &[0, 1, 2, 3], Some(&AttachmentOptions { - file_name: Some("woot.bin".to_string()), + file_name: Some("woot.bin".into()), inline: false, - content_id: Some("woot.id".to_string()), + content_id: Some("woot.id".into()), }), ) .unwrap(), ], - Some("my-boundary"), + Some(b"my-boundary"), ) .unwrap(); k9::snapshot!( @@ -1723,7 +1738,7 @@ Ok( Some(AttachmentOptions { content_id: None, inline: false, - file_name: Some("cdname".to_string()), + file_name: Some("cdname".into()), }) ); } @@ -1756,7 +1771,7 @@ Ok( Some(AttachmentOptions { content_id: None, inline: false, - file_name: Some("ctname".to_string()), + file_name: Some("ctname".into()), }) ); } diff --git a/crates/mailparsing/src/rfc5322_parser.rs b/crates/mailparsing/src/rfc5322_parser.rs index a73f85a7..55ddc020 100644 --- a/crates/mailparsing/src/rfc5322_parser.rs +++ b/crates/mailparsing/src/rfc5322_parser.rs @@ -1,7 +1,7 @@ use crate::headermap::EncodeHeaderValue; use crate::nom_utils::{explain_nom, make_context_error, make_span, IResult, ParseError, Span}; use crate::{MailParsingError, Result, SharedString}; -use bstr::{BString, ByteSlice, ByteVec}; +use bstr::{BStr, BString, ByteSlice, ByteVec}; use charset_normalizer_rs::Encoding; use nom::branch::alt; use nom::bytes::complete::{tag, take_while, take_while1}; @@ -1200,7 +1200,13 @@ fn content_type(input: Span) -> IResult { .parse(input)?; let value = format!("{mime_type}/{mime_subtype}"); - Ok((loc, MimeParameters { value, parameters })) + Ok(( + loc, + MimeParameters { + value: value.into(), + parameters, + }, + )) } fn content_transfer_encoding(input: Span) -> IResult { @@ -1230,7 +1236,7 @@ fn content_transfer_encoding(input: Span) -> IResult { Ok(( loc, MimeParameters { - value: value.to_string(), + value: value.as_bytes().into(), parameters, }, )) @@ -1261,8 +1267,8 @@ fn param_with_unquoted_rfc2047(input: Span) -> IResult { map( (attribute, opt(cfws), char('='), opt(cfws), encoded_word), |(name, _, _, _, value)| MimeParameter { - name: name.to_string(), - value, + name: name.as_bytes().into(), + value: value.as_bytes().into(), section: None, encoding: MimeParameterEncoding::UnquotedRfc2047, mime_charset: None, @@ -1285,8 +1291,8 @@ fn param_with_quoted_rfc2047(input: Span) -> IResult { delimited(char('"'), encoded_word, char('"')), ), |(name, _, _, _, value)| MimeParameter { - name: name.to_string(), - value, + name: name.as_bytes().into(), + value: value.as_bytes().into(), section: None, encoding: MimeParameterEncoding::QuotedRfc2047, mime_charset: None, @@ -1318,12 +1324,12 @@ fn extended_param_with_charset(input: Span) -> IResult { ), ), |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter { - name: name.to_string(), + name: name.as_bytes().into(), section, - mime_charset: mime_charset.map(|s| s.to_string()), - mime_language: mime_language.map(|s| s.to_string()), + 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: value.into(), }, ), ) @@ -1350,7 +1356,7 @@ fn extended_param_no_charset(input: Span) -> IResult { )), ), |(name, section, star, _, _, _, value)| MimeParameter { - name: name.to_string(), + name: name.as_bytes().into(), section, mime_charset: None, mime_language: None, @@ -1359,7 +1365,7 @@ fn extended_param_no_charset(input: Span) -> IResult { } else { MimeParameterEncoding::None }, - value, + value: value.into(), }, ), ) @@ -1410,8 +1416,8 @@ fn regular_parameter(input: Span) -> IResult { map( (attribute, opt(cfws), char('='), opt(cfws), value), |(name, _, _, _, value)| MimeParameter { - name: name.to_string(), - value, + name: name.as_bytes().into(), + value: value.as_bytes().into(), section: None, encoding: MimeParameterEncoding::None, mime_charset: None, @@ -1744,24 +1750,24 @@ pub(crate) enum MimeParameterEncoding { #[derive(Debug, Clone, PartialEq, Eq)] struct MimeParameter { - pub name: String, + pub name: BString, pub section: Option, - pub mime_charset: Option, - pub mime_language: Option, + pub mime_charset: Option, + pub mime_language: Option, pub encoding: MimeParameterEncoding, - pub value: String, + pub value: BString, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct MimeParameters { - pub value: String, + pub value: BString, parameters: Vec, } impl MimeParameters { - pub fn new(value: &str) -> Self { + pub fn new(value: impl AsRef<[u8]>) -> Self { Self { - value: value.to_string(), + value: value.as_ref().into(), parameters: vec![], } } @@ -1770,10 +1776,10 @@ impl MimeParameters { /// of the parameter names to parameters values. /// Incorrectly encoded parameters are silently ignored /// and are not returned in the resulting map. - pub fn parameter_map(&self) -> BTreeMap { + pub fn parameter_map(&self) -> BTreeMap { let mut map = BTreeMap::new(); - fn contains_key_ignore_case(map: &BTreeMap, key: &str) -> bool { + fn contains_key_ignore_case(map: &BTreeMap, key: &[u8]) -> bool { for k in map.keys() { if k.eq_ignore_ascii_case(key) { return true; @@ -1783,9 +1789,10 @@ impl MimeParameters { } for entry in &self.parameters { - if !contains_key_ignore_case(&map, &entry.name) { - if let Some(value) = self.get(&entry.name) { - map.insert(entry.name.to_string(), value); + let name = entry.name.as_bytes(); + if !contains_key_ignore_case(&map, name) { + if let Some(value) = self.get(name) { + map.insert(name.into(), value); } } } @@ -1798,11 +1805,12 @@ impl MimeParameters { /// per RFC 2231 and combine multi-element fields into a single /// contiguous value. /// Invalid charsets and encoding will be silently ignored. - pub fn get(&self, name: &str) -> Option { + pub fn get(&self, name: impl AsRef<[u8]>) -> Option { + let name = name.as_ref(); let mut elements: Vec<_> = self .parameters .iter() - .filter(|p| p.name.eq_ignore_ascii_case(name)) + .filter(|p| p.name.eq_ignore_ascii_case(name.as_bytes())) .collect(); if elements.is_empty() { return None; @@ -1810,10 +1818,10 @@ impl MimeParameters { elements.sort_by(|a, b| a.section.cmp(&b.section)); let mut mime_charset = None; - let mut result = String::new(); + let mut result: Vec = vec![]; for ele in elements { - if let Some(cset) = ele.mime_charset.as_deref() { + if let Some(cset) = ele.mime_charset.as_ref().and_then(|b| b.to_str().ok()) { mime_charset = Encoding::by_name(&*cset); } @@ -1886,30 +1894,31 @@ impl MimeParameters { } } - Some(result) + Some(result.into()) } /// Remove the named parameter - pub fn remove(&mut self, name: &str) { + pub fn remove(&mut self, name: impl AsRef<[u8]>) { + let name = name.as_ref(); self.parameters .retain(|p| !p.name.eq_ignore_ascii_case(name)); } - pub fn set(&mut self, name: &str, value: &str) { + pub fn set(&mut self, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { self.set_with_encoding(name, value, MimeParameterEncoding::None) } pub(crate) fn set_with_encoding( &mut self, - name: &str, - value: &str, + name: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, encoding: MimeParameterEncoding, ) { - self.remove(name); + self.remove(name.as_ref()); self.parameters.push(MimeParameter { - name: name.to_string(), - value: value.to_string(), + name: name.as_ref().into(), + value: value.as_ref().into(), section: None, mime_charset: None, mime_language: None, @@ -1918,21 +1927,21 @@ impl MimeParameters { } pub fn is_multipart(&self) -> bool { - self.value.starts_with("message/") || self.value.starts_with("multipart/") + self.value.starts_with_str("message/") || self.value.starts_with_str("multipart/") } pub fn is_text(&self) -> bool { - self.value.starts_with("text/") + self.value.starts_with_str("text/") } } impl EncodeHeaderValue for MimeParameters { fn encode_value(&self) -> SharedString<'static> { let mut result = self.value.to_string(); - let names: BTreeMap<&str, MimeParameterEncoding> = self + let names: BTreeMap<&BStr, MimeParameterEncoding> = self .parameters .iter() - .map(|p| (p.name.as_str(), p.encoding)) + .map(|p| (p.name.as_bstr(), p.encoding)) .collect(); for (name, stated_encoding) in names { @@ -1940,11 +1949,11 @@ impl EncodeHeaderValue for MimeParameters { match stated_encoding { MimeParameterEncoding::UnquotedRfc2047 => { - let encoded = qp_encode(&value); + let encoded = qp_encode(value.to_str().expect("FIXME: bytes")); result.push_str(&format!(";\r\n\t{name}={encoded}")); } MimeParameterEncoding::QuotedRfc2047 => { - let encoded = qp_encode(&value); + let encoded = qp_encode(value.to_str().expect("FIXME: bytes")); result.push_str(&format!(";\r\n\t{name}=\"{encoded}\"")); } MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => { @@ -1956,7 +1965,7 @@ impl EncodeHeaderValue for MimeParameters { .all(|c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii()); let mut params = vec![]; - let mut chars = value.chars().peekable(); + let mut chars = value.char_indices().peekable(); while chars.peek().is_some() { let count = params.len(); let is_first = count == 0; @@ -1969,51 +1978,45 @@ impl EncodeHeaderValue for MimeParameters { }; let limit = 74 - (name.len() + 4 + prefix.len()); - let mut encoded = String::new(); + let mut encoded: Vec = vec![]; while encoded.len() < limit { - let c = match chars.next() { - Some(c) => c, - None => break, + let Some((start, end, c)) = chars.next() else { + break; }; + let s = &value[start..end]; if use_quoted_string { if c == '"' || c == '\\' { - encoded.push('\\'); + encoded.push(b'\\'); } - encoded.push(c); + encoded.push_str(s); } else if is_mime_token(c) && (!needs_encoding || c != '%') { - encoded.push(c); + encoded.push_str(s); } else { - let mut buf = [0u8; 8]; - let s = c.encode_utf8(&mut buf); for b in s.bytes() { - encoded.push('%'); - encoded.push(HEX_CHARS[(b as usize) >> 4] as char); - encoded.push(HEX_CHARS[(b as usize) & 0x0f] as char); + encoded.push(b'%'); + encoded.push(HEX_CHARS[(b as usize) >> 4]); + encoded.push(HEX_CHARS[(b as usize) & 0x0f]); } } } if use_quoted_string { - encoded.push('"'); + encoded.push(b'"'); } params.push(MimeParameter { - name: name.to_string(), + name: name.into(), section: Some(count as u32), - mime_charset: if is_first { - Some("UTF-8".to_string()) - } else { - None - }, + mime_charset: if is_first { Some("UTF-8".into()) } else { None }, mime_language: None, encoding: if needs_encoding { MimeParameterEncoding::Rfc2231 } else { MimeParameterEncoding::None }, - value: encoded, + value: encoded.into(), }) } if params.len() == 1 { @@ -2048,11 +2051,18 @@ impl EncodeHeaderValue for MimeParameters { "" }; let charset = if use_quoted_string { - "\"" + BStr::new("\"") } else { - p.mime_charset.as_deref().unwrap_or("") + p.mime_charset + .as_ref() + .map(|b| b.as_bstr()) + .unwrap_or(BStr::new("")) }; - let lang = p.mime_language.as_deref().unwrap_or(""); + let lang = p + .mime_language + .as_ref() + .map(|b| b.as_bstr()) + .unwrap_or(BStr::new("")); let line = format!( "{name}{section}{uses_encoding}={charset}{charset_tick}{lang}{lang_tick}{value}", @@ -2070,6 +2080,7 @@ impl EncodeHeaderValue for MimeParameters { static HEX_CHARS: &[u8] = b"0123456789ABCDEF"; +#[deprecated = "revise to bytes"] pub(crate) fn qp_encode(s: &str) -> String { let prefix = b"=?UTF-8?q?"; let suffix = b"?="; diff --git a/crates/mod-mimepart/Cargo.toml b/crates/mod-mimepart/Cargo.toml index 4f33cf65..b84d384f 100644 --- a/crates/mod-mimepart/Cargo.toml +++ b/crates/mod-mimepart/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] anyhow.workspace = true +bstr.workspace = true config = {path="../config"} mailparsing = {path="../mailparsing"} mlua = {workspace=true, features=["vendored", "macros", "lua54", "async", "send", "serialize"]} diff --git a/crates/mod-mimepart/src/headers.rs b/crates/mod-mimepart/src/headers.rs index 366afe5d..112edadd 100644 --- a/crates/mod-mimepart/src/headers.rs +++ b/crates/mod-mimepart/src/headers.rs @@ -1,4 +1,5 @@ use crate::PartRef; +use bstr::BString; use config::{SerdeWrappedValue, any_err}; use mailparsing::{ AddressList, Header, HeaderMap, MailParsingError, Mailbox, MailboxList, MessageID, @@ -338,8 +339,8 @@ impl UserData for HeaderMapRef { #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MimeParams { - pub value: String, - pub parameters: BTreeMap, + pub value: BString, + pub parameters: BTreeMap, } impl From for MimeParams { diff --git a/crates/mod-mimepart/src/lib.rs b/crates/mod-mimepart/src/lib.rs index add617ac..1e4d4dd8 100644 --- a/crates/mod-mimepart/src/lib.rs +++ b/crates/mod-mimepart/src/lib.rs @@ -51,8 +51,12 @@ fn new_multipart( child_parts.push(p.resolve().map_err(any_err)?.to_owned()); } - let part = MimePart::new_multipart(&content_type, child_parts, boundary.as_deref()) - .map_err(any_err)?; + let part = MimePart::new_multipart( + &content_type, + child_parts, + boundary.as_ref().map(|s| s.as_bytes()), + ) + .map_err(any_err)?; Ok(PartRef::new(part)) } diff --git a/crates/mod-mimepart/src/mimepart.rs b/crates/mod-mimepart/src/mimepart.rs index 36a82286..fcc837bb 100644 --- a/crates/mod-mimepart/src/mimepart.rs +++ b/crates/mod-mimepart/src/mimepart.rs @@ -1,3 +1,4 @@ +use bstr::{BStr, BString, ByteSlice}; use config::any_err; use mailparsing::{DecodedBody, MimePart, PartPointer}; use mlua::{MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef}; @@ -65,7 +66,7 @@ impl PartRef { pub fn replace_body( &self, body: mlua::String, - mut content_type: Option, + mut content_type: Option, ) -> anyhow::Result<()> { self.mutate(|part| { if content_type.is_none() { @@ -76,13 +77,20 @@ impl PartRef { match body.to_str() { Ok(s) => { - part.replace_text_body(content_type.as_deref().unwrap_or("text/plain"), &*s)?; + part.replace_text_body( + content_type + .as_ref() + .map(|b| b.as_bstr()) + .unwrap_or_else(|| BStr::new("text/plain")), + &*s, + )?; } _ => { part.replace_binary_body( content_type - .as_deref() - .unwrap_or("application/octet-stream"), + .as_ref() + .map(|b| b.as_bstr()) + .unwrap_or_else(|| BStr::new("application/octet-stream")), &body.as_bytes(), )?; } @@ -175,7 +183,8 @@ impl UserData for PartRef { methods.add_method( "replace_body", move |_lua, this, (body, content_type): (mlua::String, Option)| { - this.replace_body(body, content_type).map_err(any_err) + this.replace_body(body, content_type.map(Into::into)) + .map_err(any_err) }, ); }