diff --git a/src/header.rs b/src/header.rs index 59f3124..3f28840 100644 --- a/src/header.rs +++ b/src/header.rs @@ -15,7 +15,7 @@ const fn allowed_char(c: u8) -> bool { 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() { // nothing to encode return; diff --git a/src/lib.rs b/src/lib.rs index 397b949..e260cdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,8 @@ use mailparse::{ body::Body, parse_content_disposition, parse_content_type, DispositionType, MailParseError, 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 std::borrow::Cow; use std::boxed::Box; @@ -47,7 +48,6 @@ impl From for CowMailError { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BodyType { - RELATED, HTML, PLAIN, } @@ -57,7 +57,6 @@ impl From for Name<'_> { match btype { BodyType::PLAIN => PLAIN, BodyType::HTML => HTML, - BodyType::RELATED => RELATED, } } } @@ -238,15 +237,16 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> { pub fn set_content(&mut self, content: T, ctype: Option) { self.clear_content(); - let body = lettre::message::Body::new(content.to_string()); + let content = content.to_string(); - let ctype = ctype - .unwrap_or_else(|| MediaType::new(TEXT, PLAIN)) - .essence() - .to_string(); - self.headers - .add_header("Content-Type", format!("{}; charset=\"UTF-8\"", ctype)); + let mut ctype = ctype.unwrap_or(MediaType::new(TEXT, PLAIN)); + if !content.is_ascii() && ctype.get_param(CHARSET).is_none() { + ctype.set_param(CHARSET, UTF_8); + } + self.headers.add_header("Content-Type", ctype); + + let body = lettre::message::Body::new(content); let encoding = body.encoding(); if encoding != ContentTransferEncoding::SevenBit { 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()); } - pub fn set_content_raw, T: ToString>(&mut self, content: B, ctype: Option) { + pub fn set_content_raw>(&mut self, content: B, ctype: Option) { self.clear_content(); let body = lettre::message::Body::new(content); @@ -292,10 +292,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> { Ok(()) } - fn contains_body(&self, mime: &MediaTypeBuf, subty: &Name) -> Option { + fn contains_body(&self, mime: MediaType, bodytype: BodyType) -> Option { match self.is_multipart() { - false => Some(!self.is_attachment() && mime.ty() == TEXT && mime.subty() == *subty), - true if *subty == RELATED && mime.subty() == RELATED => Some(true), + false => Some(!self.is_attachment() && mime.ty == TEXT && mime.subty == Name::from(bodytype)), _ if self.subparts.is_empty() => Some(false), _ => 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>> { - let subty = Name::from(bodytype); let mime = self.get_mime_type(); - match self.contains_body(&mime, &subty) { + match self.contains_body(mime.essence(), bodytype) { Some(res) => match res { true => Some(self), 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>> { - let subty = Name::from(bodytype); let mime = self.get_mime_type(); - match self.contains_body(&mime, &subty) { + match self.contains_body(mime.essence(), bodytype) { Some(res) => match res { true => Some(self), 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 { match self.is_multipart() { - false => Some( - !self.is_attachment() - && mime.ty() == TEXT - && (mime.subty() == Name::from(BodyType::PLAIN) || mime.subty() == Name::from(BodyType::HTML)), - ), + false => { + Some(!self.is_attachment() && mime.ty() == TEXT && (mime.subty() == PLAIN || mime.subty() == HTML)) + } true if self.subparts.is_empty() => Some(false), true => None, } @@ -890,7 +885,7 @@ fn parse_mail_recursive<'a, H: Header<'a> + 'a>( 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 boundary = String::from("--") + &ctype.params["boundary"]; 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 { #[cfg(test)] mod tests { //use std::io::{Read, Write}; - - use crate::{CowHeader, CowMail, Header, HeaderMap, Mail}; - use mediatype::{names::*, MediaType}; + use crate::{names::HTML, names::TEXT, CowHeader, CowMail, Header, HeaderMap, Mail, MediaType}; #[test] fn test_header_mutations() { @@ -1032,12 +1025,18 @@ mod tests { mail.clear_content(); assert!(mail.find_header("Content-Type").is_none()); 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(); assert_eq!(ctype.mimetype, "text/plain"); 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\" "); let mut html = CowMail::new(); @@ -1081,18 +1080,18 @@ mod tests { Content-Type: multipart/alternative;\r\n boundary=\"{alt_boundary}\"\r\n\ \r\n\ --{alt_boundary}\r\n\ - Content-Type: text/plain; charset=\"UTF-8\"\r\n\ + Content-Type: text/plain\r\n\ \r\n\ This is a new test message\r\n\ \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\ \r\n\ This is a new test message =E2=98=BA\r\n\ --{alt_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\ \r\n\ This is a test text file\r\n\