#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum HeaderParseError {
EmptyName,
InvalidName,
InvalidValue,
MissingColon,
}
impl fmt::Display for HeaderParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyName => formatter.write_str("email header name cannot be empty"),
Self::InvalidName => formatter.write_str("invalid email header name"),
Self::InvalidValue => formatter.write_str("invalid email header value"),
Self::MissingColon => formatter.write_str("email header line must contain a colon"),
}
}
}
impl Error for HeaderParseError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HeaderName(String);
impl HeaderName {
pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
validate_header_name(value.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for HeaderName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for HeaderName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for HeaderName {
type Err = HeaderParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for HeaderName {
type Error = HeaderParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HeaderValue(String);
impl HeaderValue {
pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
validate_header_value(value.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for HeaderValue {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for HeaderValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for HeaderValue {
type Err = HeaderParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for HeaderValue {
type Error = HeaderParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HeaderField {
name: HeaderName,
value: HeaderValue,
}
impl HeaderField {
pub fn new(name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
Ok(Self {
name: HeaderName::new(name)?,
value: HeaderValue::new(value)?,
})
}
#[must_use]
pub const fn name(&self) -> &HeaderName {
&self.name
}
#[must_use]
pub const fn value(&self) -> &HeaderValue {
&self.value
}
}
impl fmt::Display for HeaderField {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.name, self.value)
}
}
impl FromStr for HeaderField {
type Err = HeaderParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (name, field_value) = value
.split_once(':')
.ok_or(HeaderParseError::MissingColon)?;
Self::new(name, field_value.trim())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HeaderLine(HeaderField);
impl HeaderLine {
#[must_use]
pub const fn new(field: HeaderField) -> Self {
Self(field)
}
#[must_use]
pub const fn field(&self) -> &HeaderField {
&self.0
}
}
impl fmt::Display for HeaderLine {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum HeaderFold {
#[default]
Never,
Recommended,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HeaderBlock {
fields: Vec<HeaderField>,
fold: HeaderFold,
}
impl HeaderBlock {
#[must_use]
pub const fn new() -> Self {
Self {
fields: Vec::new(),
fold: HeaderFold::Never,
}
}
#[must_use]
pub const fn fold(&self) -> HeaderFold {
self.fold
}
#[must_use]
pub const fn with_fold(mut self, fold: HeaderFold) -> Self {
self.fold = fold;
self
}
#[must_use]
pub fn with_field(mut self, field: HeaderField) -> Self {
self.fields.push(field);
self
}
pub fn push(&mut self, field: HeaderField) {
self.fields.push(field);
}
#[must_use]
pub fn fields(&self) -> &[HeaderField] {
&self.fields
}
#[must_use]
pub fn first(&self, name: &str) -> Option<&HeaderField> {
self.fields
.iter()
.find(|field| field.name().as_str().eq_ignore_ascii_case(name))
}
}
impl fmt::Display for HeaderBlock {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, field) in self.fields.iter().enumerate() {
if index > 0 {
formatter.write_str("\r\n")?;
}
write!(formatter, "{field}")?;
}
Ok(())
}
}
macro_rules! typed_header {
($name:ident, $header_name:literal) => {
#[doc = concat!("Lightweight `", $header_name, "` header wrapper.")]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(HeaderValue);
impl $name {
pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
Ok(Self(HeaderValue::new(value)?))
}
#[must_use]
pub const fn name() -> &'static str {
$header_name
}
#[must_use]
pub const fn value(&self) -> &HeaderValue {
&self.0
}
pub fn field(&self) -> HeaderField {
HeaderField::new(Self::name(), self.value().as_str())
.expect("typed header names and stored values are valid")
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.field())
}
}
};
}
typed_header!(From, "From");
typed_header!(To, "To");
typed_header!(Cc, "Cc");
typed_header!(Bcc, "Bcc");
typed_header!(Subject, "Subject");
typed_header!(Date, "Date");
typed_header!(MessageIdHeader, "Message-ID");
typed_header!(InReplyTo, "In-Reply-To");
typed_header!(References, "References");
typed_header!(ReplyTo, "Reply-To");
typed_header!(Sender, "Sender");
typed_header!(ReturnPath, "Return-Path");
typed_header!(Received, "Received");
typed_header!(ContentType, "Content-Type");
typed_header!(ContentTransferEncoding, "Content-Transfer-Encoding");
typed_header!(ContentDisposition, "Content-Disposition");
typed_header!(MimeVersion, "MIME-Version");
fn validate_header_name(value: &str) -> Result<&str, HeaderParseError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(HeaderParseError::EmptyName);
}
if trimmed.bytes().any(|byte| !is_header_name_byte(byte)) {
return Err(HeaderParseError::InvalidName);
}
Ok(trimmed)
}
fn validate_header_value(value: &str) -> Result<&str, HeaderParseError> {
let trimmed = value.trim();
if trimmed.chars().any(|character| {
matches!(character, '\r' | '\n') || (character.is_control() && character != '\t')
}) {
return Err(HeaderParseError::InvalidValue);
}
Ok(trimmed)
}
fn is_header_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
}
#[cfg(test)]
mod tests {
use super::{HeaderBlock, HeaderField, HeaderFold, HeaderParseError, HeaderValue, Subject};
#[test]
fn parses_and_renders_fields() -> Result<(), HeaderParseError> {
let field: HeaderField = "Subject: Quarterly notes".parse()?;
let value = HeaderValue::new("Quarterly notes")?;
assert_eq!(field.name().as_str(), "Subject");
assert_eq!(field.value(), &value);
assert_eq!(field.to_string(), "Subject: Quarterly notes");
Ok(())
}
#[test]
fn typed_headers_create_fields() -> Result<(), HeaderParseError> {
let subject = Subject::new("Hello")?;
assert_eq!(Subject::name(), "Subject");
assert_eq!(subject.field().to_string(), "Subject: Hello");
Ok(())
}
#[test]
fn blocks_find_headers_case_insensitively() -> Result<(), HeaderParseError> {
let block = HeaderBlock::new()
.with_fold(HeaderFold::Recommended)
.with_field(HeaderField::new("From", "jane@example.com")?)
.with_field(HeaderField::new("Subject", "Hello")?);
assert_eq!(block.fold(), HeaderFold::Recommended);
assert_eq!(
block.first("subject").expect("subject").value().as_str(),
"Hello"
);
assert!(block.to_string().contains("\r\nSubject"));
Ok(())
}
#[test]
fn rejects_invalid_headers() {
assert_eq!(
HeaderField::new("Bad Name", "value"),
Err(HeaderParseError::InvalidName)
);
assert_eq!(
HeaderField::new("Subject", "hello\r\nthere"),
Err(HeaderParseError::InvalidValue)
);
assert_eq!(
"Subject without colon".parse::<HeaderField>(),
Err(HeaderParseError::MissingColon)
);
}
}