mail_headers_ng/header_components/
disposition.rs

1use std::borrow::Cow;
2#[cfg(feature="serde")]
3use std::fmt;
4
5use failure::Fail;
6use soft_ascii_string::SoftAsciiStr;
7use media_type::push_params_to_buffer;
8use media_type::spec::{MimeSpec, Ascii, Modern, Internationalized};
9
10#[cfg(feature="serde")]
11use serde::{
12    Serialize, Serializer,
13    Deserialize, Deserializer,
14};
15
16use internals::error::{EncodingError, EncodingErrorKind};
17use internals::encoder::{EncodableInHeader, EncodingWriter};
18use ::HeaderTryFrom;
19use ::error::ComponentCreationError;
20
21use super::FileMeta;
22
23/// Disposition Component mainly used for the Content-Disposition header (rfc2183)
24#[derive(Debug, Clone, Eq, PartialEq, Hash)]
25#[cfg_attr(feature="serde", derive(Serialize, Deserialize))]
26pub struct Disposition {
27    kind: DispositionKind,
28    file_meta: DispositionParameters
29}
30
31#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
32#[cfg_attr(feature="serde", derive(Serialize, Deserialize))]
33struct DispositionParameters(FileMeta);
34
35/// Represents what kind of disposition is used (Inline/Attachment)
36#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
37pub enum DispositionKind {
38    /// Display the body "inline".
39    ///
40    /// This disposition is mainly used to add some additional content
41    /// and then refers to it through its cid (e.g. in a html mail).
42    Inline,
43    /// Display the body as an attachment to of the mail.
44    Attachment
45}
46
47impl Disposition {
48
49    /// Create a inline disposition with default parameters.
50    pub fn inline() -> Self {
51        Disposition::new( DispositionKind::Inline, FileMeta::default() )
52    }
53
54    /// Create a attachment disposition with default parameters.
55    pub fn attachment() -> Self {
56        Disposition::new( DispositionKind::Attachment, FileMeta::default() )
57    }
58
59    /// Create a new disposition with given parameters.
60    pub fn new( kind: DispositionKind, file_meta: FileMeta ) -> Self {
61        Disposition { kind, file_meta: DispositionParameters( file_meta ) }
62    }
63
64    /// Return which kind of disposition this represents.
65    pub fn kind( &self ) -> DispositionKind {
66        self.kind
67    }
68
69    /// Returns the parameters associated with the disposition.
70    pub fn file_meta( &self ) -> &FileMeta {
71        &self.file_meta
72    }
73
74    /// Returns a mutable reference to the parameters associated with the disposition.
75    pub fn file_meta_mut( &mut self ) -> &mut FileMeta {
76        &mut self.file_meta
77    }
78
79}
80
81#[cfg(feature="serde")]
82impl Serialize for DispositionKind {
83    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84        where S: Serializer
85    {
86        match self {
87            &DispositionKind::Inline =>
88                serializer.serialize_str("inline"),
89            &DispositionKind::Attachment =>
90                serializer.serialize_str("attachment")
91        }
92    }
93}
94
95#[cfg(feature="serde")]
96impl<'de> Deserialize<'de> for DispositionKind {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98        where D: Deserializer<'de>
99    {
100        struct Visitor;
101        impl<'de> ::serde::de::Visitor<'de> for Visitor {
102            type Value = DispositionKind;
103
104            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
105                formatter.write_str("\"inline\" or \"attachment\"")
106            }
107
108            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
109                where E: ::serde::de::Error,
110            {
111                if value.eq_ignore_ascii_case("inline") {
112                    Ok(DispositionKind::Inline)
113                } else if value.eq_ignore_ascii_case("attachment") {
114                    Ok(DispositionKind::Attachment)
115                } else {
116                    Err(E::custom(format!(
117                        "unknown disposition: {:?}", value
118                    )))
119                }
120            }
121        }
122
123        deserializer.deserialize_str(Visitor)
124    }
125}
126
127/// This try from is for usability only, it is
128/// generally recommendet to use Disposition::inline()/::attachment()
129/// as it is type safe / compiler time checked, while this one
130/// isn't
131impl<'a> HeaderTryFrom<&'a str> for Disposition {
132    fn try_from(text: &'a str) -> Result<Self, ComponentCreationError> {
133        if text.eq_ignore_ascii_case("Inline") {
134            Ok(Disposition::inline())
135        } else if text.eq_ignore_ascii_case("Attachment") {
136            Ok(Disposition::attachment())
137        } else {
138            let mut err = ComponentCreationError::new("Disposition");
139            err.set_str_context(text);
140            return Err(err);
141        }
142    }
143}
144
145
146//TODO provide a gnneral way for encoding header parameter ...
147//  which follow the scheme: <mainvalue> *(";" <key>"="<value> )
148//  this are: ContentType and ContentDisposition for now
149impl EncodableInHeader for DispositionParameters {
150
151    fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
152        let mut params = Vec::<(&str, Cow<str>)>::new();
153        if let Some(filename) = self.file_name.as_ref() {
154            params.push(("filename", Cow::Borrowed(filename)));
155        }
156        if let Some(creation_date) = self.creation_date.as_ref() {
157            params.push(("creation-date", Cow::Owned(creation_date.to_rfc2822())));
158        }
159        if let Some(date) = self.modification_date.as_ref() {
160            params.push(("modification-date", Cow::Owned(date.to_rfc2822())));
161        }
162        if let Some(date) = self.read_date.as_ref() {
163            params.push(("read-date", Cow::Owned(date.to_rfc2822())));
164        }
165        if let Some(size) = self.size.as_ref() {
166            params.push(("size", Cow::Owned(size.to_string())));
167        }
168
169        //TODO instead do optCFWS ; spCFWS <name>=<value>
170        // so that soft line brakes can be done
171        let mut buff = String::new();
172        let res =
173            if handle.mail_type().is_internationalized() {
174                push_params_to_buffer::<MimeSpec<Internationalized, Modern>, _, _, _>(
175                    &mut buff, params
176                )
177            } else {
178                push_params_to_buffer::<MimeSpec<Ascii, Modern>, _, _, _>(
179                    &mut buff, params
180                )
181            };
182
183        match res {
184            Err(err) => {
185                Err(err.context(EncodingErrorKind::Malformed).into())
186            },
187            Ok(_) => {
188                handle.write_str_unchecked(&*buff)?;
189                Ok(())
190            }
191        }
192    }
193
194    fn boxed_clone(&self) -> Box<EncodableInHeader> {
195        Box::new(self.clone())
196    }
197}
198
199
200impl EncodableInHeader for Disposition {
201
202    fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> {
203        use self::DispositionKind::*;
204        match self.kind {
205            Inline => {
206                handle.write_str(SoftAsciiStr::from_unchecked("inline"))?;
207            },
208            Attachment => {
209                handle.write_str(SoftAsciiStr::from_unchecked("attachment"))?;
210            }
211        }
212        self.file_meta.encode( handle )?;
213        Ok( () )
214    }
215
216    fn boxed_clone(&self) -> Box<EncodableInHeader> {
217        Box::new(self.clone())
218    }
219}
220
221
222deref0!{+mut DispositionParameters => FileMeta }
223
224#[cfg(test)]
225mod test {
226    use chrono;
227    use std::default::Default;
228
229    use super::*;
230
231    pub fn test_time( modif: u32 ) -> chrono::DateTime<chrono::Utc> {
232        use chrono::prelude::*;
233        Utc.ymd( 2013, 8, 6 ).and_hms( 7, 11, modif )
234    }
235
236    ec_test!{ no_params_inline, {
237        Disposition::inline()
238    } => ascii => [
239        Text "inline"
240    ]}
241
242    ec_test!{ no_params_attachment, {
243        Disposition::attachment()
244    } => ascii => [
245        Text "attachment"
246    ]}
247
248    ec_test!{ attachment_encode_file_name, {
249        Disposition::new( DispositionKind::Attachment, FileMeta {
250            file_name: Some("this is nice".to_owned()),
251            ..Default::default()
252        })
253    } => ascii => [
254        Text "attachment; filename=\"this is nice\""
255    ]}
256
257    ec_test!{ attachment_all_params, {
258        Disposition::new( DispositionKind::Attachment, FileMeta {
259            file_name: Some( "random.png".to_owned() ),
260            creation_date: Some( test_time( 1 ) ),
261            modification_date: Some( test_time( 2 ) ),
262            read_date: Some( test_time( 3 ) ),
263            size: Some( 4096 )
264        })
265    } => ascii => [
266        Text concat!( "attachment",
267            "; filename=random.png",
268            "; creation-date=\"Tue,  6 Aug 2013 07:11:01 +0000\"",
269            "; modification-date=\"Tue,  6 Aug 2013 07:11:02 +0000\"",
270            "; read-date=\"Tue,  6 Aug 2013 07:11:03 +0000\"",
271            "; size=4096" ),
272    ]}
273
274    ec_test!{ inline_file_name_param, {
275        Disposition::new(DispositionKind::Inline, FileMeta {
276            file_name: Some("logo.png".to_owned()),
277            ..Default::default()
278        })
279    } => ascii => [
280        Text "inline; filename=logo.png"
281    ]}
282    //TODO: (1 allow FWS or so in parameters) (2 utf8 file names)
283
284    #[test]
285    fn test_from_str() {
286        assert_ok!( Disposition::try_from( "Inline" ) );
287        assert_ok!( Disposition::try_from( "InLine" ) );
288        assert_ok!( Disposition::try_from( "Attachment" ) );
289
290        assert_err!( Disposition::try_from( "In line") );
291    }
292
293    #[cfg(feature="serde")]
294    fn assert_serialize<S: ::serde::Serialize>() {}
295    #[cfg(feature="serde")]
296    fn assert_deserialize<S: ::serde::Serialize>() {}
297
298    #[cfg(feature="serde")]
299    #[test]
300    fn disposition_serialization() {
301        assert_serialize::<Disposition>();
302        assert_serialize::<DispositionKind>();
303        assert_serialize::<DispositionParameters>();
304        assert_deserialize::<Disposition>();
305        assert_deserialize::<DispositionKind>();
306        assert_deserialize::<DispositionParameters>();
307    }
308}