mirror of
https://github.com/KumoCorp/kumomta.git
synced 2026-09-13 08:01:18 +00:00
mailparsing: refactor: MimeParameters -> BString
This commit is contained in:
Generated
+1
@@ -4959,6 +4959,7 @@ name = "mod-mimepart"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"config",
|
||||
"mailparsing",
|
||||
"mlua",
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"?=";
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user