summaryrefslogtreecommitdiffstats
path: root/headers/src/header_components
diff options
context:
space:
mode:
Diffstat (limited to 'headers/src/header_components')
-rw-r--r--headers/src/header_components/cfws.rs64
-rw-r--r--headers/src/header_components/date_time.rs90
-rw-r--r--headers/src/header_components/disposition.rs308
-rw-r--r--headers/src/header_components/email.rs404
-rw-r--r--headers/src/header_components/file_meta.rs80
-rw-r--r--headers/src/header_components/mailbox.rs212
-rw-r--r--headers/src/header_components/mailbox_list.rs300
-rw-r--r--headers/src/header_components/media_type.rs513
-rw-r--r--headers/src/header_components/message_id.rs353
-rw-r--r--headers/src/header_components/mod.rs66
-rw-r--r--headers/src/header_components/path.rs73
-rw-r--r--headers/src/header_components/phrase.rs127
-rw-r--r--headers/src/header_components/phrase_list.rs157
-rw-r--r--headers/src/header_components/raw_unstructured.rs87
-rw-r--r--headers/src/header_components/received_token.rs97
-rw-r--r--headers/src/header_components/transfer_encoding.rs117
-rw-r--r--headers/src/header_components/unstructured.rs194
-rw-r--r--headers/src/header_components/utils/mod.rs90
-rw-r--r--headers/src/header_components/utils/text_partition.rs62
-rw-r--r--headers/src/header_components/word.rs222
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