mail_headers/
name.rs

1use std::fmt;
2use soft_ascii_string::SoftAsciiStr;
3
4use internals::grammar::is_ftext;
5
6///
7/// Note: Normally you will never have the need to create a HeaderName instance by
8/// yourself (except maybe for testing). At last as long as you use `def_header!`
9/// for defining custom Headers, which is highly recommended
10///
11#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
12pub struct HeaderName {
13    name: &'static SoftAsciiStr
14}
15
16impl HeaderName {
17    ///
18    /// Be aware, that this library only accepts header names with a letter case,
19    /// that any first character of an alphanumeric part of a header name has to
20    /// be uppercase and all other lowercase. E.g. `Message-Id` is accepted but
21    /// `Message-ID` is rejected, even through both are _semantically_ the same.
22    /// This frees us from doing either case insensitive comparison/hash wrt. hash map
23    /// lookups, or converting all names to upper/lower case.
24    ///
25    pub fn new( name: &'static SoftAsciiStr ) -> Result<Self, InvalidHeaderName> {
26        HeaderName::validate_name( name )?;
27        Ok( HeaderName { name } )
28    }
29
30    pub fn from_ascii_unchecked<B: ?Sized>( name: &'static B ) -> HeaderName
31        where B: AsRef<str>
32    {
33        HeaderName { name: SoftAsciiStr::from_unchecked( name.as_ref() ) }
34    }
35
36    #[inline(always)]
37    pub fn as_ascii_str( &self ) -> &'static SoftAsciiStr {
38        self.name
39    }
40    #[inline(always)]
41    pub fn as_str( &self ) -> &'static str {
42        self.name.as_str()
43    }
44}
45
46impl fmt::Display for HeaderName {
47    fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
48        write!(fter, "{}", self.as_str())
49    }
50}
51
52impl PartialEq<str> for HeaderName {
53    fn eq(&self, other: &str) -> bool {
54        self.name.as_str() == other
55    }
56}
57
58impl PartialEq<SoftAsciiStr> for HeaderName {
59    fn eq(&self, other: &SoftAsciiStr) -> bool {
60        self.name == other
61    }
62}
63
64impl HeaderName {
65
66    /// validates if the header name is valid
67    ///
68    /// by only allowing names in "snake case" no case
69    /// insensitive comparison or case conversion is needed
70    /// for header names
71    fn validate_name(name: &SoftAsciiStr) -> Result<(), InvalidHeaderName> {
72        let mut begin_of_word = true;
73        if name.len() < 1 {
74            return Err(InvalidHeaderName { invalid_name: name.to_owned().into() });
75        }
76
77        for ch in name.as_str().chars() {
78            if !is_ftext( ch ) {
79                return Err(InvalidHeaderName { invalid_name: name.to_owned().into() });
80            }
81            match ch {
82                'a'...'z' => {
83                    if begin_of_word {
84                        return Err(InvalidHeaderName { invalid_name: name.to_owned().into() });
85                    }
86                },
87                'A'...'Z' => {
88                    if begin_of_word {
89                        begin_of_word = false;
90                    } else {
91                        return Err(InvalidHeaderName { invalid_name: name.to_owned().into() });
92                    }
93                },
94                '0'...'9' => {
95                    begin_of_word = false;
96                },
97                ch => {
98                    if ch < '!' || ch > '~' || ch == ':' {
99                        return Err(InvalidHeaderName { invalid_name: name.to_owned().into() });
100                    }
101                    begin_of_word = true;
102                }
103
104            }
105
106        }
107        Ok( () )
108    }
109}
110
111#[derive(Clone, Debug, Fail)]
112#[fail(display = "given name is not a valid header name: {:?}", invalid_name)]
113pub struct InvalidHeaderName {
114    invalid_name: String
115}
116
117
118/// a utility trait allowing us to use type hint structs
119/// in `HeaderMap::{contains, get_untyped}`
120pub trait HasHeaderName {
121    fn get_name(&self) -> HeaderName;
122}
123
124impl HasHeaderName for HeaderName {
125    fn get_name(&self) -> HeaderName {
126        *self
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133
134
135    #[test]
136    fn valide_header_names() {
137        let valid_cases = &[
138            "Date",
139            "Some-Header",
140            "33",
141            "Some34",
142            // even trough they seem wrong the email standard only states
143            // header field names have to be at last one char and can
144            // only consist of printable US-ACII chars without :
145            // meaning e.g. "<3" is as valide as "3*4=12"
146            "-33-",
147            "---",
148            "<3+Who-Cares&44",
149            "(3*4=12)^[{~}]"
150        ];
151        for case in valid_cases.iter() {
152            assert_ok!(
153                HeaderName::validate_name( SoftAsciiStr::from_str( case ).unwrap() ) );
154        }
155    }
156
157    #[test]
158    fn invalide_header_names() {
159        // we only alow "snake case" like names to not have to do
160        // case insensitive comparsion in hashmap lookups
161        let invalid_cases = &[
162            "ID",
163            "DaD",
164            "ans",
165            "all-lower-calse",
166            "ALL-UPPER-CASE",
167            "",
168            "a:b",
169            ":",
170            "-:-",
171            "Message Id",
172            " Leading-Ws",
173            "Message\tId",
174            "Null\0Msg"
175        ];
176        for case in invalid_cases.iter() {
177            assert_err!( HeaderName::validate_name( SoftAsciiStr::from_str( case ).unwrap() ), case );
178        }
179    }
180}
181
182