cow-mail
A Rust library for parsing, modifying and serializing email messages (RFC 5322 / MIME).
Copy-on-write throughout: a parsed message borrows from the buffer it was parsed from, and only the
fields you actually change become owned. Serializing hands back a Vec<Cow<[u8]>> of borrowed and
owned chunks rather than one concatenated buffer, so untouched parts are never copied.
parse → serialize is byte-identical. The separators a message was written with — line endings,
delimiter-line padding, preamble and epilogue — are kept verbatim, so a message can be read,
modified in one place, and written back without disturbing anything else. This matters for signed
messages and for anything that has to pass a message through unchanged.
Quickstart
use cow_mail::{CowMail, HeaderMap, Mail};
let raw = b"Subject: Hello\r\n\
Content-Type: text/plain\r\n\
\r\n\
Hi there\r\n";
let mut mail = CowMail::try_from(&raw[..]).unwrap();
// Read headers and the decoded body.
assert_eq!(mail.find_value("Subject").unwrap(), "Hello");
assert_eq!(mail.get_content().unwrap(), "Hi there");
// An untouched message serializes back to exactly the bytes it was parsed from.
assert_eq!(mail.serialize().concat(), raw.to_vec());
// Modify it.
mail.add_header("X-Mailer", "cow-mail");
mail.set_text_body("Hi again").unwrap();
let out: Vec<u8> = mail.serialize().concat();
assert!(out.starts_with(b"Subject: Hello"));
Building a multipart message from scratch:
use cow_mail::{ContentType, CowMail, HeaderMap, Mail};
let mut mail = CowMail::default();
mail.add_header("Subject", "Report");
mail.set_body("plain text version", "<p>html version</p>").unwrap();
let mut attachment = CowMail::new();
attachment.add_header("Content-Disposition", "attachment; filename=\"report.csv\"");
attachment.set_content("a,b\n1,2\n", Some(ContentType::new("text", "csv")));
mail.add_attachment(attachment);
assert!(mail.is_multipart());
Design
CowMailis the message, and every part of a multipart message is aCowMailas well. Its fields are public:headers,body_bytesandsubpartsare meant to be edited directly when the provided methods do not fit.- The
*_origfields (bytes_orig,header_bytes_orig,body_bytes_orig) are set while parsing and never written to again, so the message as it arrived can be serialized or compared against at any point, however much has been modified in the meantime — no copy has to be kept on the side.bytes_origis the whole part, subparts included; the other two are the original header block and body on their own. HeaderMapfinds, iterates and edits headers by key; keys match case-insensitively and anindexargument counts occurrences of that key, not positions in the list. It is implemented forCowMailand forVec<H>.Mailadds the MIME questions on top: content type, disposition, multipart-ness, and decoding the body.Headerabstracts a single header line;CowHeaderis the default implementation and stores the raw line plus the offsets of its key and value.- MIME structure is decided by
mailparse— boundary matching,multipart/digestpropagation, recursion limits. This crate keeps the separators mailparse discards, which is what makes the round-trip exact.
Limitations
- Anything written by this crate uses CRLF. A message whose headers were rewritten therefore mixes line endings with its untouched parts — that is deliberate: fidelity for what was not touched.
- A message truncated before its closing delimiter gains a
--boundary--on the way out, because its declared structure says there should be one. - A message that ends with its header block and no blank line gains that blank line, because the header/body separator is always written.
License
GPL-3.0. See the LICENSE file in the repository.