mail_headers_ng/header_components/
email.rs

1use std::ops::Deref;
2use std::borrow::Cow;
3use std::str::FromStr;
4
5use failure::Fail;
6use soft_ascii_string::{SoftAsciiStr, SoftAsciiString, SoftAsciiChar};
7
8use media_type::spec::{MimeSpec, Ascii, Internationalized, Modern};
9use quoted_string::quote_if_needed;
10
11use internals::error::{EncodingError, EncodingErrorKind};
12use internals::grammar::{
13    is_ascii,
14    is_atext,
15    is_dtext,
16    is_ws,
17};
18use internals::MailType;
19use internals::encoder::{EncodingWriter, EncodableInHeader};
20use internals::bind::idna;
21use internals::bind::quoted_string::UnquotedDotAtomTextValidator;
22
23use ::{HeaderTryFrom, HeaderTryInto};
24use ::data::{Input, SimpleItem, InnerUtf8 };
25use ::error::ComponentCreationError;
26
27/// an email of the form `local-part@domain`
28/// corresponds to RFC5322 addr-spec, so `<`, `>` padding is _not_
29/// part of this Email type (but of the Mailbox type instead)
30#[derive(Debug,  Clone, Hash, PartialEq, Eq)]
31pub struct Email {
32    pub local_part: LocalPart,
33    pub domain: Domain
34}
35
36
37#[derive(Debug,  Clone, Hash, PartialEq, Eq)]
38pub struct LocalPart( Input );
39
40#[derive(Debug,  Clone, Hash, PartialEq, Eq)]
41pub struct Domain( SimpleItem );
42
43impl Email {
44
45    pub fn check_if_internationalized(&self) -> bool {
46        self.local_part.check_if_internationalized()
47    }
48
49    pub fn new<T: HeaderTryInto<Input>>(email: T) -> Result<Self, ComponentCreationError> {
50        let email = email.try_into()?.into_shared();
51        match email {
52            Input( InnerUtf8::Owned( .. ) ) => unreachable!(),
53            Input( InnerUtf8::Shared( shared ) ) => {
54                //1. ownify Input
55                //2. get 2 sub shares split befor/after @
56                let index = shared.find( "@" )
57                    .ok_or_else(|| {
58                        ComponentCreationError::new_with_str("Email", shared.to_string())
59                    })?;
60
61                let left = shared.clone().map( |all| &all[..index] );
62                let local_part = LocalPart::try_from( Input( InnerUtf8::Shared( left ) ) )?;
63                //index+1 is ok as '@'.utf8_len() == 1
64                let right = shared.map( |all| &all[index+1..] );
65                let domain = Domain::try_from( Input( InnerUtf8::Shared( right ) ) )?;
66                Ok( Email { local_part, domain } )
67            }
68        }
69    }
70}
71
72impl LocalPart {
73
74    pub fn check_if_internationalized(&self) -> bool {
75        self.0.as_str().bytes().any(|b| b > 0x7f)
76    }
77}
78
79impl<'a> HeaderTryFrom<&'a str> for Email {
80    fn try_from( email: &str ) -> Result<Self, ComponentCreationError> {
81        Email::new(email)
82    }
83}
84
85impl HeaderTryFrom<String> for Email {
86    fn try_from( email: String ) -> Result<Self, ComponentCreationError> {
87        Email::new(email)
88    }
89}
90
91impl HeaderTryFrom<Input> for Email {
92    fn try_from( email: Input ) -> Result<Self, ComponentCreationError> {
93        Email::new(email)
94    }
95}
96
97
98impl EncodableInHeader for  Email {
99
100    fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
101        self.local_part.encode( handle )?;
102        handle.write_char( SoftAsciiChar::from_unchecked('@') )?;
103        self.domain.encode( handle )?;
104        Ok( () )
105    }
106
107    fn boxed_clone(&self) -> Box<EncodableInHeader> {
108        Box::new(self.clone())
109    }
110}
111
112impl<T> HeaderTryFrom<T> for LocalPart
113    where T: HeaderTryInto<Input>
114{
115
116    fn try_from( input: T ) -> Result<Self, ComponentCreationError> {
117        Ok( LocalPart( input.try_into()? ) )
118    }
119
120}
121
122impl EncodableInHeader for LocalPart {
123
124    fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
125        let input: &str = &*self.0;
126        let mail_type = handle.mail_type();
127
128        let mut validator = UnquotedDotAtomTextValidator::new(mail_type);
129
130        let res =
131            if mail_type.is_internationalized() {
132                quote_if_needed::<MimeSpec<Internationalized, Modern>, _>(input, &mut validator)
133            } else {
134                quote_if_needed::<MimeSpec<Ascii, Modern>, _>(input, &mut validator)
135            }.map_err(|err| EncodingError
136                ::from(err.context(EncodingErrorKind::Malformed))
137                .with_str_context(input)
138            )?;
139
140
141        handle.mark_fws_pos();
142        // if mail_type == Ascii quote_if_needed already made sure it's ascii
143        // it also made sure it is valid as it is either `dot-atom-text` or `quoted-string`
144        handle.write_str_unchecked(&*res)?;
145        handle.mark_fws_pos();
146        Ok( () )
147    }
148
149    fn boxed_clone(&self) -> Box<EncodableInHeader> {
150        Box::new(self.clone())
151    }
152}
153
154impl Deref for LocalPart {
155    type Target = Input;
156
157    fn deref(&self) -> &Self::Target {
158        &self.0
159    }
160}
161
162
163
164impl<T> HeaderTryFrom<T> for Domain
165    where T: HeaderTryInto<Input>
166{
167    fn try_from(input: T) -> Result<Self, ComponentCreationError> {
168        let input = input.try_into()?;
169        let item =
170            match Domain::check_domain(input.as_str())? {
171                MailType::Ascii | MailType::Mime8BitEnabled => {
172                    SimpleItem::Ascii(input.into_ascii_item_unchecked())
173                },
174                MailType::Internationalized => {
175                    SimpleItem::from_utf8_input(input)
176                }
177            };
178
179        Ok(Domain(item))
180    }
181}
182
183impl FromStr for Domain {
184    type Err = ComponentCreationError;
185
186    fn from_str(domain: &str) -> Result<Self, Self::Err> {
187        let input = Input::from(domain);
188        Self::try_from(input)
189    }
190}
191
192impl Domain {
193
194    /// creates a domain from a string without checking for validity
195    pub fn from_unchecked(string: String) -> Self {
196        let item =
197            match SoftAsciiString::from_string(string) {
198                Ok(ascii) => ascii.into(),
199                Err(err) => err.into_source().into()
200            };
201
202        Domain(item)
203    }
204
205    //CONSTRAINT:
206    //  the function is only allowed to return MailType::Ascii
207    //  if the domain is actually ascii
208    fn check_domain( domain: &str ) -> Result<MailType, ComponentCreationError> {
209        if domain.starts_with("[") && domain.ends_with("]") {
210            //TODO improved support for domain literals, e.g. internationalized ones? CRLF? etc.
211            for ch in domain.chars() {
212                if !(is_dtext(ch, MailType::Ascii) || is_ws(ch)) {
213                    let mut err = ComponentCreationError::new("Domain");
214                    err.set_str_context(domain);
215                    return Err(err);
216                }
217            }
218            Ok(MailType::Ascii)
219        } else {
220            let mut ascii = true;
221            let mut dot_alowed = false;
222            for char in domain.chars() {
223                if ascii { ascii = is_ascii( char ) }
224                if char == '.' && dot_alowed {
225                    dot_alowed = false;
226                } else if !is_atext( char, MailType::Internationalized ) {
227                    let mut err = ComponentCreationError::new("Domain");
228                    err.set_str_context(domain);
229                    return Err(err);
230                } else {
231                    dot_alowed = true;
232                }
233            }
234            Ok(if ascii {
235                MailType::Ascii
236            } else {
237                MailType::Internationalized
238            })
239        }
240    }
241
242    pub fn as_str(&self) -> &str {
243        self.0.as_str()
244    }
245
246    pub fn into_ascii_string(self) -> Result<SoftAsciiString, EncodingError> {
247        match self.0 {
248            SimpleItem::Ascii(ascii) => Ok(ascii.into()),
249            SimpleItem::Utf8(utf8) => idna::puny_code_domain(utf8)
250        }
251    }
252
253    pub fn to_ascii_string(&self) -> Result<Cow<SoftAsciiStr>, EncodingError> {
254        Ok(match self.0 {
255            SimpleItem::Ascii(ref ascii) => {
256                Cow::Borrowed(ascii)
257            },
258            SimpleItem::Utf8(ref utf8) => {
259                Cow::Owned(idna::puny_code_domain(utf8)?)
260            }
261        })
262    }
263}
264
265impl EncodableInHeader for  Domain {
266
267    fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
268        handle.mark_fws_pos();
269        match self.0 {
270            SimpleItem::Ascii( ref ascii ) => {
271                handle.write_str( ascii )?;
272            },
273            SimpleItem::Utf8( ref utf8 ) => {
274                handle.write_if_utf8(utf8)
275                    .handle_condition_failure(|handle| {
276                        handle.write_str( &*idna::puny_code_domain( utf8 )? )
277                    })?;
278            }
279        }
280        handle.mark_fws_pos();
281        Ok( () )
282    }
283
284    fn boxed_clone(&self) -> Box<EncodableInHeader> {
285        Box::new(self.clone())
286    }
287}
288
289impl Deref for Domain {
290    type Target = SimpleItem;
291
292    fn deref(&self) -> &Self::Target {
293        &self.0
294    }
295}
296
297
298
299#[cfg(test)]
300mod test {
301    use internals::encoder::EncodingBuffer;
302    use super::*;
303
304    #[test]
305    fn email_try_from() {
306        let email = Email::try_from( "abc@de.fg" ).unwrap();
307        assert_eq!(
308            Email {
309                local_part: LocalPart::try_from( "abc" ).unwrap(),
310                domain: Domain::try_from( "de.fg" ).unwrap()
311            },
312            email
313        )
314    }
315
316    ec_test!{ local_part_simple, {
317        LocalPart::try_from(  "hans" )?
318    } => ascii => [
319        MarkFWS,
320        Text "hans",
321        MarkFWS
322    ]}
323
324    //fails tries to write utf8
325    ec_test!{ local_part_quoted, {
326        LocalPart::try_from(  "ha ns" )?
327    } => ascii => [
328        MarkFWS,
329        Text "\"ha ns\"",
330        MarkFWS
331    ]}
332
333
334    ec_test!{ local_part_utf8, {
335        LocalPart::try_from( "Jörn" )?
336    } => utf8 => [
337        MarkFWS,
338        Text "Jörn",
339        MarkFWS
340    ]}
341
342    #[test]
343    fn local_part_utf8_on_ascii() {
344        let mut encoder = EncodingBuffer::new( MailType::Ascii );
345        let mut handle = encoder.writer();
346        let local = LocalPart::try_from( "Jörn" ).unwrap();
347        assert_err!(local.encode( &mut handle ));
348        handle.undo_header();
349    }
350
351    ec_test!{ domain, {
352        Domain::try_from( "bad.at.domain" )?
353    } => ascii => [
354        MarkFWS,
355        Text "bad.at.domain",
356        MarkFWS
357    ]}
358
359    ec_test!{ domain_international, {
360        Domain::try_from( "dömain" )?
361    } => utf8 => [
362        MarkFWS,
363        Text "dömain",
364        MarkFWS
365    ]}
366
367
368    ec_test!{ domain_encoded, {
369        Domain::try_from( "dat.ü.dü" )?
370    } => ascii => [
371        MarkFWS,
372        Text "dat.xn--tda.xn--d-eha",
373        MarkFWS
374    ]}
375
376
377    ec_test!{ email_simple, {
378        Email::try_from( "simple@and.ascii" )?
379    } => ascii => [
380        MarkFWS,
381        Text "simple",
382        MarkFWS,
383        Text "@",
384        MarkFWS,
385        Text "and.ascii",
386        MarkFWS
387    ]}
388
389    #[test]
390    fn local_part_as_str() {
391        let lp = LocalPart::try_from("hello").unwrap();
392        assert_eq!(lp.as_str(), "hello")
393    }
394
395    #[test]
396    fn domain_as_str() {
397        let domain = Domain::try_from("hello").unwrap();
398        assert_eq!(domain.as_str(), "hello")
399    }
400
401    #[test]
402    fn to_ascii_string_puny_encodes_if_needed() {
403        let domain = Domain::try_from("hö.test").unwrap();
404        let stringified = domain.to_ascii_string().unwrap();
405        assert_eq!(&*stringified, "xn--h-1ga.test")
406    }
407
408    #[test]
409    fn into_ascii_string_puny_encodes_if_needed() {
410        let domain = Domain::try_from("hö.test").unwrap();
411        let stringified = domain.into_ascii_string().unwrap();
412        assert_eq!(&*stringified, "xn--h-1ga.test")
413    }
414
415    #[test]
416    fn domain_from_str() {
417        let domain: Domain = "1aim.com".parse().unwrap();
418        assert_eq!(domain.as_str(), "1aim.com");
419
420        let res: Result<Domain, _> = "...".parse();
421        assert!(res.is_err());
422    }
423}