Skip to main content

use_email_header/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when header primitives fail validation or parsing.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum HeaderParseError {
10    /// The header name was empty.
11    EmptyName,
12    /// The header name contained invalid token characters.
13    InvalidName,
14    /// The header value contained invalid control characters.
15    InvalidValue,
16    /// A header line did not contain a colon separator.
17    MissingColon,
18}
19
20impl fmt::Display for HeaderParseError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::EmptyName => formatter.write_str("email header name cannot be empty"),
24            Self::InvalidName => formatter.write_str("invalid email header name"),
25            Self::InvalidValue => formatter.write_str("invalid email header value"),
26            Self::MissingColon => formatter.write_str("email header line must contain a colon"),
27        }
28    }
29}
30
31impl Error for HeaderParseError {}
32
33/// Email header field name.
34#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct HeaderName(String);
36
37impl HeaderName {
38    /// Creates a validated header name.
39    pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
40        validate_header_name(value.as_ref()).map(|value| Self(value.to_owned()))
41    }
42
43    /// Returns the stored header name.
44    #[must_use]
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl AsRef<str> for HeaderName {
51    fn as_ref(&self) -> &str {
52        self.as_str()
53    }
54}
55
56impl fmt::Display for HeaderName {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        formatter.write_str(self.as_str())
59    }
60}
61
62impl FromStr for HeaderName {
63    type Err = HeaderParseError;
64
65    fn from_str(value: &str) -> Result<Self, Self::Err> {
66        Self::new(value)
67    }
68}
69
70impl TryFrom<&str> for HeaderName {
71    type Error = HeaderParseError;
72
73    fn try_from(value: &str) -> Result<Self, Self::Error> {
74        Self::new(value)
75    }
76}
77
78/// Email header field value.
79#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct HeaderValue(String);
81
82impl HeaderValue {
83    /// Creates a validated single-line header value.
84    pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
85        validate_header_value(value.as_ref()).map(|value| Self(value.to_owned()))
86    }
87
88    /// Returns the stored header value.
89    #[must_use]
90    pub fn as_str(&self) -> &str {
91        &self.0
92    }
93}
94
95impl AsRef<str> for HeaderValue {
96    fn as_ref(&self) -> &str {
97        self.as_str()
98    }
99}
100
101impl fmt::Display for HeaderValue {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        formatter.write_str(self.as_str())
104    }
105}
106
107impl FromStr for HeaderValue {
108    type Err = HeaderParseError;
109
110    fn from_str(value: &str) -> Result<Self, Self::Err> {
111        Self::new(value)
112    }
113}
114
115impl TryFrom<&str> for HeaderValue {
116    type Error = HeaderParseError;
117
118    fn try_from(value: &str) -> Result<Self, Self::Error> {
119        Self::new(value)
120    }
121}
122
123/// Complete single header field.
124#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub struct HeaderField {
126    name: HeaderName,
127    value: HeaderValue,
128}
129
130impl HeaderField {
131    /// Creates a header field from name and value text.
132    pub fn new(name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
133        Ok(Self {
134            name: HeaderName::new(name)?,
135            value: HeaderValue::new(value)?,
136        })
137    }
138
139    /// Returns the header name.
140    #[must_use]
141    pub const fn name(&self) -> &HeaderName {
142        &self.name
143    }
144
145    /// Returns the header value.
146    #[must_use]
147    pub const fn value(&self) -> &HeaderValue {
148        &self.value
149    }
150}
151
152impl fmt::Display for HeaderField {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(formatter, "{}: {}", self.name, self.value)
155    }
156}
157
158impl FromStr for HeaderField {
159    type Err = HeaderParseError;
160
161    fn from_str(value: &str) -> Result<Self, Self::Err> {
162        let (name, field_value) = value
163            .split_once(':')
164            .ok_or(HeaderParseError::MissingColon)?;
165        Self::new(name, field_value.trim())
166    }
167}
168
169/// Header line wrapper.
170#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub struct HeaderLine(HeaderField);
172
173impl HeaderLine {
174    /// Creates a header line.
175    #[must_use]
176    pub const fn new(field: HeaderField) -> Self {
177        Self(field)
178    }
179
180    /// Returns the wrapped field.
181    #[must_use]
182    pub const fn field(&self) -> &HeaderField {
183        &self.0
184    }
185}
186
187impl fmt::Display for HeaderLine {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(formatter, "{}", self.0)
190    }
191}
192
193/// Lightweight folding preference marker.
194#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
195pub enum HeaderFold {
196    /// Never fold when rendering this primitive.
197    #[default]
198    Never,
199    /// Folding would be acceptable to a higher-level renderer.
200    Recommended,
201}
202
203/// Collection of header fields.
204#[derive(Clone, Debug, Default, Eq, PartialEq)]
205pub struct HeaderBlock {
206    fields: Vec<HeaderField>,
207    fold: HeaderFold,
208}
209
210impl HeaderBlock {
211    /// Creates an empty header block.
212    #[must_use]
213    pub const fn new() -> Self {
214        Self {
215            fields: Vec::new(),
216            fold: HeaderFold::Never,
217        }
218    }
219
220    /// Returns the folding preference.
221    #[must_use]
222    pub const fn fold(&self) -> HeaderFold {
223        self.fold
224    }
225
226    /// Sets the folding preference.
227    #[must_use]
228    pub const fn with_fold(mut self, fold: HeaderFold) -> Self {
229        self.fold = fold;
230        self
231    }
232
233    /// Adds a field and returns the updated block.
234    #[must_use]
235    pub fn with_field(mut self, field: HeaderField) -> Self {
236        self.fields.push(field);
237        self
238    }
239
240    /// Appends a field.
241    pub fn push(&mut self, field: HeaderField) {
242        self.fields.push(field);
243    }
244
245    /// Returns all fields.
246    #[must_use]
247    pub fn fields(&self) -> &[HeaderField] {
248        &self.fields
249    }
250
251    /// Finds the first field with the requested header name.
252    #[must_use]
253    pub fn first(&self, name: &str) -> Option<&HeaderField> {
254        self.fields
255            .iter()
256            .find(|field| field.name().as_str().eq_ignore_ascii_case(name))
257    }
258}
259
260impl fmt::Display for HeaderBlock {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        for (index, field) in self.fields.iter().enumerate() {
263            if index > 0 {
264                formatter.write_str("\r\n")?;
265            }
266            write!(formatter, "{field}")?;
267        }
268        Ok(())
269    }
270}
271
272macro_rules! typed_header {
273    ($name:ident, $header_name:literal) => {
274        #[doc = concat!("Lightweight `", $header_name, "` header wrapper.")]
275        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
276        pub struct $name(HeaderValue);
277
278        impl $name {
279            /// Creates a typed header wrapper.
280            pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
281                Ok(Self(HeaderValue::new(value)?))
282            }
283
284            /// Returns the stable header name.
285            #[must_use]
286            pub const fn name() -> &'static str {
287                $header_name
288            }
289
290            /// Returns the header value.
291            #[must_use]
292            pub const fn value(&self) -> &HeaderValue {
293                &self.0
294            }
295
296            /// Converts this wrapper into a header field.
297            pub fn field(&self) -> HeaderField {
298                HeaderField::new(Self::name(), self.value().as_str())
299                    .expect("typed header names and stored values are valid")
300            }
301        }
302
303        impl fmt::Display for $name {
304            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305                write!(formatter, "{}", self.field())
306            }
307        }
308    };
309}
310
311typed_header!(From, "From");
312typed_header!(To, "To");
313typed_header!(Cc, "Cc");
314typed_header!(Bcc, "Bcc");
315typed_header!(Subject, "Subject");
316typed_header!(Date, "Date");
317typed_header!(MessageIdHeader, "Message-ID");
318typed_header!(InReplyTo, "In-Reply-To");
319typed_header!(References, "References");
320typed_header!(ReplyTo, "Reply-To");
321typed_header!(Sender, "Sender");
322typed_header!(ReturnPath, "Return-Path");
323typed_header!(Received, "Received");
324typed_header!(ContentType, "Content-Type");
325typed_header!(ContentTransferEncoding, "Content-Transfer-Encoding");
326typed_header!(ContentDisposition, "Content-Disposition");
327typed_header!(MimeVersion, "MIME-Version");
328
329fn validate_header_name(value: &str) -> Result<&str, HeaderParseError> {
330    let trimmed = value.trim();
331    if trimmed.is_empty() {
332        return Err(HeaderParseError::EmptyName);
333    }
334    if trimmed.bytes().any(|byte| !is_header_name_byte(byte)) {
335        return Err(HeaderParseError::InvalidName);
336    }
337    Ok(trimmed)
338}
339
340fn validate_header_value(value: &str) -> Result<&str, HeaderParseError> {
341    let trimmed = value.trim();
342    if trimmed.chars().any(|character| {
343        matches!(character, '\r' | '\n') || (character.is_control() && character != '\t')
344    }) {
345        return Err(HeaderParseError::InvalidValue);
346    }
347    Ok(trimmed)
348}
349
350fn is_header_name_byte(byte: u8) -> bool {
351    byte.is_ascii_alphanumeric()
352        || matches!(
353            byte,
354            b'!' | b'#'
355                | b'$'
356                | b'%'
357                | b'&'
358                | b'\''
359                | b'*'
360                | b'+'
361                | b'-'
362                | b'.'
363                | b'^'
364                | b'_'
365                | b'`'
366                | b'|'
367                | b'~'
368        )
369}
370
371#[cfg(test)]
372mod tests {
373    use super::{HeaderBlock, HeaderField, HeaderFold, HeaderParseError, HeaderValue, Subject};
374
375    #[test]
376    fn parses_and_renders_fields() -> Result<(), HeaderParseError> {
377        let field: HeaderField = "Subject: Quarterly notes".parse()?;
378        let value = HeaderValue::new("Quarterly notes")?;
379
380        assert_eq!(field.name().as_str(), "Subject");
381        assert_eq!(field.value(), &value);
382        assert_eq!(field.to_string(), "Subject: Quarterly notes");
383        Ok(())
384    }
385
386    #[test]
387    fn typed_headers_create_fields() -> Result<(), HeaderParseError> {
388        let subject = Subject::new("Hello")?;
389
390        assert_eq!(Subject::name(), "Subject");
391        assert_eq!(subject.field().to_string(), "Subject: Hello");
392        Ok(())
393    }
394
395    #[test]
396    fn blocks_find_headers_case_insensitively() -> Result<(), HeaderParseError> {
397        let block = HeaderBlock::new()
398            .with_fold(HeaderFold::Recommended)
399            .with_field(HeaderField::new("From", "jane@example.com")?)
400            .with_field(HeaderField::new("Subject", "Hello")?);
401
402        assert_eq!(block.fold(), HeaderFold::Recommended);
403        assert_eq!(
404            block.first("subject").expect("subject").value().as_str(),
405            "Hello"
406        );
407        assert!(block.to_string().contains("\r\nSubject"));
408        Ok(())
409    }
410
411    #[test]
412    fn rejects_invalid_headers() {
413        assert_eq!(
414            HeaderField::new("Bad Name", "value"),
415            Err(HeaderParseError::InvalidName)
416        );
417        assert_eq!(
418            HeaderField::new("Subject", "hello\r\nthere"),
419            Err(HeaderParseError::InvalidValue)
420        );
421        assert_eq!(
422            "Subject without colon".parse::<HeaderField>(),
423            Err(HeaderParseError::MissingColon)
424        );
425    }
426}