diff options
Diffstat (limited to 'headers/src/header_components')
20 files changed, 3616 insertions, 0 deletions
diff --git a/headers/src/header_components/cfws.rs b/headers/src/header_components/cfws.rs new file mode 100644 index 0000000..3ab8585 --- /dev/null +++ b/headers/src/header_components/cfws.rs @@ -0,0 +1,64 @@ +use internals::error::EncodingError; +use internals::encoder::{EncodableInHeader, EncodingWriter}; + +//FEATURE_TODO(fws_controll): allow controlling the amount of WS and if a CRLF should be used in FWS +// this is also usefull for parsing and keeping information about FWS structure +//FEATURE_TODO(cfws_with_comments): allow encoding commnets in CFWS +// this allows encoding comments in CFWS, combine with (part of?) fws_controll +// required (partially) for parsing comments (through skipping them works without this) + +// +//pub enum WS { +// TAB, +// SPACE +//} +// +//pub struct FWS(pub Option<WithCRLF>, pub Vec1<WS> ); +// +//pub struct WithCRLF { +// pub trailing: Vec<WS> +//} + +#[derive(Debug, Hash, Clone, Eq, PartialEq)] +pub struct FWS; + +//NOTE(IMPORTANT): when implementing this I have to assure that encoding CFWS followed by FWS works +// mainly after using a CR-LF-WSP seq you CAN NOT have another FWS which uses unsolds to a CR-LF-WSP +// currently we only remember the last FWS and do only make it in a CR-LF-SPACE sequence when we +// need to, so no problem here for now. +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum CFWS { + //WithComment( Vec1<(Option<FWS>, Comment)>, Option<FWS> ), + SingleFws( FWS ) +} + + +impl EncodableInHeader for CFWS { + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + match *self { + CFWS::SingleFws(ref _fws ) => { + handle.write_fws(); + } + } + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + ec_test!{ simple_encode, + { + CFWS::SingleFws( FWS ) + } => utf8 => [ + MarkFWS, + Text " " + ] + } + +}
\ No newline at end of file diff --git a/headers/src/header_components/date_time.rs b/headers/src/header_components/date_time.rs new file mode 100644 index 0000000..314f20c --- /dev/null +++ b/headers/src/header_components/date_time.rs @@ -0,0 +1,90 @@ +use chrono; +use soft_ascii_string::SoftAsciiString; + + +use internals::encoder::{EncodingWriter, EncodableInHeader}; +use internals::error::EncodingError; +use ::HeaderTryFrom; +use ::error::ComponentCreationError; + +#[cfg(feature="serde")] +use serde::{Serialize, Deserialize}; + +/// A DateTime header component wrapping chrono::DateTime<chrono::Utc> +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[cfg_attr(feature="serde", derive(Serialize, Deserialize))] +pub struct DateTime( + #[cfg_attr(feature="serde", serde(with = "super::utils::serde::date_time"))] + chrono::DateTime<chrono::Utc> +); + +impl DateTime { + + /// create a new DateTime of the current Time + pub fn now() -> DateTime { + DateTime( chrono::Utc::now() ) + } + + /// create a new DateTime from a `chrono::DateTime<TimeZone>` for any `TimeZone` + pub fn new<TZ: chrono::TimeZone>( date_time: chrono::DateTime<TZ>) -> DateTime { + DateTime( date_time.with_timezone( &chrono::Utc ) ) + } + + #[doc(hidden)] + #[cfg(test)] + pub fn test_time( modif: u32 ) -> Self { + use chrono::prelude::*; + Self::new( FixedOffset::east( 3 * 3600 ).ymd( 2013, 8, 6 ).and_hms( 7, 11, modif ) ) + } +} + +impl EncodableInHeader for DateTime { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + let time = SoftAsciiString::from_unchecked(self.to_rfc2822()); + handle.write_str( &*time )?; + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + + +impl<TZ> HeaderTryFrom<chrono::DateTime<TZ>> for DateTime + where TZ: chrono::TimeZone +{ + fn try_from(val: chrono::DateTime<TZ>) -> Result<Self, ComponentCreationError> { + Ok(Self::new(val)) + } +} + +impl<TZ> From<chrono::DateTime<TZ>> for DateTime + where TZ: chrono::TimeZone +{ + fn from(val: chrono::DateTime<TZ>) -> Self { + Self::new(val) + } +} + +impl Into<chrono::DateTime<chrono::Utc>> for DateTime { + fn into(self) -> chrono::DateTime<chrono::Utc> { + self.0 + } +} + +deref0!{-mut DateTime => chrono::DateTime<chrono::Utc> } + + +#[cfg(test)] +mod test { + use super::DateTime; + + ec_test!{ date_time, { + DateTime::test_time( 45 ) + } => ascii => [ + Text "Tue, 6 Aug 2013 04:11:45 +0000" + ]} + +}
\ No newline at end of file diff --git a/headers/src/header_components/disposition.rs b/headers/src/header_components/disposition.rs new file mode 100644 index 0000000..66c4a25 --- /dev/null +++ b/headers/src/header_components/disposition.rs @@ -0,0 +1,308 @@ +use std::borrow::Cow; +#[cfg(feature="serde")] +use std::fmt; + +use failure::Fail; +use soft_ascii_string::SoftAsciiStr; +use mime::push_params_to_buffer; +use mime::spec::{MimeSpec, Ascii, Modern, Internationalized}; + +#[cfg(feature="serde")] +use serde::{ + Serialize, Serializer, + Deserialize, Deserializer, +}; + +use internals::error::{EncodingError, EncodingErrorKind}; +use internals::encoder::{EncodableInHeader, EncodingWriter}; +use ::HeaderTryFrom; +use ::error::ComponentCreationError; + +use super::FileMeta; + +/// Disposition Component mainly used for the Content-Disposition header (rfc2183) +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature="serde", derive(Serialize, Deserialize))] +pub struct Disposition { + kind: DispositionKind, + file_meta: DispositionParameters +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +#[cfg_attr(feature="serde", derive(Serialize, Deserialize))] +struct DispositionParameters(FileMeta); + +/// Represents what kind of disposition is used (Inline/Attachment) +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum DispositionKind { + /// Display the body "inline". + /// + /// This disposition is mainly used to add some additional content + /// and then refers to it through its cid (e.g. in a html mail). + Inline, + /// Display the body as an attachment to of the mail. + Attachment +} + +impl Disposition { + + /// Create a inline disposition with default parameters. + pub fn inline() -> Self { + Disposition::new( DispositionKind::Inline, FileMeta::default() ) + } + + /// Create a attachment disposition with default parameters. + pub fn attachment() -> Self { + Disposition::new( DispositionKind::Attachment, FileMeta::default() ) + } + + /// Create a new disposition with given parameters. + pub fn new( kind: DispositionKind, file_meta: FileMeta ) -> Self { + Disposition { kind, file_meta: DispositionParameters( file_meta ) } + } + + /// Return which kind of disposition this represents. + pub fn kind( &self ) -> DispositionKind { + self.kind + } + + /// Returns the parameters associated with the disposition. + pub fn file_meta( &self ) -> &FileMeta { + &self.file_meta + } + + /// Returns a mutable reference to the parameters associated with the disposition. + pub fn file_meta_mut( &mut self ) -> &mut FileMeta { + &mut self.file_meta + } + +} + +#[cfg(feature="serde")] +impl Serialize for DispositionKind { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where S: Serializer + { + match self { + &DispositionKind::Inline => + serializer.serialize_str("inline"), + &DispositionKind::Attachment => + serializer.serialize_str("attachment") + } + } +} + +#[cfg(feature="serde")] +impl<'de> Deserialize<'de> for DispositionKind { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where D: Deserializer<'de> + { + struct Visitor; + impl<'de> ::serde::de::Visitor<'de> for Visitor { + type Value = DispositionKind; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("\"inline\" or \"attachment\"") + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where E: ::serde::de::Error, + { + if value.eq_ignore_ascii_case("inline") { + Ok(DispositionKind::Inline) + } else if value.eq_ignore_ascii_case("attachment") { + Ok(DispositionKind::Attachment) + } else { + Err(E::custom(format!( + "unknown disposition: {:?}", value + ))) + } + } + } + + deserializer.deserialize_str(Visitor) + } +} + +/// This try from is for usability only, it is +/// generally recommendet to use Disposition::inline()/::attachment() +/// as it is type safe / compiler time checked, while this one +/// isn't +impl<'a> HeaderTryFrom<&'a str> for Disposition { + fn try_from(text: &'a str) -> Result<Self, ComponentCreationError> { + if text.eq_ignore_ascii_case("Inline") { + Ok(Disposition::inline()) + } else if text.eq_ignore_ascii_case("Attachment") { + Ok(Disposition::attachment()) + } else { + let mut err = ComponentCreationError::new("Disposition"); + err.set_str_context(text); + return Err(err); + } + } +} + + +//TODO provide a gnneral way for encoding header parameter ... +// which follow the scheme: <mainvalue> *(";" <key>"="<value> ) +// this are: ContentType and ContentDisposition for now +impl EncodableInHeader for DispositionParameters { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + let mut params = Vec::<(&str, Cow<str>)>::new(); + if let Some(filename) = self.file_name.as_ref() { + params.push(("filename", Cow::Borrowed(filename))); + } + if let Some(creation_date) = self.creation_date.as_ref() { + params.push(("creation-date", Cow::Owned(creation_date.to_rfc2822()))); + } + if let Some(date) = self.modification_date.as_ref() { + params.push(("modification-date", Cow::Owned(date.to_rfc2822()))); + } + if let Some(date) = self.read_date.as_ref() { + params.push(("read-date", Cow::Owned(date.to_rfc2822()))); + } + if let Some(size) = self.size.as_ref() { + params.push(("size", Cow::Owned(size.to_string()))); + } + + //TODO instead do optCFWS ; spCFWS <name>=<value> + // so that soft line brakes can be done + let mut buff = String::new(); + let res = + if handle.mail_type().is_internationalized() { + push_params_to_buffer::<MimeSpec<Internationalized, Modern>, _, _, _>( + &mut buff, params + ) + } else { + push_params_to_buffer::<MimeSpec<Ascii, Modern>, _, _, _>( + &mut buff, params + ) + }; + + match res { + Err(err) => { + Err(err.context(EncodingErrorKind::Malformed).into()) + }, + Ok(_) => { + handle.write_str_unchecked(&*buff)?; + Ok(()) + } + } + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + + +impl EncodableInHeader for Disposition { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + use self::DispositionKind::*; + match self.kind { + Inline => { + handle.write_str(SoftAsciiStr::from_unchecked("inline"))?; + }, + Attachment => { + handle.write_str(SoftAsciiStr::from_unchecked("attachment"))?; + } + } + self.file_meta.encode( handle )?; + Ok( () ) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + + +deref0!{+mut DispositionParameters => FileMeta } + +#[cfg(test)] +mod test { + use chrono; + use std::default::Default; + + use super::*; + + pub fn test_time( modif: u32 ) -> chrono::DateTime<chrono::Utc> { + use chrono::prelude::*; + Utc.ymd( 2013, 8, 6 ).and_hms( 7, 11, modif ) + } + + ec_test!{ no_params_inline, { + Disposition::inline() + } => ascii => [ + Text "inline" + ]} + + ec_test!{ no_params_attachment, { + Disposition::attachment() + } => ascii => [ + Text "attachment" + ]} + + ec_test!{ attachment_encode_file_name, { + Disposition::new( DispositionKind::Attachment, FileMeta { + file_name: Some("this is nice".to_owned()), + ..Default::default() + }) + } => ascii => [ + Text "attachment; filename=\"this is nice\"" + ]} + + ec_test!{ attachment_all_params, { + Disposition::new( DispositionKind::Attachment, FileMeta { + file_name: Some( "random.png".to_owned() ), + creation_date: Some( test_time( 1 ) ), + modification_date: Some( test_time( 2 ) ), + read_date: Some( test_time( 3 ) ), + size: Some( 4096 ) + }) + } => ascii => [ + Text concat!( "attachment", + "; filename=random.png", + "; creation-date=\"Tue, 6 Aug 2013 07:11:01 +0000\"", + "; modification-date=\"Tue, 6 Aug 2013 07:11:02 +0000\"", + "; read-date=\"Tue, 6 Aug 2013 07:11:03 +0000\"", + "; size=4096" ), + ]} + + ec_test!{ inline_file_name_param, { + Disposition::new(DispositionKind::Inline, FileMeta { + file_name: Some("logo.png".to_owned()), + ..Default::default() + }) + } => ascii => [ + Text "inline; filename=logo.png" + ]} + //TODO: (1 allow FWS or so in parameters) (2 utf8 file names) + + #[test] + fn test_from_str() { + assert_ok!( Disposition::try_from( "Inline" ) ); + assert_ok!( Disposition::try_from( "InLine" ) ); + assert_ok!( Disposition::try_from( "Attachment" ) ); + + assert_err!( Disposition::try_from( "In line") ); + } + + #[cfg(feature="serde")] + fn assert_serialize<S: ::serde::Serialize>() {} + #[cfg(feature="serde")] + fn assert_deserialize<S: ::serde::Serialize>() {} + + #[cfg(feature="serde")] + #[test] + fn disposition_serialization() { + assert_serialize::<Disposition>(); + assert_serialize::<DispositionKind>(); + assert_serialize::<DispositionParameters>(); + assert_deserialize::<Disposition>(); + assert_deserialize::<DispositionKind>(); + assert_deserialize::<DispositionParameters>(); + } +}
\ No newline at end of file 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 diff --git a/headers/src/header_components/file_meta.rs b/headers/src/header_components/file_meta.rs new file mode 100644 index 0000000..e8f1d1e --- /dev/null +++ b/headers/src/header_components/file_meta.rs @@ -0,0 +1,80 @@ + +use chrono::DateTime; +use chrono::Utc; + +use std::mem::replace; + +#[cfg(feature="serde")] +use serde::{Serialize, Deserialize}; + +/// A struct representing common file metadata. +/// +/// This is used by e.g. attachments, when attaching +/// a file (or embedding an image). Through it's usage +/// is optional. +/// +/// # Stability Note +/// +/// This is likely to move to an different place at +/// some point, potentially in a different `mail-*` +/// crate. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +#[cfg_attr(feature="serde", derive(Serialize, Deserialize))] +pub struct FileMeta { + /// The file name. + /// + /// Note that this utility is limited to utf-8 file names. + /// This is normally used when downloading a attachment to + /// choose the default file name. + #[cfg_attr(feature="serde", serde(default))] + pub file_name: Option<String>, + + /// The creation date of the file (in utc). + #[cfg_attr(feature="serde", serde(default))] + #[cfg_attr(feature="serde", serde(with = "super::utils::serde::opt_date_time"))] + pub creation_date: Option<DateTime<Utc>>, + + /// The last modification date of the file (in utc). + #[cfg_attr(feature="serde", serde(default))] + #[cfg_attr(feature="serde", serde(with = "super::utils::serde::opt_date_time"))] + pub modification_date: Option<DateTime<Utc>>, + + /// The date time the file was read, i.e. placed in the mail (in utc). + #[cfg_attr(feature="serde", serde(default))] + #[cfg_attr(feature="serde", serde(with = "super::utils::serde::opt_date_time"))] + pub read_date: Option<DateTime<Utc>>, + + /// The size the file should have. + /// + /// Note that normally mail explicitly opts to NOT specify the size + /// of a mime-multi part body (e.g. an attachments) and you can never + /// rely on it to e.g |