lettre/message/header/
content_disposition.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::fmt::Write;

use email_encoding::headers::EmailWriter;

use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;

/// `Content-Disposition` of an attachment
///
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentDisposition(HeaderValue);

impl ContentDisposition {
    /// An attachment which should be displayed inline into the message
    pub fn inline() -> Self {
        Self(HeaderValue::dangerous_new_pre_encoded(
            Self::name(),
            "inline".to_owned(),
            "inline".to_owned(),
        ))
    }

    /// An attachment which should be displayed inline into the message, but that also
    /// species the filename in case it were to be downloaded
    pub fn inline_with_name(file_name: &str) -> Self {
        Self::with_name("inline", file_name)
    }

    /// An attachment which is separate from the body of the message, and can be downloaded separately
    pub fn attachment(file_name: &str) -> Self {
        Self::with_name("attachment", file_name)
    }

    fn with_name(kind: &str, file_name: &str) -> Self {
        let raw_value = format!("{kind}; filename=\"{file_name}\"");

        let mut encoded_value = String::new();
        let line_len = "Content-Disposition: ".len();
        {
            let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
            w.write_str(kind).expect("writing `kind` returned an error");
            w.write_char(';').expect("writing `;` returned an error");
            w.optional_breakpoint();

            email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
                .expect("some Write implementation returned an error");
        }

        Self(HeaderValue::dangerous_new_pre_encoded(
            Self::name(),
            raw_value,
            encoded_value,
        ))
    }
}

impl Header for ContentDisposition {
    fn name() -> HeaderName {
        HeaderName::new_from_ascii_str("Content-Disposition")
    }

    fn parse(s: &str) -> Result<Self, BoxError> {
        match (s.split_once(';'), s) {
            (_, "inline") => Ok(Self::inline()),
            (Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
                .split_once(" filename=\"")
                .and_then(|(_, file_name)| file_name.strip_suffix('"'))
                .map(|file_name| Self::with_name(kind, file_name))
                .ok_or_else(|| "Unsupported ContentDisposition value".into()),
            _ => Err("Unsupported ContentDisposition value".into()),
        }
    }

    fn display(&self) -> HeaderValue {
        self.0.clone()
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::ContentDisposition;
    use crate::message::header::{HeaderName, HeaderValue, Headers};

    #[test]
    fn format_content_disposition() {
        let mut headers = Headers::new();

        headers.set(ContentDisposition::inline());

        assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");

        headers.set(ContentDisposition::attachment("something.txt"));

        assert_eq!(
            format!("{headers}"),
            "Content-Disposition: attachment; filename=\"something.txt\"\r\n"
        );
    }

    #[test]
    fn parse_content_disposition() {
        let mut headers = Headers::new();

        headers.insert_raw(HeaderValue::new(
            HeaderName::new_from_ascii_str("Content-Disposition"),
            "inline".to_owned(),
        ));

        assert_eq!(
            headers.get::<ContentDisposition>(),
            Some(ContentDisposition::inline())
        );

        headers.insert_raw(HeaderValue::new(
            HeaderName::new_from_ascii_str("Content-Disposition"),
            "attachment; filename=\"something.txt\"".to_owned(),
        ));

        assert_eq!(
            headers.get::<ContentDisposition>(),
            Some(ContentDisposition::attachment("something.txt"))
        );
    }
}