lettre/message/header/
content_type.rs

1use std::{
2    error::Error as StdError,
3    fmt::{self, Display},
4    str::FromStr,
5};
6
7use mime::Mime;
8
9use super::{Header, HeaderName, HeaderValue};
10use crate::BoxError;
11
12/// `Content-Type` of the body
13///
14/// This struct can represent any valid [MIME type], which can be parsed via
15/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
16///
17/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
18///
19/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ContentType(Mime);
22
23impl ContentType {
24    /// A `ContentType` of type `text/plain; charset=utf-8`
25    ///
26    /// Indicates that the body is in utf-8 encoded plain text.
27    pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
28
29    /// A `ContentType` of type `text/html; charset=utf-8`
30    ///
31    /// Indicates that the body is in utf-8 encoded html.
32    pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
33
34    /// Parse `s` into `ContentType`
35    pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
36        Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
37    }
38
39    pub(crate) const fn from_mime(mime: Mime) -> Self {
40        Self(mime)
41    }
42
43    pub(crate) fn as_ref(&self) -> &Mime {
44        &self.0
45    }
46}
47
48impl Header for ContentType {
49    fn name() -> HeaderName {
50        HeaderName::new_from_ascii_str("Content-Type")
51    }
52
53    fn parse(s: &str) -> Result<Self, BoxError> {
54        Ok(Self(s.parse()?))
55    }
56
57    fn display(&self) -> HeaderValue {
58        HeaderValue::new(Self::name(), self.0.to_string())
59    }
60}
61
62impl FromStr for ContentType {
63    type Err = ContentTypeErr;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        Self::parse(s)
67    }
68}
69
70#[cfg(feature = "mime03")]
71#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
72impl From<Mime> for ContentType {
73    fn from(mime: Mime) -> Self {
74        Self::from_mime(mime)
75    }
76}
77
78/// An error occurred while trying to [`ContentType::parse`].
79#[derive(Debug)]
80pub struct ContentTypeErr(mime::FromStrError);
81
82impl StdError for ContentTypeErr {
83    fn source(&self) -> Option<&(dyn StdError + 'static)> {
84        Some(&self.0)
85    }
86}
87
88impl Display for ContentTypeErr {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        Display::fmt(&self.0, f)
91    }
92}
93
94// -- Serialization and Deserialization --
95#[cfg(feature = "serde")]
96mod serde {
97    use std::fmt;
98
99    use serde::{
100        de::{self, Deserialize, Deserializer, Visitor},
101        ser::{Serialize, Serializer},
102    };
103
104    use super::ContentType;
105
106    impl Serialize for ContentType {
107        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108        where
109            S: Serializer,
110        {
111            serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
112        }
113    }
114
115    impl<'de> Deserialize<'de> for ContentType {
116        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117        where
118            D: Deserializer<'de>,
119        {
120            struct ContentTypeVisitor;
121
122            impl Visitor<'_> for ContentTypeVisitor {
123                type Value = ContentType;
124
125                // The error message which states what the Visitor expects to
126                // receive
127                fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128                    formatter.write_str("a ContentType string like `text/plain`")
129                }
130
131                fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
132                where
133                    E: de::Error,
134                {
135                    match ContentType::parse(mime) {
136                        Ok(content_type) => Ok(content_type),
137                        Err(_) => Err(E::custom(format!(
138                            "Couldn't parse the following MIME-Type: {mime}"
139                        ))),
140                    }
141                }
142            }
143
144            deserializer.deserialize_str(ContentTypeVisitor)
145        }
146    }
147}
148
149#[cfg(test)]
150mod test {
151    use pretty_assertions::assert_eq;
152
153    use super::ContentType;
154    use crate::message::header::{HeaderName, HeaderValue, Headers};
155
156    #[test]
157    fn format_content_type() {
158        let mut headers = Headers::new();
159
160        headers.set(ContentType::TEXT_PLAIN);
161
162        assert_eq!(
163            headers.to_string(),
164            "Content-Type: text/plain; charset=utf-8\r\n"
165        );
166
167        headers.set(ContentType::TEXT_HTML);
168
169        assert_eq!(
170            headers.to_string(),
171            "Content-Type: text/html; charset=utf-8\r\n"
172        );
173    }
174
175    #[test]
176    fn parse_content_type() {
177        let mut headers = Headers::new();
178
179        headers.insert_raw(HeaderValue::new(
180            HeaderName::new_from_ascii_str("Content-Type"),
181            "text/plain; charset=utf-8".to_owned(),
182        ));
183
184        assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
185
186        headers.insert_raw(HeaderValue::new(
187            HeaderName::new_from_ascii_str("Content-Type"),
188            "text/html; charset=utf-8".to_owned(),
189        ));
190
191        assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
192    }
193}