improve content type handling and code cleanup

This commit is contained in:
2025-02-21 15:07:01 +01:00
parent e5e2004464
commit cf9b3573eb
2 changed files with 32 additions and 33 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ const fn allowed_char(c: u8) -> bool {
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127 c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
} }
fn encode<'a>(encode_buf: &mut String, writer: &'a mut EmailWriter) { fn encode(encode_buf: &mut String, writer: &mut EmailWriter) {
if encode_buf.is_empty() { if encode_buf.is_empty() {
// nothing to encode // nothing to encode
return; return;
+31 -32
View File
@@ -13,7 +13,8 @@ use mailparse::{
body::Body, parse_content_disposition, parse_content_type, DispositionType, MailParseError, body::Body, parse_content_disposition, parse_content_type, DispositionType, MailParseError,
ParsedContentDisposition, ParsedContentType, ParsedContentDisposition, ParsedContentType,
}; };
use mediatype::{names::*, MediaType, MediaTypeBuf, Name}; pub use mediatype::{names, values, MediaType};
use mediatype::{names::*, values::UTF_8, MediaTypeBuf, Name, ReadParams, WriteParams};
use rand::{distr::Alphanumeric, rng, Rng as _}; use rand::{distr::Alphanumeric, rng, Rng as _};
use std::borrow::Cow; use std::borrow::Cow;
use std::boxed::Box; use std::boxed::Box;
@@ -47,7 +48,6 @@ impl From<MailParseError> for CowMailError {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyType { pub enum BodyType {
RELATED,
HTML, HTML,
PLAIN, PLAIN,
} }
@@ -57,7 +57,6 @@ impl From<BodyType> for Name<'_> {
match btype { match btype {
BodyType::PLAIN => PLAIN, BodyType::PLAIN => PLAIN,
BodyType::HTML => HTML, BodyType::HTML => HTML,
BodyType::RELATED => RELATED,
} }
} }
} }
@@ -238,15 +237,16 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
pub fn set_content<T: ToString>(&mut self, content: T, ctype: Option<MediaType>) { pub fn set_content<T: ToString>(&mut self, content: T, ctype: Option<MediaType>) {
self.clear_content(); self.clear_content();
let body = lettre::message::Body::new(content.to_string()); let content = content.to_string();
let ctype = ctype let mut ctype = ctype.unwrap_or(MediaType::new(TEXT, PLAIN));
.unwrap_or_else(|| MediaType::new(TEXT, PLAIN)) if !content.is_ascii() && ctype.get_param(CHARSET).is_none() {
.essence() ctype.set_param(CHARSET, UTF_8);
.to_string(); }
self.headers
.add_header("Content-Type", format!("{}; charset=\"UTF-8\"", ctype));
self.headers.add_header("Content-Type", ctype);
let body = lettre::message::Body::new(content);
let encoding = body.encoding(); let encoding = body.encoding();
if encoding != ContentTransferEncoding::SevenBit { if encoding != ContentTransferEncoding::SevenBit {
self.headers.add_header("Content-Transfer-Encoding", encoding); self.headers.add_header("Content-Transfer-Encoding", encoding);
@@ -255,7 +255,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
self.body_bytes = Cow::Owned(body.into_vec()); self.body_bytes = Cow::Owned(body.into_vec());
} }
pub fn set_content_raw<B: Into<MaybeString>, T: ToString>(&mut self, content: B, ctype: Option<T>) { pub fn set_content_raw<B: Into<MaybeString>>(&mut self, content: B, ctype: Option<MediaType>) {
self.clear_content(); self.clear_content();
let body = lettre::message::Body::new(content); let body = lettre::message::Body::new(content);
@@ -292,10 +292,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
Ok(()) Ok(())
} }
fn contains_body(&self, mime: &MediaTypeBuf, subty: &Name) -> Option<bool> { fn contains_body(&self, mime: MediaType, bodytype: BodyType) -> Option<bool> {
match self.is_multipart() { match self.is_multipart() {
false => Some(!self.is_attachment() && mime.ty() == TEXT && mime.subty() == *subty), false => Some(!self.is_attachment() && mime.ty == TEXT && mime.subty == Name::from(bodytype)),
true if *subty == RELATED && mime.subty() == RELATED => Some(true),
_ if self.subparts.is_empty() => Some(false), _ if self.subparts.is_empty() => Some(false),
_ => None, _ => None,
} }
@@ -313,10 +312,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
} }
pub fn get_body_part(&self, bodytype: BodyType) -> Option<&CowMail<'a, H>> { pub fn get_body_part(&self, bodytype: BodyType) -> Option<&CowMail<'a, H>> {
let subty = Name::from(bodytype);
let mime = self.get_mime_type(); let mime = self.get_mime_type();
match self.contains_body(&mime, &subty) { match self.contains_body(mime.essence(), bodytype) {
Some(res) => match res { Some(res) => match res {
true => Some(self), true => Some(self),
false => None, false => None,
@@ -333,9 +331,8 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
} }
pub fn get_body_part_mut(&mut self, bodytype: BodyType) -> Option<&mut CowMail<'a, H>> { pub fn get_body_part_mut(&mut self, bodytype: BodyType) -> Option<&mut CowMail<'a, H>> {
let subty = Name::from(bodytype);
let mime = self.get_mime_type(); let mime = self.get_mime_type();
match self.contains_body(&mime, &subty) { match self.contains_body(mime.essence(), bodytype) {
Some(res) => match res { Some(res) => match res {
true => Some(self), true => Some(self),
false => None, false => None,
@@ -455,11 +452,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
fn contains_plain_or_html_body(&self, mime: &MediaTypeBuf) -> Option<bool> { fn contains_plain_or_html_body(&self, mime: &MediaTypeBuf) -> Option<bool> {
match self.is_multipart() { match self.is_multipart() {
false => Some( false => {
!self.is_attachment() Some(!self.is_attachment() && mime.ty() == TEXT && (mime.subty() == PLAIN || mime.subty() == HTML))
&& mime.ty() == TEXT }
&& (mime.subty() == Name::from(BodyType::PLAIN) || mime.subty() == Name::from(BodyType::HTML)),
),
true if self.subparts.is_empty() => Some(false), true if self.subparts.is_empty() => Some(false),
true => None, true => None,
} }
@@ -890,7 +885,7 @@ fn parse_mail_recursive<'a, H: Header<'a> + 'a>(
default_ctype, default_ctype,
}; };
if ctype.mimetype.starts_with("multipart/") && ctype.params.get("boundary").is_some() && raw_data.len() > body_idx { if ctype.mimetype.starts_with("multipart/") && ctype.params.contains_key("boundary") && raw_data.len() > body_idx {
let in_multipart_digest = ctype.mimetype == "multipart/digest"; let in_multipart_digest = ctype.mimetype == "multipart/digest";
let boundary = String::from("--") + &ctype.params["boundary"]; let boundary = String::from("--") + &ctype.params["boundary"];
if let Some(body_end_idx) = find_from_u8_line_prefix(raw_data, body_idx, boundary.as_bytes()) { if let Some(body_end_idx) = find_from_u8_line_prefix(raw_data, body_idx, boundary.as_bytes()) {
@@ -961,9 +956,7 @@ fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
//use std::io::{Read, Write}; //use std::io::{Read, Write};
use crate::{names::HTML, names::TEXT, CowHeader, CowMail, Header, HeaderMap, Mail, MediaType};
use crate::{CowHeader, CowMail, Header, HeaderMap, Mail};
use mediatype::{names::*, MediaType};
#[test] #[test]
fn test_header_mutations() { fn test_header_mutations() {
@@ -1032,12 +1025,18 @@ mod tests {
mail.clear_content(); mail.clear_content();
assert!(mail.find_header("Content-Type").is_none()); assert!(mail.find_header("Content-Type").is_none());
assert_eq!(mail.get_content().unwrap(), ""); assert_eq!(mail.get_content().unwrap(), "");
mail.set_content("This is a new test message\n".to_string(), None); mail.set_content("This is a new test message ❤\n".to_string(), None);
let ctype = mail.content_type(); let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain"); assert_eq!(ctype.mimetype, "text/plain");
assert_eq!(ctype.charset, "UTF-8"); assert_eq!(ctype.charset, "UTF-8");
mail.clear_content();
assert_eq!(mail.get_content().unwrap(), "");
mail.set_content("This is a new test message\n".to_string(), None);
let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain");
assert_eq!(ctype.charset, "us-ascii");
mail.add_header("From", "\"John Doe\" <john.doe@example.com>"); mail.add_header("From", "\"John Doe\" <john.doe@example.com>");
let mut html = CowMail::new(); let mut html = CowMail::new();
@@ -1081,18 +1080,18 @@ mod tests {
Content-Type: multipart/alternative;\r\n boundary=\"{alt_boundary}\"\r\n\ Content-Type: multipart/alternative;\r\n boundary=\"{alt_boundary}\"\r\n\
\r\n\ \r\n\
--{alt_boundary}\r\n\ --{alt_boundary}\r\n\
Content-Type: text/plain; charset=\"UTF-8\"\r\n\ Content-Type: text/plain\r\n\
\r\n\ \r\n\
This is a new test message\r\n\ This is a new test message\r\n\
\r\n\ \r\n\
--{alt_boundary}\r\n\ --{alt_boundary}\r\n\
Content-Type: text/html; charset=\"UTF-8\"\r\n\ Content-Type: text/html; charset=UTF-8\r\n\
Content-Transfer-Encoding: quoted-printable\r\n\ Content-Transfer-Encoding: quoted-printable\r\n\
\r\n\ \r\n\
<html><body>This is a new test message =E2=98=BA</body></html>\r\n\ <html><body>This is a new test message =E2=98=BA</body></html>\r\n\
--{alt_boundary}--\r\n\ --{alt_boundary}--\r\n\
--{mixed_boundary}\r\n\ --{mixed_boundary}\r\n\
Content-Type: text/plain; charset=\"UTF-8\"\r\n\ Content-Type: text/plain\r\n\
Content-Disposition: attachment;\r\n filename=\"=?utf-8?b?VGVzdOKYukZpbGUuaHRtbA==?=\"\r\n\ Content-Disposition: attachment;\r\n filename=\"=?utf-8?b?VGVzdOKYukZpbGUuaHRtbA==?=\"\r\n\
\r\n\ \r\n\
This is a test text file\r\n\ This is a test text file\r\n\