mailparsing: refactor: MimeParameters -> BString

This commit is contained in:
Wez Furlong
2026-03-28 07:20:30 +00:00
parent 7642c7ee0c
commit 4ede478688
11 changed files with 171 additions and 122 deletions
Generated
+1
View File
@@ -4959,6 +4959,7 @@ name = "mod-mimepart"
version = "0.1.0"
dependencies = [
"anyhow",
"bstr",
"config",
"mailparsing",
"mlua",
+12 -8
View File
@@ -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<BString>,
}
pub(crate) fn content_type(part: &MimePart) -> Option<String> {
pub(crate) fn content_type(part: &MimePart) -> Option<BString> {
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
},
+9 -6
View File
@@ -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,
+1 -1
View File
@@ -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(),
};
+5 -5
View File
@@ -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,
}),
)
+39 -24
View File
@@ -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<BStr>,
) -> 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<BStr>) -> Result<Self> {
pub fn new_text(content_type: impl AsRef<[u8]>, content: impl AsRef<BStr>) -> Result<Self> {
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<Self>,
boundary: Option<&str>,
boundary: Option<&[u8]>,
) -> Result<Self> {
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<Self> {
@@ -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<String>,
pub file_name: Option<BString>,
#[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()),
})
);
}
+80 -69
View File
@@ -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<Span, MimeParameters> {
.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<Span, MimeParameters> {
@@ -1230,7 +1236,7 @@ fn content_transfer_encoding(input: Span) -> IResult<Span, MimeParameters> {
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<Span, MimeParameter> {
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<Span, MimeParameter> {
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<Span, MimeParameter> {
),
),
|(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<Span, MimeParameter> {
)),
),
|(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<Span, MimeParameter> {
} else {
MimeParameterEncoding::None
},
value,
value: value.into(),
},
),
)
@@ -1410,8 +1416,8 @@ fn regular_parameter(input: Span) -> IResult<Span, MimeParameter> {
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<u32>,
pub mime_charset: Option<String>,
pub mime_language: Option<String>,
pub mime_charset: Option<BString>,
pub mime_language: Option<BString>,
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<MimeParameter>,
}
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<String, String> {
pub fn parameter_map(&self) -> BTreeMap<BString, BString> {
let mut map = BTreeMap::new();
fn contains_key_ignore_case(map: &BTreeMap<String, String>, key: &str) -> bool {
fn contains_key_ignore_case(map: &BTreeMap<BString, BString>, 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<String> {
pub fn get(&self, name: impl AsRef<[u8]>) -> Option<BString> {
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<u8> = 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<u8> = 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"?=";
+1
View File
@@ -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"]}
+3 -2
View File
@@ -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<String, String>,
pub value: BString,
pub parameters: BTreeMap<BString, BString>,
}
impl From<MimeParameters> for MimeParams {
+6 -2
View File
@@ -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))
}
+14 -5
View File
@@ -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<String>,
mut content_type: Option<BString>,
) -> 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<String>)| {
this.replace_body(body, content_type).map_err(any_err)
this.replace_body(body, content_type.map(Into::into))
.map_err(any_err)
},
);
}