summaryrefslogtreecommitdiffstats
path: root/headers/src/header_components/message_id.rs
diff options
context:
space:
mode:
Diffstat (limited to 'headers/src/header_components/message_id.rs')
-rw-r--r--headers/src/header_components/message_id.rs353
1 files changed, 353 insertions, 0 deletions
diff --git a/headers/src/header_components/message_id.rs b/headers/src/header_components/message_id.rs
new file mode 100644
index 0000000..6ee8a61
--- /dev/null
+++ b/headers/src/header_components/message_id.rs
@@ -0,0 +1,353 @@
+use std::fmt::{self, Display};
+use nom::IResult;
+
+use soft_ascii_string::{SoftAsciiChar, SoftAsciiStr, SoftAsciiString};
+use vec1::Vec1;
+#[cfg(feature="serde")]
+use serde::{
+ Serialize, Serializer,
+ Deserialize, Deserializer,
+ de::Error
+};
+
+use internals::error::EncodingError;
+use internals::encoder::{EncodingWriter, EncodableInHeader};
+use ::{HeaderTryFrom, HeaderTryInto};
+use ::error::ComponentCreationError;
+use ::data::{ Input, SimpleItem };
+
+/// # Implementation Details
+///
+/// This is used for both message-id/content-id, but
+/// depending on usage and support for obsolete parts there
+/// are two "kind" of id's one which allows FWS(/CFWS) in
+/// some places and one which doesn't. This implementation
+/// only supports the later one.
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+pub struct MessageId {
+ message_id: SimpleItem
+}
+
+
+impl MessageId {
+
+ /// creates a message id from a string without checking for validity
+ ///
+ /// The string is expected to have the format `<left_part> "@" <right_part>`,
+ /// i.e. it should not include the `"<"`, `">"` surrounding message id's in
+ /// more or less all places they are used.
+ pub fn from_unchecked(string: String) -> Self {
+ let item =
+ match SoftAsciiString::from_string(string) {
+ Ok(ascii) => ascii.into(),
+ Err(err) => err.into_source().into()
+ };
+
+ MessageId { message_id: item }
+ }
+
+ pub fn new(left_part: &SoftAsciiStr, right_part: &SoftAsciiStr)
+ -> Result<Self, ComponentCreationError>
+ {
+ use self::{parser_parts as parser};
+
+ match parser::id_left(left_part.as_str()) {
+ IResult::Done( "", _part ) => {},
+ _other => {
+ return Err(ComponentCreationError::new_with_str(
+ "MessageId", format!("{}@{}", left_part, right_part)));
+ }
+ }
+
+ match parser::id_right(right_part.as_str()) {
+ IResult::Done( "", _part ) => {},
+ _other => {
+ return Err(ComponentCreationError::new_with_str(
+ "MessageId", format!("{}@{}", left_part, right_part)));
+ }
+ }
+
+ let id = SoftAsciiString::from_unchecked(
+ format!("{}@{}", left_part, right_part));
+ let item = SimpleItem::Ascii(id.into());
+ Ok(MessageId { message_id: item })
+ }
+
+ //FIXME make into AsRef<str> for MessageId
+ pub fn as_str( &self ) -> &str {
+ self.message_id.as_str()
+ }
+}
+
+#[cfg(feature="serde")]
+impl Serialize for MessageId {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where S: Serializer
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature="serde")]
+impl<'de> Deserialize<'de> for MessageId {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where D: Deserializer<'de>
+ {
+ let as_string = String::deserialize(deserializer)?;
+ let as_ascii = SoftAsciiStr::from_str(&as_string)
+ .map_err(|err| D::Error::custom(format!("message id is not ascii: {}", err)))?;
+
+ let split_point =
+ if as_ascii.as_str().ends_with("]") {
+ as_ascii.as_str()
+ .bytes()
+ .rposition(|bch| bch == b'[')
+ .and_then(|pos| pos.checked_sub(1))
+ .ok_or_else(|| D::Error::custom("invalid message id format"))?
+ } else {
+ as_ascii.as_str()
+ .bytes()
+ .rposition(|bch| bch == b'@')
+ .ok_or_else(|| D::Error::custom("invalid message id format"))?
+ };
+
+ let left_part = &as_ascii[..split_point];
+ let right_part = &as_ascii[split_point+1..];
+ MessageId::new(left_part, right_part)
+ .map_err(|err| D::Error::custom(format!("invalid message id format: {}", err)))
+ }
+}
+
+impl Display for MessageId {
+ fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
+ fter.write_str(self.as_str())
+ }
+}
+
+impl<T> HeaderTryFrom<T> for MessageId
+ where T: HeaderTryInto<Input>
+{
+ fn try_from( input: T ) -> Result<Self, ComponentCreationError> {
+ use self::parser_parts::parse_message_id;
+
+ let input = input.try_into()?;
+
+ match parse_message_id(input.as_str()) {
+ IResult::Done( "", _msg_id ) => {},
+ _other => {
+ return Err(ComponentCreationError::new_with_str("MessageId", input.as_str()));
+ }
+ }
+
+
+ Ok( MessageId { message_id: input.into() } )
+ }
+}
+
+impl EncodableInHeader for MessageId {
+
+ fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
+ handle.mark_fws_pos();
+ handle.write_char( SoftAsciiChar::from_unchecked('<') )?;
+ match self.message_id {
+ SimpleItem::Ascii( ref ascii ) => handle.write_str( ascii )?,
+ SimpleItem::Utf8( ref utf8 ) => handle.write_utf8( utf8 )?
+ }
+ handle.write_char( SoftAsciiChar::from_unchecked('>') )?;
+ handle.mark_fws_pos();
+ Ok( () )
+ }
+
+ fn boxed_clone(&self) -> Box<EncodableInHeader> {
+ Box::new(self.clone())
+ }
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature="serde", derive(Serialize, Deserialize))]
+pub struct MessageIdList( pub Vec1<MessageId> );
+
+deref0!{ +mut MessageIdList => Vec1<MessageId> }
+
+impl EncodableInHeader for MessageIdList {
+
+ fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
+ for msg_id in self.iter() {
+ msg_id.encode( handle )?;
+ }
+ Ok( () )
+ }
+
+ fn boxed_clone(&self) -> Box<EncodableInHeader> {
+ Box::new(self.clone())
+ }
+}
+
+
+mod parser_parts {
+ use nom::IResult;
+ use internals::grammar::{is_atext, is_dtext};
+ use internals::MailType;
+
+ pub fn parse_message_id( input: &str) -> IResult<&str, (&str, &str)> {
+ do_parse!( input,
+ l: id_left >>
+ char!( '@' ) >>
+ r: id_right >>
+ (l, r)
+ )
+ }
+
+ pub fn id_left(input: &str) -> IResult<&str, &str> {
+ dot_atom_text(input)
+ }
+
+ pub fn id_right(input: &str) -> IResult<&str, &str> {
+ alt!(
+ input,
+ no_fold_literal |
+ dot_atom_text
+ )
+ }
+
+ fn no_fold_literal( input: &str ) -> IResult<&str, &str> {
+ recognize!( input,
+ tuple!(
+ char!( '[' ),
+ take_while!( call!( is_dtext, MailType::Internationalized ) ),
+ char!( ']' )
+ )
+ )
+ }
+
+ fn dot_atom_text(input: &str) -> IResult<&str, &str> {
+ recognize!( input, tuple!(
+ take_while1!( call!( is_atext, MailType::Internationalized ) ),
+ many0!(tuple!(
+ char!( '.' ),
+ take_while1!( call!( is_atext, MailType::Internationalized ) )
+ ))
+ ) )
+ }
+
+ #[cfg(test)]
+ mod test {
+ use nom;
+ use super::*;
+
+ #[test]
+ fn rec_dot_atom_text_no_dot() {
+ match dot_atom_text( "abc" ) {
+ IResult::Done( "", "abc" ) => {},
+ other => panic!("excepted Done(\"\",\"abc\") got {:?}", other )
+ }
+ }
+
+ #[test]
+ fn rec_dot_atom_text_dots() {
+ match dot_atom_text( "abc.def.ghi" ) {
+ IResult::Done( "", "abc.def.ghi" ) => {},
+ other => panic!("excepted Done(\"\",\"abc.def.ghi\") got {:?}", other )
+ }
+ }
+
+ #[test]
+ fn rec_dot_atom_text_no_end_dot() {
+ let test_str = "abc.";
+ let need_size = test_str.len() + 1;
+ match dot_atom_text( test_str ) {
+ IResult::Incomplete( nom::Needed::Size( ns ) ) if ns == need_size => {}
+ other => panic!("excepted Incomplete(Complete) got {:?}", other )
+ }
+ }
+
+ #[test]
+ fn rec_dot_atom_text_no_douple_dot() {
+ match dot_atom_text( "abc..de" ) {
+ IResult::Done( "..de", "abc" ) => {},
+ other => panic!( "excepted Done(\"..de\",\"abc\") got {:?}", other )
+ }
+ }
+
+ #[test]
+ fn rec_dot_atom_text_no_start_dot() {
+ match dot_atom_text( ".abc" ) {
+ IResult::Error( .. ) => {},
+ other => panic!( "expected error got {:?}", other )
+ }
+ }
+
+
+
+ #[test]
+ fn no_empty() {
+ match dot_atom_text( "" ) {
+ IResult::Incomplete( nom::Needed::Size( 1 ) ) => {},
+ other => panic!( "excepted Incomplete(Size(1)) got {:?}", other )
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use internals::MailType;
+ use internals::encoder::EncodingBuffer;
+ use super::*;
+
+ ec_test!{ new, {
+ MessageId::new(
+ SoftAsciiStr::from_unchecked("just.me"),
+ SoftAsciiStr::from_unchecked("[127.0.0.1]")
+ )?
+ } => ascii => [
+ MarkFWS,
+ Text "<just.me@[127.0.0.1]>",
+ MarkFWS
+ ]}
+
+ ec_test!{ simple, {
+ MessageId::try_from( "affen@haus" )?
+ } => ascii => [
+ MarkFWS,
+ // there are two "context" one which allows FWS inside (defined = email)
+ // and one which doesn't for simplicity we use the later every where
+ Text "<affen@haus>",
+ MarkFWS
+ ]}
+
+ ec_test!{ utf8, {
+ MessageId::try_from( "↓@↑.utf8")?
+ } => utf8 => [
+ MarkFWS,
+ Text "<↓@↑.utf8>",
+ MarkFWS
+ ]}
+
+ #[test]
+ fn utf8_fails() {
+ let mut encoder = EncodingBuffer::new(MailType::Ascii);
+ let mut handle = encoder.writer();
+ let mid = MessageId::try_from( "abc@øpunny.code" ).unwrap();
+ assert_err!(mid.encode( &mut handle ));
+ handle.undo_header();
+ }
+
+ ec_test!{ multipls, {
+ let fst = MessageId::try_from( "affen@haus" )?;
+ let snd = MessageId::try_from( "obst@salat" )?;
+ MessageIdList( vec1! [
+ fst,
+ snd
+ ])
+ } => ascii => [
+ MarkFWS,
+ Text "<affen@haus>",
+ MarkFWS,
+ MarkFWS,
+ Text "<obst@salat>",
+ MarkFWS,
+ ]}
+}
+
+