1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum HeaderParseError {
10 EmptyName,
12 InvalidName,
14 InvalidValue,
16 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct HeaderName(String);
36
37impl HeaderName {
38 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 #[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#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct HeaderValue(String);
81
82impl HeaderValue {
83 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub struct HeaderField {
126 name: HeaderName,
127 value: HeaderValue,
128}
129
130impl HeaderField {
131 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 #[must_use]
141 pub const fn name(&self) -> &HeaderName {
142 &self.name
143 }
144
145 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub struct HeaderLine(HeaderField);
172
173impl HeaderLine {
174 #[must_use]
176 pub const fn new(field: HeaderField) -> Self {
177 Self(field)
178 }
179
180 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
195pub enum HeaderFold {
196 #[default]
198 Never,
199 Recommended,
201}
202
203#[derive(Clone, Debug, Default, Eq, PartialEq)]
205pub struct HeaderBlock {
206 fields: Vec<HeaderField>,
207 fold: HeaderFold,
208}
209
210impl HeaderBlock {
211 #[must_use]
213 pub const fn new() -> Self {
214 Self {
215 fields: Vec::new(),
216 fold: HeaderFold::Never,
217 }
218 }
219
220 #[must_use]
222 pub const fn fold(&self) -> HeaderFold {
223 self.fold
224 }
225
226 #[must_use]
228 pub const fn with_fold(mut self, fold: HeaderFold) -> Self {
229 self.fold = fold;
230 self
231 }
232
233 #[must_use]
235 pub fn with_field(mut self, field: HeaderField) -> Self {
236 self.fields.push(field);
237 self
238 }
239
240 pub fn push(&mut self, field: HeaderField) {
242 self.fields.push(field);
243 }
244
245 #[must_use]
247 pub fn fields(&self) -> &[HeaderField] {
248 &self.fields
249 }
250
251 #[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 pub fn new(value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
281 Ok(Self(HeaderValue::new(value)?))
282 }
283
284 #[must_use]
286 pub const fn name() -> &'static str {
287 $header_name
288 }
289
290 #[must_use]
292 pub const fn value(&self) -> &HeaderValue {
293 &self.0
294 }
295
296 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}