mail_headers_ng/header_components/
email.rs1use 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#[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 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 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 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 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 fn check_domain( domain: &str ) -> Result<MailType, ComponentCreationError> {
209 if domain.starts_with("[") && domain.ends_with("]") {
210 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 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}