2026-10-10 23:09:24 +02:00
2024-06-16 01:48:33 +02:00
2026-10-10 21:20:33 +02:00
2024-06-16 01:48:33 +02:00
2024-06-16 01:48:33 +02:00

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

  • CowMail is the message, and every part of a multipart message is a CowMail as well. Its fields are public: headers, body_bytes and subparts are meant to be edited directly when the provided methods do not fit.
  • The *_orig fields (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_orig is the whole part, subparts included; the other two are the original header block and body on their own.
  • HeaderMap finds, iterates and edits headers by key; keys match case-insensitively and an index argument counts occurrences of that key, not positions in the list. It is implemented for CowMail and for Vec<H>.
  • Mail adds the MIME questions on top: content type, disposition, multipart-ness, and decoding the body.
  • Header abstracts a single header line; CowHeader is 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/digest propagation, 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.

S
Description
A Rust library for parsing, modifying and serializing email messages (RFC 5322 / MIME).
Readme GPL-3.0
812 KiB
Languages
Rust 100%