restructure source and refactor set_text_body and set_html_body

This commit is contained in:
2025-12-15 18:54:30 +01:00
parent d1944d92d1
commit b5b0f2a14f
7 changed files with 379 additions and 325 deletions
+56
View File
@@ -0,0 +1,56 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
```bash
cargo build # Build the library
cargo build --release # Build with optimizations (LTO enabled)
cargo test # Run all tests
cargo test test_name # Run a specific test
cargo doc --open # Generate and view documentation
```
## Architecture
This is a Rust library for parsing, modifying, and serializing email messages (RFC 5322/MIME). It uses copy-on-write semantics to minimize allocations when working with email data.
### Core Types
- **`CowMail<H>`** (`lib.rs`): Main email message struct. Holds headers, body bytes, and nested subparts for multipart messages. Uses `Cow<[u8]>` for zero-copy parsing with owned modifications.
- **`CowMailReader`** (`lib.rs`): Streaming reader implementing `std::io::Read`. Allows efficient serialization without full memory allocation.
- **`CowHeader`** (`header.rs`): Single email header with copy-on-write storage. Tracks byte indices for key/value positions within raw data.
- **`Header` trait** (`header.rs`): Interface for header implementations including parsing, serialization, and RFC 2045 parameter handling.
- **`HeaderMap` trait** (`headers.rs`): Collection operations for headers - find, iterate, add, replace, remove.
- **`Mail` trait** (`lib.rs`): High-level operations for email bodies - content decoding, content-type detection, multipart handling.
- **`CowMailError`** (`lib.rs`): Error type for mail operations (generic errors, parse errors, missing headers).
- **`BodyType`** (`lib.rs`): Enum for body type selection (HTML, PLAIN).
- **`MessageType`** (`lib.rs`): Enum distinguishing SinglePart from MultiPart messages (with boundary).
- **`ContentType`** (`lib.rs`): Wrapper around `ParsedContentType` with conditional defaults for multipart/digest.
### Key Design Patterns
**Copy-on-Write**: Parsed messages borrow from the original byte slice. Only modified fields become owned. The `serialize()` method returns `Vec<Cow<[u8]>>` to preserve this optimization.
**Multipart Handling**: `CowMail` recursively contains `subparts: Vec<CowMail>`. Methods like `make_mixed()`, `make_alternative()`, and `make_related()` transform single-part messages into multipart containers. The original content becomes the first subpart.
**Body Part Navigation**: `get_body_part(BodyType)` traverses the MIME tree to find text/plain or text/html parts, correctly handling `multipart/alternative`, `multipart/related` with `start` parameter, and attachments. The internal `BodyStatus` enum tracks whether a part is a body, not a body, or needs traversal.
### Content Encoding
Body content is automatically encoded/decoded using the `lettre` and `mailparse` crates. `set_content()` automatically selects transfer encoding (quoted-printable for UTF-8 text) and sets appropriate headers.
### High-Level Body Manipulation
- `set_text_body()` / `set_html_body()` / `set_body()`: Intelligently insert or update body parts, creating `multipart/alternative` structures as needed
- These methods use `get_body_parent_part_mut()` to find the correct insertion point in nested MIME structures
+1 -1
View File
@@ -2,7 +2,7 @@
name = "email-message"
version = "0.0.1"
authors = ["Thomas Oettli <spacefreak@noop.ch>"]
edition = "2021"
edition = "2024"
[profile.release]
lto = true
+48
View File
@@ -0,0 +1,48 @@
use std::fmt::Write;
use email_encoding::headers::writer::EmailWriter;
fn allowed_str(s: &str) -> bool {
s.bytes().all(allowed_char)
}
const fn allowed_char(c: u8) -> bool {
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
}
fn encode(encode_buf: &mut String, writer: &mut EmailWriter) {
if encode_buf.is_empty() {
return;
}
let prefix = encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, writer).expect("String is always writable");
// TODO: add a better API for doing this in email-encoding
let spaces = encode_buf.len() - prefix.len();
for _ in 0..spaces {
writer.space();
}
encode_buf.clear();
}
pub fn encode_header_value(key: &str, value: &str) -> String {
let mut encoded_value = String::new();
let line_len = key.len() + ": ".len();
let mut writer = EmailWriter::new(&mut encoded_value, line_len, 0, false);
let mut encode_buf = String::new();
for next_word in value.split_inclusive(' ') {
if allowed_str(next_word) {
// This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode
encode(&mut encode_buf, &mut writer);
writer.folding().write_str(next_word).expect("String is always writable");
} else {
// This word contains unallowed characters
encode_buf.push_str(next_word);
}
}
encode(&mut encode_buf, &mut writer);
drop(writer);
encoded_value
}
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::header::{Header, parse_header};
use super::{Header, parse_header};
pub trait HeaderMap<'a, H: Header<'a> + 'a> {
fn iter_headers(&'a self, key: &str) -> impl Iterator<Item = &'a H>;
@@ -110,7 +110,7 @@ pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, u
return Err("Headers were followed by an unexpected lone CR character!");
}
}
let (header, ix_next) = parse_header(&raw_data[ix..]).unwrap();
let (header, ix_next) = parse_header(&raw_data[ix..])?;
headers.push(header);
ix += ix_next;
}
+8 -53
View File
@@ -1,60 +1,15 @@
mod encoding;
mod headers;
pub use encoding::encode_header_value;
pub use headers::{HeaderMap, parse_headers};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{self, Display, Write};
use std::fmt::{self, Display};
use charset::decode_latin1;
use email::rfc2045::Rfc2045Parser;
use email::FromHeader;
use email_encoding::headers::writer::EmailWriter;
fn allowed_str(s: &str) -> bool {
s.bytes().all(allowed_char)
}
const fn allowed_char(c: u8) -> bool {
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
}
fn encode(encode_buf: &mut String, writer: &mut EmailWriter) {
if encode_buf.is_empty() {
// nothing to encode
return;
}
let prefix = encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, writer).expect("String is always writable");
// TODO: add a better API for doing this in email-encoding
let spaces = encode_buf.len() - prefix.len();
for _ in 0..spaces {
writer.space();
}
encode_buf.clear();
}
fn encode_header_value(key: &str, value: &str) -> String {
let mut encoded_value = String::new();
let line_len = key.len() + ": ".len();
let mut writer = EmailWriter::new(&mut encoded_value, line_len, 0, false);
let mut encode_buf = String::new();
for next_word in value.split_inclusive(' ') {
if allowed_str(next_word) {
// This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode
encode(&mut encode_buf, &mut writer);
writer.folding().write_str(next_word).expect("String is always writable");
} else {
// This word contains unallowed characters
encode_buf.push_str(next_word);
}
}
encode(&mut encode_buf, &mut writer);
drop(writer);
encoded_value
}
use email::{FromHeader, rfc2045::Rfc2045Parser};
pub trait Header<'a>: Clone + Display {
fn new<K: ToString, V: ToString>(key: K, value: V) -> Self;
+102 -271
View File
@@ -1,21 +1,22 @@
#![forbid(unsafe_code)]
pub mod header;
pub mod headers;
use header::CowHeader;
pub use header::Header;
use headers::parse_headers;
pub use headers::HeaderMap;
use lettre::message::header::ContentTransferEncoding;
#[cfg(test)]
mod tests;
use header::CowHeader;
pub use header::{Header, HeaderMap, parse_headers};
use lettre::message::MaybeString;
use lettre::message::header::ContentTransferEncoding;
use mailparse::{
body::Body, parse_content_disposition, parse_content_type, DispositionType, MailParseError,
ParsedContentDisposition, ParsedContentType,
DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType, body::Body,
parse_content_disposition, parse_content_type,
};
pub use mediatype::{names, values, MediaType};
use mediatype::{names::*, values::UTF_8, MediaTypeBuf, Name, ReadParams, WriteParams};
use rand::{distr::Alphanumeric, rng, Rng as _};
pub use mediatype::{MediaType, names, values};
use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8};
use rand::{Rng as _, distr::Alphanumeric, rng};
use std::borrow::Cow;
use std::boxed::Box;
use std::ops::Deref;
@@ -131,11 +132,11 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
match ctype.mimetype.parse::<MediaTypeBuf>() {
Ok(mtype) => {
let mut res = MessageType::SinglePart;
if mtype.ty() == MULTIPART {
if let Some(boundary) = ctype.params.get("boundary") {
if mtype.ty() == MULTIPART
&& let Some(boundary) = ctype.params.get("boundary")
{
res = MessageType::MultiPart(boundary.clone());
}
}
res
}
Err(_) => MessageType::SinglePart,
@@ -194,7 +195,7 @@ pub fn generate_boundary() -> String {
}
/// Result of checking whether a MIME part is a text body (plain/html).
enum BodyStatus {
enum BodyCheckStatus {
/// This part is a text/plain or text/html body (not an attachment)
IsBody,
/// This is a leaf node but not a body (attachment, other content type, etc.)
@@ -300,14 +301,14 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
}
/// Check if this MIME part is a text body (plain/html), not a body, or needs traversal.
fn body_status(&self, bodytype: Option<BodyType>) -> BodyStatus {
fn check_body_status(&self, bodytype: Option<BodyType>) -> BodyCheckStatus {
match self.is_multipart() {
true if self.subparts.is_empty() => BodyStatus::NotBody,
true => BodyStatus::NeedsTraversal,
true if self.subparts.is_empty() => BodyCheckStatus::NotBody,
true => BodyCheckStatus::NeedsTraversal,
false => {
let mime = self.get_mime_type();
if mime.ty() != TEXT || self.is_attachment() {
return BodyStatus::NotBody;
return BodyCheckStatus::NotBody;
}
let body_types = match bodytype {
@@ -316,8 +317,8 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
};
match body_types.contains(&mime.subty()) {
true => BodyStatus::IsBody,
false => BodyStatus::NotBody,
true => BodyCheckStatus::IsBody,
false => BodyCheckStatus::NotBody,
}
}
}
@@ -335,30 +336,30 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
}
pub fn get_body_part(&self, bodytype: BodyType) -> Option<&CowMail<'a, H>> {
match self.body_status(Some(bodytype)) {
BodyStatus::IsBody => Some(self),
BodyStatus::NotBody => None,
BodyStatus::NeedsTraversal => {
if self.get_mime_type().subty() == RELATED {
if let Some(candidate) = self.get_related_candidate() {
match self.check_body_status(Some(bodytype)) {
BodyCheckStatus::IsBody => Some(self),
BodyCheckStatus::NotBody => None,
BodyCheckStatus::NeedsTraversal => {
if self.get_mime_type().subty() == RELATED
&& let Some(candidate) = self.get_related_candidate()
{
return self.subparts[candidate].get_body_part(bodytype);
}
}
self.subparts.iter().find_map(|p| p.get_body_part(bodytype))
}
}
}
pub fn get_body_part_mut(&mut self, bodytype: BodyType) -> Option<&mut CowMail<'a, H>> {
match self.body_status(Some(bodytype)) {
BodyStatus::IsBody => Some(self),
BodyStatus::NotBody => None,
BodyStatus::NeedsTraversal => {
if self.get_mime_type().subty() == RELATED {
if let Some(candidate) = self.get_related_candidate() {
match self.check_body_status(Some(bodytype)) {
BodyCheckStatus::IsBody => Some(self),
BodyCheckStatus::NotBody => None,
BodyCheckStatus::NeedsTraversal => {
if self.get_mime_type().subty() == RELATED
&& let Some(candidate) = self.get_related_candidate()
{
return self.subparts[candidate].get_body_part_mut(bodytype);
}
}
self.subparts.iter_mut().find_map(|p| p.get_body_part_mut(bodytype))
}
}
@@ -467,10 +468,10 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
for i in subpart_indices {
let subpart = &self.subparts[i];
match subpart.body_status(bodytype) {
BodyStatus::IsBody => return Some(path.clone()),
BodyStatus::NotBody => continue,
BodyStatus::NeedsTraversal => {
match subpart.check_body_status(bodytype) {
BodyCheckStatus::IsBody => return Some(path.clone()),
BodyCheckStatus::NotBody => continue,
BodyCheckStatus::NeedsTraversal => {
path.push(i);
match subpart.find_body_parent_path(path, bodytype) {
None => {
@@ -486,10 +487,10 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
}
fn get_body_parent_part_mut(&mut self) -> Option<&mut CowMail<'a, H>> {
match self.body_status(None) {
BodyStatus::IsBody => Some(self),
BodyStatus::NotBody => None,
BodyStatus::NeedsTraversal => match self.find_body_parent_path(&mut Vec::new(), None) {
match self.check_body_status(None) {
BodyCheckStatus::IsBody => Some(self),
BodyCheckStatus::NotBody => None,
BodyCheckStatus::NeedsTraversal => match self.find_body_parent_path(&mut Vec::new(), None) {
Some(path) => {
let mut current = self;
for idx in path {
@@ -502,25 +503,48 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
}
}
pub fn set_text_body<T: ToString>(&mut self, content: T) -> Result<(), CowMailError> {
if let Some(parent) = self.get_body_parent_part_mut() {
match parent.get_body_part_mut(BodyType::PLAIN) {
Some(part) => part.set_content(content, None),
None => {
let mut text_part = Self::new();
text_part.set_content(content, None);
if parent.get_mime_type().subty() == ALTERNATIVE {
parent.subparts.insert(0, text_part);
} else {
let html_part = parent
.get_body_part_mut(BodyType::HTML)
.ok_or(CowMailError::Generic("get_body_parent_part_mut returned a parent without a plain or html body, this should never happen!".into()))?;
html_part.make_alternative(None);
html_part.subparts.insert(0, text_part);
}
}
}
fn set_body_part<T: ToString>(&mut self, content: T, body_type: BodyType) -> Result<(), CowMailError> {
let ctype = match body_type {
BodyType::PLAIN => None,
BodyType::HTML => Some(MediaType::new(TEXT, HTML)),
};
if let Some(parent) = self.get_body_parent_part_mut() {
match parent.get_body_part_mut(body_type) {
Some(part) => part.set_content(content, ctype),
None => {
let mut new_part = Self::new();
new_part.set_content(content, ctype);
let parent_mime = parent.get_mime_type();
let parent_subty = parent_mime.subty();
if parent_subty == ALTERNATIVE {
match body_type {
BodyType::PLAIN => parent.subparts.insert(0, new_part),
BodyType::HTML => parent.subparts.push(new_part),
}
} else if parent_subty == RELATED {
parent.make_alternative(None);
match body_type {
BodyType::PLAIN => parent.subparts.insert(0, new_part),
BodyType::HTML => parent.subparts.push(new_part),
}
} else {
let other_type = match body_type {
BodyType::PLAIN => BodyType::HTML,
BodyType::HTML => BodyType::PLAIN,
};
let other_part = parent.get_body_part_mut(other_type).ok_or(CowMailError::Generic(
"get_body_parent_part_mut returned a parent without a plain or html body".into(),
))?;
other_part.make_alternative(None);
match body_type {
BodyType::PLAIN => other_part.subparts.insert(0, new_part),
BodyType::HTML => other_part.subparts.push(new_part),
}
}
}
}
return Ok(());
}
@@ -528,44 +552,19 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
self.make_mixed(None);
}
let mut text_part = Self::new();
text_part.set_content(content, None);
self.subparts.insert(0, text_part);
let mut new_part = Self::new();
new_part.set_content(content, ctype);
self.subparts.insert(0, new_part);
Ok(())
}
pub fn set_text_body<T: ToString>(&mut self, content: T) -> Result<(), CowMailError> {
self.set_body_part(content, BodyType::PLAIN)
}
pub fn set_html_body<T: ToString>(&mut self, content: T) -> Result<(), CowMailError> {
if let Some(parent) = self.get_body_parent_part_mut() {
match parent.get_body_part_mut(BodyType::HTML) {
Some(part) => part.set_content(content, Some(MediaType::new(TEXT, HTML))),
None => {
let mut html_part = Self::new();
html_part.set_content(content, Some(MediaType::new(TEXT, HTML)));
if parent.get_mime_type().subty() == ALTERNATIVE {
parent.subparts.push(html_part);
} else {
let text_part = parent
.get_body_part_mut(BodyType::PLAIN)
.ok_or(CowMailError::Generic("get_body_parent_part_mut returned a parent without a plain or html body, this should never happen!".into()))?;
text_part.make_alternative(None);
text_part.subparts.push(html_part);
}
}
}
return Ok(());
}
if !self.is_multipart() {
self.make_mixed(None);
}
let mut html_part = Self::new();
html_part.set_content(content, Some(MediaType::new(TEXT, HTML)));
self.subparts.insert(0, html_part);
Ok(())
self.set_body_part(content, BodyType::HTML)
}
pub fn set_body<T: ToString>(&mut self, text: T, html: T) -> Result<(), CowMailError> {
@@ -924,19 +923,6 @@ fn parse_mail_recursive<'a, H: Header<'a> + 'a>(
Ok(mail)
}
// Like find_from_u8, but additionally filters such that `key` is at the start
// of a line (preceded by `\n`) or at the start of the search space.
fn find_from_u8_line_prefix(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
let mut start = ix_start;
while let Some(ix) = find_from_u8(line, start, key) {
if ix == ix_start || line[ix - 1] == b'\n' {
return Some(ix);
}
start = ix + 1;
}
None
}
fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
assert!(!key.is_empty());
assert!(ix_start < line.len());
@@ -963,170 +949,15 @@ fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
None
}
#[cfg(test)]
mod tests {
//use std::io::{Read, Write};
use crate::{names::HTML, names::TEXT, CowHeader, CowMail, Header, HeaderMap, Mail, MediaType};
#[test]
fn test_header_mutations() {
let mut mail = CowMail::<CowHeader>::new();
assert_eq!(mail.headers.len(), 0);
let test_headers = vec![
("X-Test", "Test"),
("X-Another-Test", "Another"),
("X-Test", "Test2"),
("X-Another-Test", "Another1"),
("X-Test", "Test3"),
];
for (key, value) in test_headers.iter() {
mail.add_header(key, value);
/// Like find_from_u8, but additionally filters such that `key` is at the start
/// of a line (preceded by `\n`) or at the start of the search space.
fn find_from_u8_line_prefix(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
let mut start = ix_start;
while let Some(ix) = find_from_u8(line, start, key) {
if ix == ix_start || line[ix - 1] == b'\n' {
return Some(ix);
}
assert_eq!(mail.headers.len(), test_headers.len());
for (idx, header) in mail.headers.iter().enumerate() {
assert_eq!(header.get_key(), test_headers[idx].0);
assert_eq!(header.get_value(), test_headers[idx].1);
}
mail.insert_header("X-Test", "Test1", 1);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test1", "Test2", "Test3"]
);
mail.replace_all_headers("X-Test", "Test555");
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test555", "Test555", "Test555", "Test555"]
);
mail.replace_header("X-Test", "Test", 0);
mail.replace_header("X-Test", "Test1", 1);
mail.replace_header("X-Test", "Test2", 2);
mail.replace_header("X-Test", "Test3", 3);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test1", "Test2", "Test3"]
);
mail.remove_header("X-Test", 1);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test2", "Test3"]
);
mail.remove_all_headers("X-Test");
assert_eq!(mail.headers.len(), 2);
}
#[test]
fn test_body_mutations() {
let mut mail =
CowMail::try_from(concat!("Content-Type: text/plain\n", "\n", "This is a test message").as_bytes())
.unwrap();
let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain");
assert_eq!(ctype.charset, "us-ascii");
assert_eq!(mail.is_multipart(), false);
assert_eq!(mail.get_content().unwrap(), "This is a test message");
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);
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\" <john.doe@example.com>");
let mut html = CowMail::new();
html.set_content(
"<html><body>This is a new test message ☺</body></html>".to_string(),
Some(MediaType::new(TEXT, HTML)),
);
mail.add_alternative(html);
let alt_boundary = mail.get_boundary().unwrap();
let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "multipart/alternative");
assert_eq!(ctype.params.get("boundary").unwrap(), &alt_boundary);
assert_eq!(
mail.find_header("Content-Type").unwrap().get_value(),
format!("multipart/alternative; boundary=\"{}\"", alt_boundary)
);
let mut attachment = CowMail::new();
attachment.set_content("This is a test text file".to_string(), None);
let mut disp = CowHeader::new("Content-Disposition", "attachment");
disp.set_param("filename", "Test☺File.html");
attachment.headers.push(disp);
mail.add_attachment(attachment);
let mixed_boundary = mail.get_boundary().unwrap();
let encoded = mail
.serialize()
.iter()
.flat_map(|p| p.into_iter())
.cloned()
.collect::<Vec<u8>>();
let expected = format!(
"From: \"John Doe\" <john.doe@example.com>\r\n\
MIME-Version: 1.0\r\n\
Content-Type: multipart/mixed;\r\n boundary=\"{mixed_boundary}\"\r\n\
\r\n\
--{mixed_boundary}\r\n\
Content-Type: multipart/alternative;\r\n boundary=\"{alt_boundary}\"\r\n\
\r\n\
--{alt_boundary}\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-Transfer-Encoding: quoted-printable\r\n\
\r\n\
<html><body>This is a new test message =E2=98=BA</body></html>\r\n\
--{alt_boundary}--\r\n\
--{mixed_boundary}\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\
--{mixed_boundary}--\r\n"
);
let expected = expected.into_bytes();
assert_eq!(encoded, expected);
/*
let mut f = std::fs::File::open("/tmp/test.eml").unwrap();
let mut data = Vec::new();
f.read_to_end(&mut data).unwrap();
let mail = CowMail::try_from(&data[..]).unwrap();
let mut serialized = mail
.serialize()
.iter()
.flat_map(|p| p.into_iter())
.cloned()
.collect::<Vec<u8>>();
assert_eq!(data, serialized);
let mut f = std::fs::File::create("/tmp/test1.eml").unwrap();
f.write_all(&mut serialized).unwrap();
*/
start = ix + 1;
}
None
}
+164
View File
@@ -0,0 +1,164 @@
//use std::io::{Read, Write};
use crate::{CowHeader, CowMail, Header, HeaderMap, Mail, MediaType, names::HTML, names::TEXT};
#[test]
fn test_header_mutations() {
let mut mail = CowMail::<CowHeader>::new();
assert_eq!(mail.headers.len(), 0);
let test_headers = vec![
("X-Test", "Test"),
("X-Another-Test", "Another"),
("X-Test", "Test2"),
("X-Another-Test", "Another1"),
("X-Test", "Test3"),
];
for (key, value) in test_headers.iter() {
mail.add_header(key, value);
}
assert_eq!(mail.headers.len(), test_headers.len());
for (idx, header) in mail.headers.iter().enumerate() {
assert_eq!(header.get_key(), test_headers[idx].0);
assert_eq!(header.get_value(), test_headers[idx].1);
}
mail.insert_header("X-Test", "Test1", 1);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test1", "Test2", "Test3"]
);
mail.replace_all_headers("X-Test", "Test555");
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test555", "Test555", "Test555", "Test555"]
);
mail.replace_header("X-Test", "Test", 0);
mail.replace_header("X-Test", "Test1", 1);
mail.replace_header("X-Test", "Test2", 2);
mail.replace_header("X-Test", "Test3", 3);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test1", "Test2", "Test3"]
);
mail.remove_header("X-Test", 1);
assert_eq!(
mail.iter_values("X-Test").collect::<Vec<_>>(),
["Test", "Test2", "Test3"]
);
mail.remove_all_headers("X-Test");
assert_eq!(mail.headers.len(), 2);
}
#[test]
fn test_body_mutations() {
let mut mail =
CowMail::try_from(concat!("Content-Type: text/plain\n", "\n", "This is a test message").as_bytes())
.unwrap();
let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain");
assert_eq!(ctype.charset, "us-ascii");
assert_eq!(mail.is_multipart(), false);
assert_eq!(mail.get_content().unwrap(), "This is a test message");
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);
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\" <john.doe@example.com>");
let mut html = CowMail::new();
html.set_content(
"<html><body>This is a new test message ☺</body></html>".to_string(),
Some(MediaType::new(TEXT, HTML)),
);
mail.add_alternative(html);
let alt_boundary = mail.get_boundary().unwrap();
let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "multipart/alternative");
assert_eq!(ctype.params.get("boundary").unwrap(), &alt_boundary);
assert_eq!(
mail.find_header("Content-Type").unwrap().get_value(),
format!("multipart/alternative; boundary=\"{}\"", alt_boundary)
);
let mut attachment = CowMail::new();
attachment.set_content("This is a test text file".to_string(), None);
let mut disp = CowHeader::new("Content-Disposition", "attachment");
disp.set_param("filename", "Test☺File.html");
attachment.headers.push(disp);
mail.add_attachment(attachment);
let mixed_boundary = mail.get_boundary().unwrap();
let encoded = mail
.serialize()
.iter()
.flat_map(|p| p.into_iter())
.cloned()
.collect::<Vec<u8>>();
let expected = format!(
"From: \"John Doe\" <john.doe@example.com>\r\n\
MIME-Version: 1.0\r\n\
Content-Type: multipart/mixed;\r\n boundary=\"{mixed_boundary}\"\r\n\
\r\n\
--{mixed_boundary}\r\n\
Content-Type: multipart/alternative;\r\n boundary=\"{alt_boundary}\"\r\n\
\r\n\
--{alt_boundary}\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-Transfer-Encoding: quoted-printable\r\n\
\r\n\
<html><body>This is a new test message =E2=98=BA</body></html>\r\n\
--{alt_boundary}--\r\n\
--{mixed_boundary}\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\
--{mixed_boundary}--\r\n"
);
let expected = expected.into_bytes();
assert_eq!(encoded, expected);
/*
let mut f = std::fs::File::open("/tmp/test.eml").unwrap();
let mut data = Vec::new();
f.read_to_end(&mut data).unwrap();
let mail = CowMail::try_from(&data[..]).unwrap();
let mut serialized = mail
.serialize()
.iter()
.flat_map(|p| p.into_iter())
.cloned()
.collect::<Vec<u8>>();
assert_eq!(data, serialized);
let mut f = std::fs::File::create("/tmp/test1.eml").unwrap();
f.write_all(&mut serialized).unwrap();
*/
}