document HeaderMap, body transfer-encoding, fix Examples headings

This commit is contained in:
2026-10-10 20:25:05 +02:00
parent 4860823103
commit d658516288
4 changed files with 120 additions and 11 deletions
+1 -1
View File
@@ -85,4 +85,4 @@ assert!(mail.is_multipart());
## License
GPL-3.0. See [LICENSE](LICENSE).
GPL-3.0. See the `LICENSE` file in the repository.
+30
View File
@@ -11,12 +11,21 @@ use std::mem;
use email_encoding::body::{Encoding, base64};
/// The `Content-Transfer-Encoding` of a body, as defined in RFC 2045 section 6.
///
/// [`Display`] writes the value as it belongs in the header. The values are case-insensitive per
/// RFC 2045 section 6.1, which is why decoding lowercases the header before matching on it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentTransferEncoding {
/// `7bit`: ASCII, short lines, no encoding applied. The default when no header is present, and
/// the only one [`crate::CowMail::set_content_raw`] leaves undeclared.
SevenBit,
/// `8bit`: unencoded bytes above 127, in lines short enough to transmit.
EightBit,
/// `quoted-printable`: mostly-ASCII content with the rest escaped as `=XX`.
QuotedPrintable,
/// `base64`: the choice for binary content and for text that is largely non-ASCII.
Base64,
/// `binary`: unencoded bytes with no line-length limit. Never chosen by [`encode`].
Binary,
}
@@ -39,7 +48,9 @@ impl Display for ContentTransferEncoding {
/// always ends up as `base64`.
#[derive(Debug, Clone)]
pub enum BodyContent {
/// Valid UTF-8 text. Line endings are normalized to CRLF by [`encode`].
Text(String),
/// Arbitrary bytes, always base64-encoded.
Binary(Vec<u8>),
}
@@ -79,6 +90,25 @@ impl From<BodyContent> for Vec<u8> {
/// Encode `content`, choosing the most compact of `7bit`, `quoted-printable` and `base64`.
///
/// Line endings of text content are normalized to CRLF.
///
/// # Examples
///
/// ```
/// use email_message::{ContentTransferEncoding, body::encode};
///
/// // Plain ASCII needs no encoding at all.
/// let (bytes, encoding) = encode("hello");
/// assert_eq!(bytes, b"hello");
/// assert_eq!(encoding, ContentTransferEncoding::SevenBit);
///
/// // Line endings are normalized on the way through.
/// let (bytes, _) = encode("one\ntwo");
/// assert_eq!(bytes, b"one\r\ntwo");
///
/// // Bytes are always base64.
/// let (_, encoding) = encode(&b"\x89PNG\r\n\x1a\n"[..]);
/// assert_eq!(encoding, ContentTransferEncoding::Base64);
/// ```
pub fn encode(content: impl Into<BodyContent>) -> (Vec<u8>, ContentTransferEncoding) {
let mut content = content.into();
+89 -8
View File
@@ -10,6 +10,10 @@ use crate::{CowMailError, extend_over_newline};
/// Implementors only have to hand out their headers; everything else is
/// provided. Keys match case-insensitively throughout, and an `index` argument
/// counts occurrences of `key`, not positions in the collection.
///
/// [`headers_mut`](Self::headers_mut) hands out a `&mut Vec<H>` rather than a slice, because
/// inserting and removing go through it — the one constraint this trait puts on an implementor's
/// storage. Implemented for `Vec<H>` and for [`CowMail`](crate::CowMail).
pub trait HeaderMap<'a, H: Header<'a> + 'a> {
/// The headers, in the order they appear in the message.
fn headers(&self) -> &[H];
@@ -18,6 +22,7 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
/// removing go through it.
fn headers_mut(&mut self) -> &mut Vec<H>;
/// Every header with this key, in message order.
fn iter_headers<'b>(&'b self, key: &str) -> impl Iterator<Item = &'b H>
where
H: 'b,
@@ -25,6 +30,7 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
self.headers().iter().filter(move |h| h.key_eq(key))
}
/// Every header with this key, mutably, in message order.
fn iter_headers_mut<'b>(&'b mut self, key: &str) -> impl Iterator<Item = &'b mut H>
where
H: 'b,
@@ -32,7 +38,9 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
self.headers_mut().iter_mut().filter(move |h| h.key_eq(key))
}
/// Examples
/// The decoded value of every header with this key, in message order.
///
/// # Examples
/// ```
/// use email_message::{CowMail, HeaderMap};
/// let mail = CowMail::try_from(concat!(
@@ -53,15 +61,24 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
self.iter_headers(key).map(|h| h.get_value())
}
/// The first header with this key.
///
/// Use [`iter_headers`](Self::iter_headers) when a key can occur more than once, as `Received`
/// does.
fn find_header(&self, key: &str) -> Option<&H> {
self.headers().iter().find(|h| h.key_eq(key))
}
/// The first header with this key, mutably.
fn find_header_mut(&mut self, key: &str) -> Option<&mut H> {
self.headers_mut().iter_mut().find(|h| h.key_eq(key))
}
/// Examples
/// The decoded value of the first header with this key.
///
/// Unfolded and RFC 2047-decoded — see [`Header::get_value`](crate::Header::get_value).
///
/// # Examples
/// ```
/// use email_message::{CowMail, HeaderMap};
/// let mail = CowMail::try_from(concat!(
@@ -76,7 +93,16 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
self.find_header(key).map(|h| h.get_value())
}
/// Examples
/// Append a header, encoding `value` as needed.
///
/// Duplicates are not checked for: adding a key that is already there leaves two of them. Use
/// [`replace_header`](Self::replace_header) to change an existing one.
///
/// # Panics
///
/// If `key` or `value` converts to an empty string.
///
/// # Examples
/// ```
/// use email_message::{CowMail, HeaderMap};
/// let mut mail = CowMail::default();
@@ -87,7 +113,16 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
self.headers_mut().push(H::new(key, value))
}
/// Examples
/// Insert a header at `index` in the header list, encoding `value` as needed.
///
/// Unlike everywhere else in this trait, `index` is a position in the list, not a count of
/// occurrences of `key`. An index past the end appends.
///
/// # Panics
///
/// If `key` or `value` converts to an empty string.
///
/// # Examples
/// ```
/// use email_message::{CowMail, Header, HeaderMap};
/// let mut mail = CowMail::try_from(concat!(
@@ -108,7 +143,15 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
headers.insert(index, header);
}
/// Examples
/// Replace the value of the `index`th header with this key, counting from the first one.
///
/// Does nothing when there is no such occurrence. The key keeps the spelling it had.
///
/// # Panics
///
/// If `value` converts to an empty string.
///
/// # Examples
/// ```
/// use email_message::{CowMail, Header, HeaderMap};
/// let mut mail = CowMail::try_from(concat!(
@@ -139,7 +182,15 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
}
}
/// Examples
/// Replace the value of every header with this key.
///
/// Does nothing when there is none.
///
/// # Panics
///
/// If `value` converts to an empty string.
///
/// # Examples
/// ```
/// use email_message::{CowMail, Header, HeaderMap};
/// let mut mail = CowMail::try_from(concat!(
@@ -164,7 +215,11 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
}
}
/// Examples
/// Remove the `index`th header with this key, counting from the first one.
///
/// Does nothing when there is no such occurrence.
///
/// # Examples
/// ```
/// use email_message::{CowMail, Header, HeaderMap};
/// let mut mail = CowMail::try_from(concat!(
@@ -192,7 +247,9 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
}
}
/// Examples
/// Remove every header with this key.
///
/// # Examples
/// ```
/// use email_message::{CowMail, Header, HeaderMap};
/// let mut mail = CowMail::try_from(concat!(
@@ -211,6 +268,9 @@ pub trait HeaderMap<'a, H: Header<'a> + 'a> {
}
}
/// Implemented for the header list itself, which is what makes `mail.headers.add_header(..)` work
/// alongside `mail.add_header(..)` — useful when the borrow checker objects to going through the
/// message.
impl<'a, H: Header<'a> + 'a> HeaderMap<'a, H> for Vec<H> {
fn headers(&self) -> &[H] {
self
@@ -243,6 +303,27 @@ pub(crate) fn build_headers<'a, H: Header<'a>>(
Ok((headers, extend_over_newline(raw_data, line_end)))
}
/// Parse a header block, borrowing from `raw_data`.
///
/// Returns the headers and the index the body starts at, i.e. just past the blank line that ends
/// the block — or the end of the buffer, for a message that stops at its headers.
///
/// # Errors
///
/// [`CowMailError::MailParseError`] when [`mailparse::parse_headers`] rejects the block.
///
/// # Examples
///
/// ```
/// use email_message::{CowHeader, HeaderMap, parse_headers};
///
/// let raw = b"Subject: Hi\r\nTo: nobody\r\n\r\nbody\r\n";
/// let (headers, body_idx) = parse_headers::<CowHeader>(&raw[..]).unwrap();
///
/// assert_eq!(headers.len(), 2);
/// assert_eq!(headers.find_value("Subject").unwrap(), "Hi");
/// assert_eq!(&raw[body_idx..], b"body\r\n");
/// ```
pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, usize), CowMailError> {
let (parsed, _) = mailparse::parse_headers(raw_data)?;
build_headers(raw_data, &parsed)
-2
View File
@@ -4,9 +4,7 @@
#![warn(missing_debug_implementations)]
#![warn(rustdoc::broken_intra_doc_links)]
/// Body transfer-encoding: choosing one, and the types describing it.
pub mod body;
/// Header lines: the [`Header`] trait, [`CowHeader`](header::CowHeader), and header-value encoding.
pub mod header;
#[cfg(test)]