diff options
author | Philipp Korber <p.korber@1aim.com> | 2018-11-16 15:46:43 +0100 |
---|---|---|
committer | Philipp Korber <p.korber@1aim.com> | 2018-11-16 15:46:43 +0100 |
commit | 652d6f0ffeee7302a2cb51059bef75d8b0bb50be (patch) | |
tree | c3851592642938172f280f7428d43e08b0fe2cbe /headers/src/header_components/email.rs | |
parent | 0947fe8996149fe20a6d47a793f9555790eb2eae (diff) |
refactor: merged sources of mail-headers,mail-internals,mail-core, mail
Originally it was palaned to do a merge with `--allow-unrelated-history`
but this can not be doesn as `mail-core` has a "invalid" history which
has a merge conflict **with itself**. So even rewinding the history on
a empty repo is not possible.
Instead the code was directly coppied over losing history.
But the history is still available in the different
`history-backup-*` branches. It is just that the past history
is decoupled from the current history.
Diffstat (limited to 'headers/src/header_components/email.rs')
-rw-r--r-- | headers/src/header_components/email.rs | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/headers/src/header_components/email.rs b/headers/src/header_components/email.rs new file mode 100644 index 0000000..c3cb4fe --- /dev/null +++ b/headers/src/header_components/email.rs @@ -0,0 +1,404 @@ +use std::ops::Deref; +use std::borrow::Cow; + +use failure::Fail; +use soft_ascii_string::{SoftAsciiStr, SoftAsciiString, SoftAsciiChar}; + +use mime::spec::{MimeSpec, Ascii, Internationalized, Modern}; +use quoted_string::quote_if_needed; + +use internals::error::{EncodingError, EncodingErrorKind}; +use internals::grammar::{ + is_ascii, + is_atext, + is_dtext, + is_ws, +}; +use internals::MailType; +use internals::encoder::{EncodingWriter, EncodableInHeader}; +use internals::bind::idna; +use internals::bind::quoted_string::UnquotedDotAtomTextValidator; + +use ::{HeaderTryFrom, HeaderTryInto}; +use ::data::{Input, SimpleItem, InnerUtf8 }; +use ::error::ComponentCreationError; + +/// an email of the form `local-part@domain` +/// corresponds to RFC5322 addr-spec, so `<`, `>` padding is _not_ +/// part of this Email type (but of the Mailbox type instead) +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Email { + pub local_part: LocalPart, + pub domain: Domain +} + + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct LocalPart( Input ); + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Domain( SimpleItem ); + +impl Email { + + pub fn check_if_internationalized(&self) -> bool { + self.local_part.check_if_internationalized() + } + + pub fn new<T: HeaderTryInto<Input>>(email: T) -> Result<Self, ComponentCreationError> { + let email = email.try_into()?.into_shared(); + match email { + Input( InnerUtf8::Owned( .. ) ) => unreachable!(), + Input( InnerUtf8::Shared( shared ) ) => { + //1. ownify Input + //2. get 2 sub shares split befor/after @ + let index = shared.find( "@" ) + .ok_or_else(|| { + ComponentCreationError::new_with_str("Email", shared.to_string()) + })?; + + let left = shared.clone().map( |all| &all[..index] ); + let local_part = LocalPart::try_from( Input( InnerUtf8::Shared( left ) ) )?; + //index+1 is ok as '@'.utf8_len() == 1 + let right = shared.map( |all| &all[index+1..] ); + let domain = Domain::try_from( Input( InnerUtf8::Shared( right ) ) )?; + Ok( Email { local_part, domain } ) + } + } + } +} + +impl LocalPart { + + pub fn check_if_internationalized(&self) -> bool { + self.0.as_str().bytes().any(|b| b > 0x7f) + } +} + +impl<'a> HeaderTryFrom<&'a str> for Email { + fn try_from( email: &str ) -> Result<Self, ComponentCreationError> { + Email::new(email) + } +} + +impl HeaderTryFrom<String> for Email { + fn try_from( email: String ) -> Result<Self, ComponentCreationError> { + Email::new(email) + } +} + +impl HeaderTryFrom<Input> for Email { + fn try_from( email: Input ) -> Result<Self, ComponentCreationError> { + Email::new(email) + } +} + + +impl EncodableInHeader for Email { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + self.local_part.encode( handle )?; + handle.write_char( SoftAsciiChar::from_unchecked('@') )?; + self.domain.encode( handle )?; + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + +impl<T> HeaderTryFrom<T> for LocalPart + where T: HeaderTryInto<Input> +{ + + fn try_from( input: T ) -> Result<Self, ComponentCreationError> { + Ok( LocalPart( input.try_into()? ) ) + } + +} + +impl EncodableInHeader for LocalPart { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + let input: &str = &*self.0; + let mail_type = handle.mail_type(); + + let mut validator = UnquotedDotAtomTextValidator::new(mail_type); + + let res = + if mail_type.is_internationalized() { + quote_if_needed::<MimeSpec<Internationalized, Modern>, _>(input, &mut validator) + } else { + quote_if_needed::<MimeSpec<Ascii, Modern>, _>(input, &mut validator) + }.map_err(|err| EncodingError + ::from(err.context(EncodingErrorKind::Malformed)) + .with_str_context(input) + )?; + + + handle.mark_fws_pos(); + // if mail_type == Ascii quote_if_needed already made sure it's ascii + // it also made sure it is valid as it is either `dot-atom-text` or `quoted-string` + handle.write_str_unchecked(&*res)?; + handle.mark_fws_pos(); + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + +impl Deref for LocalPart { + type Target = Input; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + + + +impl<T> HeaderTryFrom<T> for Domain + where T: HeaderTryInto<Input> +{ + fn try_from( input: T ) -> Result<Self, ComponentCreationError> { + let input = input.try_into()?; + let item = + match Domain::check_domain( input.as_str() )? { + MailType::Ascii | MailType::Mime8BitEnabled => { + SimpleItem::Ascii( input.into_ascii_item_unchecked() ) + }, + MailType::Internationalized => { + SimpleItem::from_utf8_input( input ) + } + }; + + Ok( Domain( item ) ) + } +} + +impl Domain { + + /// creates a domain from a string without checking for validity + pub fn from_unchecked(string: String) -> Self { + let item = + match SoftAsciiString::from_string(string) { + Ok(ascii) => ascii.into(), + Err(err) => err.into_source().into() + }; + + Domain(item) + } + + //CONSTRAINT: + // the function is only allowed to return MailType::Ascii + // if the domain is actually ascii + fn check_domain( domain: &str ) -> Result<MailType, ComponentCreationError> { + if domain.starts_with("[") && domain.ends_with("]") { + //TODO improved support for domain literals, e.g. internationalized ones? CRLF? etc. + for ch in domain.chars() { + if !(is_dtext(ch, MailType::Ascii) || is_ws(ch)) { + let mut err = ComponentCreationError::new("Domain"); + err.set_str_context(domain); + return Err(err); + } + } + Ok(MailType::Ascii) + } else { + let mut ascii = true; + let mut dot_alowed = false; + for char in domain.chars() { + if ascii { ascii = is_ascii( char ) } + if char == '.' && dot_alowed { + dot_alowed = false; + } else if !is_atext( char, MailType::Internationalized ) { + let mut err = ComponentCreationError::new("Domain"); + err.set_str_context(domain); + return Err(err); + } else { + dot_alowed = true; + } + } + Ok(if ascii { + MailType::Ascii + } else { + MailType::Internationalized + }) + } + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn into_ascii_string(self) -> Result<SoftAsciiString, EncodingError> { + match self.0 { + SimpleItem::Ascii(ascii) => Ok(ascii.into()), + SimpleItem::Utf8(utf8) => idna::puny_code_domain(utf8) + } + } + + pub fn to_ascii_string(&self) -> Result<Cow<SoftAsciiStr>, EncodingError> { + Ok(match self.0 { + SimpleItem::Ascii(ref ascii) => { + Cow::Borrowed(ascii) + }, + SimpleItem::Utf8(ref utf8) => { + Cow::Owned(idna::puny_code_domain(utf8)?) + } + }) + } +} + +impl EncodableInHeader for Domain { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + handle.mark_fws_pos(); + match self.0 { + SimpleItem::Ascii( ref ascii ) => { + handle.write_str( ascii )?; + }, + SimpleItem::Utf8( ref utf8 ) => { + handle.write_if_utf8(utf8) + .handle_condition_failure(|handle| { + handle.write_str( &*idna::puny_code_domain( utf8 )? ) + })?; + } + } + handle.mark_fws_pos(); + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + +impl Deref for Domain { + type Target = SimpleItem; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + + + +#[cfg(test)] +mod test { + use internals::encoder::EncodingBuffer; + use super::*; + + #[test] + fn email_try_from() { + let email = Email::try_from( "abc@de.fg" ).unwrap(); + assert_eq!( + Email { + local_part: LocalPart::try_from( "abc" ).unwrap(), + domain: Domain::try_from( "de.fg" ).unwrap() + }, + email + ) + } + + ec_test!{ local_part_simple, { + LocalPart::try_from( "hans" )? + } => ascii => [ + MarkFWS, + Text "hans", + MarkFWS + ]} + + //fails tries to write utf8 + ec_test!{ local_part_quoted, { + LocalPart::try_from( "ha ns" )? + } => ascii => [ + MarkFWS, + Text "\"ha ns\"", + MarkFWS + ]} + + + ec_test!{ local_part_utf8, { + LocalPart::try_from( "Jörn" )? + } => utf8 => [ + MarkFWS, + Text "Jörn", + MarkFWS + ]} + + #[test] + fn local_part_utf8_on_ascii() { + let mut encoder = EncodingBuffer::new( MailType::Ascii ); + let mut handle = encoder.writer(); + let local = LocalPart::try_from( "Jörn" ).unwrap(); + assert_err!(local.encode( &mut handle )); + handle.undo_header(); + } + + ec_test!{ domain, { + Domain::try_from( "bad.at.domain" )? + } => ascii => [ + MarkFWS, + Text "bad.at.domain", + MarkFWS + ]} + + ec_test!{ domain_international, { + Domain::try_from( "dömain" )? + } => utf8 => [ + MarkFWS, + Text "dömain", + MarkFWS + ]} + + + ec_test!{ domain_encoded, { + Domain::try_from( "dat.ü.dü" )? + } => ascii => [ + MarkFWS, + Text "dat.xn--tda.xn--d-eha", + MarkFWS + ]} + + + ec_test!{ email_simple, { + Email::try_from( "simple@and.ascii" )? + } => ascii => [ + MarkFWS, + Text "simple", + MarkFWS, + Text "@", + MarkFWS, + Text "and.ascii", + MarkFWS + ]} + + #[test] + fn local_part_as_str() { + let lp = LocalPart::try_from("hello").unwrap(); + assert_eq!(lp.as_str(), "hello") + } + + #[test] + fn domain_as_str() { + let domain = Domain::try_from("hello").unwrap(); + assert_eq!(domain.as_str(), "hello") + } + + #[test] + fn to_ascii_string_puny_encodes_if_needed() { + let domain = Domain::try_from("hö.test").unwrap(); + let stringified = domain.to_ascii_string().unwrap(); + assert_eq!(&*stringified, "xn--h-1ga.test") + } + + #[test] + fn into_ascii_string_puny_encodes_if_needed() { + let domain = Domain::try_from("hö.test").unwrap(); + let stringified = domain.into_ascii_string().unwrap(); + assert_eq!(&*stringified, "xn--h-1ga.test") + } +}
\ No newline at end of file |