document crate, split CowMailError, re-export mailparse/mediatype/CowHeader
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
# email-message
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use email_message::{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", "email-message");
|
||||||
|
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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use email_message::{CowMail, HeaderMap, Mail, MediaType, names};
|
||||||
|
|
||||||
|
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(MediaType::new(names::TEXT, names::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`) keep the originally
|
||||||
|
parsed slices even after `body_bytes` has become owned, so the original and the modified message
|
||||||
|
can be compared.
|
||||||
|
- **`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`](https://crates.io/crates/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 [LICENSE](LICENSE).
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
//! Body transfer-encoding.
|
//! Body transfer-encoding.
|
||||||
|
//!
|
||||||
|
//! [`encode`] takes the content of a part and returns the encoded bytes together with the
|
||||||
|
//! [`ContentTransferEncoding`] to declare for them. It picks the most compact of `7bit`,
|
||||||
|
//! `quoted-printable` and `base64`, so a plain ASCII body stays readable in the serialized message
|
||||||
|
//! while binary content ends up base64-encoded.
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! RFC 2047 encoding and folding of header values.
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use email_encoding::headers::writer::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! The [`HeaderMap`] collection trait and header-block parsing.
|
||||||
|
|
||||||
use mailparse::MailHeader;
|
use mailparse::MailHeader;
|
||||||
|
|
||||||
use super::{Header, build_header};
|
use super::{Header, build_header};
|
||||||
|
|||||||
+11
-2
@@ -1,3 +1,13 @@
|
|||||||
|
//! Header lines.
|
||||||
|
//!
|
||||||
|
//! A header is abstracted by the [`Header`] trait, which is generic over the storage so that a
|
||||||
|
//! message can be parsed into whatever representation the caller needs. [`CowHeader`] is the
|
||||||
|
//! default implementation: it keeps the raw line — key, separator, value and trailing newline —
|
||||||
|
//! as a [`Cow`], and becomes owned only when the value is changed.
|
||||||
|
//!
|
||||||
|
//! [`HeaderMap`] is the collection side: finding, iterating and editing headers by key. It is
|
||||||
|
//! implemented for `Vec<H>` and for [`CowMail`](crate::CowMail).
|
||||||
|
|
||||||
mod encoding;
|
mod encoding;
|
||||||
mod headers;
|
mod headers;
|
||||||
|
|
||||||
@@ -255,8 +265,7 @@ fn build_header<'a, H: Header<'a>>(
|
|||||||
next: Option<&MailHeader<'a>>,
|
next: Option<&MailHeader<'a>>,
|
||||||
) -> Result<(H, usize), CowMailError> {
|
) -> Result<(H, usize), CowMailError> {
|
||||||
let offset_of = |bytes: &[u8]| {
|
let offset_of = |bytes: &[u8]| {
|
||||||
offset_in(raw_data, bytes)
|
offset_in(raw_data, bytes).ok_or(CowMailError::HeaderNotASubslice)
|
||||||
.ok_or_else(|| CowMailError::Generic("header is not a slice of its part".to_string()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = parsed.get_key_raw();
|
let key = parsed.get_key_raw();
|
||||||
|
|||||||
+64
-14
@@ -1,40 +1,83 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![warn(missing_debug_implementations)]
|
||||||
|
#![warn(rustdoc::broken_intra_doc_links)]
|
||||||
|
|
||||||
|
/// Body transfer-encoding: choosing one, and the types describing it.
|
||||||
pub mod body;
|
pub mod body;
|
||||||
|
/// Header lines: the [`Header`] trait, [`CowHeader`](header::CowHeader), and header-value encoding.
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use body::{BodyContent, ContentTransferEncoding};
|
pub use body::{BodyContent, ContentTransferEncoding};
|
||||||
use header::{CowHeader, build_headers};
|
pub use header::{CowHeader, Header, HeaderMap, parse_header, parse_headers};
|
||||||
pub use header::{Header, HeaderMap, parse_headers};
|
use header::build_headers;
|
||||||
|
|
||||||
use mailparse::{
|
/// Re-exported from [`mailparse`], which parses the MIME structure for this crate: these types
|
||||||
DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType, ParsedMail, body::Body,
|
/// appear in this crate's public API, so depending on `mailparse` separately is neither necessary
|
||||||
parse_content_disposition, parse_content_type,
|
/// nor advisable — the version re-exported here is the one these signatures refer to.
|
||||||
};
|
pub use mailparse::{DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType};
|
||||||
|
use mailparse::{ParsedMail, body::Body, parse_content_disposition, parse_content_type};
|
||||||
|
|
||||||
|
/// Re-exported from [`mediatype`]. [`MediaType`] is the content type accepted by
|
||||||
|
/// [`CowMail::set_content`] and [`CowMail::set_content_raw`], and [`names`] / [`values`] hold the
|
||||||
|
/// typed constants to build one with, so depending on `mediatype` separately is neither necessary
|
||||||
|
/// nor advisable.
|
||||||
pub use mediatype::{MediaType, names, values};
|
pub use mediatype::{MediaType, names, values};
|
||||||
use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8};
|
use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8};
|
||||||
use rand::{distr::Alphanumeric, rng, RngExt};
|
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
/// Everything that can go wrong while parsing or modifying a message.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CowMailError {
|
pub enum CowMailError {
|
||||||
Generic(String),
|
/// mailparse failed to parse the message. Carries its error unchanged.
|
||||||
MailParseError(MailParseError),
|
MailParseError(MailParseError),
|
||||||
|
/// A part declares a `multipart/*` content type without a `boundary` parameter.
|
||||||
|
///
|
||||||
|
/// Only reachable from [`parse_mail`], and only for a part mailparse did split into subparts —
|
||||||
|
/// which it cannot do without a boundary. In other words: a broken invariant of the bridge to
|
||||||
|
/// mailparse, not malformed input.
|
||||||
|
MissingBoundary,
|
||||||
|
/// A header line mailparse scanned does not point into the part it was scanned from.
|
||||||
|
///
|
||||||
|
/// Header positions are recovered by comparing pointers (see the crate's `offset_in`), which
|
||||||
|
/// relies on every key and value being an exact subslice of its part. A mailparse that stops
|
||||||
|
/// doing that surfaces here instead of as a wrong index.
|
||||||
|
HeaderNotASubslice,
|
||||||
|
/// A subpart mailparse split out does not point into its parent's bytes.
|
||||||
|
///
|
||||||
|
/// The same invariant as [`HeaderNotASubslice`](Self::HeaderNotASubslice), one level up: part
|
||||||
|
/// positions are recovered from `ParsedMail::raw_bytes` being an exact subslice of its
|
||||||
|
/// parent's.
|
||||||
|
SubpartNotASubslice,
|
||||||
|
/// The `Content-Type` header a modification needs is absent.
|
||||||
|
///
|
||||||
|
/// Returned by [`CowMail::set_boundary`], which has no header to put the parameter on.
|
||||||
MissingContentTypeHeader,
|
MissingContentTypeHeader,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for CowMailError {}
|
impl std::error::Error for CowMailError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::MailParseError(error) => Some(error),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for CowMailError {
|
impl std::fmt::Display for CowMailError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Generic(error) => write!(f, "{}", error),
|
|
||||||
Self::MailParseError(error) => write!(f, "{}", error),
|
Self::MailParseError(error) => write!(f, "{}", error),
|
||||||
Self::MissingContentTypeHeader => write!(f, "missing ContentType header"),
|
Self::MissingBoundary => write!(f, "multipart without a boundary parameter"),
|
||||||
|
Self::HeaderNotASubslice => write!(f, "header is not a slice of its part"),
|
||||||
|
Self::SubpartNotASubslice => write!(f, "subpart is not a slice of its parent"),
|
||||||
|
Self::MissingContentTypeHeader => write!(f, "missing Content-Type header"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,6 +774,14 @@ impl<'a> CowMailReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for CowMailReader<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("CowMailReader")
|
||||||
|
.field("current", &self.current)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, H: Header<'a>> From<&'a CowMail<'a, H>> for CowMailReader<'a> {
|
impl<'a, H: Header<'a>> From<&'a CowMail<'a, H>> for CowMailReader<'a> {
|
||||||
fn from(msg: &'a CowMail<'a, H>) -> Self {
|
fn from(msg: &'a CowMail<'a, H>) -> Self {
|
||||||
CowMailReader {
|
CowMailReader {
|
||||||
@@ -849,11 +900,10 @@ fn build_mail<'a, H: Header<'a> + 'a>(
|
|||||||
.ctype
|
.ctype
|
||||||
.params
|
.params
|
||||||
.get("boundary")
|
.get("boundary")
|
||||||
.ok_or_else(|| CowMailError::Generic("multipart without a boundary parameter".to_string()))?;
|
.ok_or(CowMailError::MissingBoundary)?;
|
||||||
|
|
||||||
let offset_of = |part: &ParsedMail<'a>| {
|
let offset_of = |part: &ParsedMail<'a>| {
|
||||||
offset_in(raw_data, part.raw_bytes)
|
offset_in(raw_data, part.raw_bytes).ok_or(CowMailError::SubpartNotASubslice)
|
||||||
.ok_or_else(|| CowMailError::Generic("subpart is not a slice of its parent".to_string()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The preamble ends where the delimiter line before the first part begins.
|
// The preamble ends where the delimiter line before the first part begins.
|
||||||
|
|||||||
Reference in New Issue
Block a user