lettre/message/header/
content_disposition.rs

1use std::fmt::Write;
2
3use email_encoding::headers::writer::EmailWriter;
4
5use super::{Header, HeaderName, HeaderValue};
6use crate::BoxError;
7
8/// `Content-Disposition` of an attachment
9///
10/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
11#[derive(Debug, Clone, PartialEq)]
12pub struct ContentDisposition(HeaderValue);
13
14impl ContentDisposition {
15    /// An attachment which should be displayed inline into the message
16    pub fn inline() -> Self {
17        Self(HeaderValue::dangerous_new_pre_encoded(
18            Self::name(),
19            "inline".to_owned(),
20            "inline".to_owned(),
21        ))
22    }
23
24    /// An attachment which should be displayed inline into the message, but that also
25    /// species the filename in case it is downloaded
26    pub fn inline_with_name(file_name: &str) -> Self {
27        Self::with_name("inline", file_name)
28    }
29
30    /// An attachment which is separate from the body of the message, and can be downloaded separately
31    pub fn attachment(file_name: &str) -> Self {
32        Self::with_name("attachment", file_name)
33    }
34
35    fn with_name(kind: &str, file_name: &str) -> Self {
36        let raw_value = format!("{kind}; filename=\"{file_name}\"");
37
38        let mut encoded_value = String::new();
39        let line_len = "Content-Disposition: ".len();
40        {
41            let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
42            w.write_str(kind).expect("writing `kind` returned an error");
43            w.write_char(';').expect("writing `;` returned an error");
44            w.space();
45
46            email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
47                .expect("some Write implementation returned an error");
48        }
49
50        Self(HeaderValue::dangerous_new_pre_encoded(
51            Self::name(),
52            raw_value,
53            encoded_value,
54        ))
55    }
56}
57
58impl Header for ContentDisposition {
59    fn name() -> HeaderName {
60        HeaderName::new_from_ascii_str("Content-Disposition")
61    }
62
63    fn parse(s: &str) -> Result<Self, BoxError> {
64        match (s.split_once(';'), s) {
65            (_, "inline") => Ok(Self::inline()),
66            (Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
67                .split_once(" filename=\"")
68                .and_then(|(_, file_name)| file_name.strip_suffix('"'))
69                .map(|file_name| Self::with_name(kind, file_name))
70                .ok_or_else(|| "Unsupported ContentDisposition value".into()),
71            _ => Err("Unsupported ContentDisposition value".into()),
72        }
73    }
74
75    fn display(&self) -> HeaderValue {
76        self.0.clone()
77    }
78}
79
80#[cfg(test)]
81mod test {
82    use pretty_assertions::assert_eq;
83
84    use super::ContentDisposition;
85    use crate::message::header::{HeaderName, HeaderValue, Headers};
86
87    #[test]
88    fn format_content_disposition() {
89        let mut headers = Headers::new();
90
91        headers.set(ContentDisposition::inline());
92
93        assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
94
95        headers.set(ContentDisposition::attachment("something.txt"));
96
97        assert_eq!(
98            format!("{headers}"),
99            "Content-Disposition: attachment; filename=\"something.txt\"\r\n"
100        );
101    }
102
103    #[test]
104    fn parse_content_disposition() {
105        let mut headers = Headers::new();
106
107        headers.insert_raw(HeaderValue::new(
108            HeaderName::new_from_ascii_str("Content-Disposition"),
109            "inline".to_owned(),
110        ));
111
112        assert_eq!(
113            headers.get::<ContentDisposition>(),
114            Some(ContentDisposition::inline())
115        );
116
117        headers.insert_raw(HeaderValue::new(
118            HeaderName::new_from_ascii_str("Content-Disposition"),
119            "attachment; filename=\"something.txt\"".to_owned(),
120        ));
121
122        assert_eq!(
123            headers.get::<ContentDisposition>(),
124            Some(ContentDisposition::attachment("something.txt"))
125        );
126    }
127}