mail_builder/headers/
content_type.rs1use super::Header;
8use crate::encoders::encode::rfc2047_encode;
9use std::borrow::Cow;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub struct ContentType<'x> {
14 pub c_type: Cow<'x, str>,
15 pub attributes: Vec<(Cow<'x, str>, Cow<'x, str>)>,
16}
17
18impl<'x> ContentType<'x> {
19 pub fn new(c_type: impl Into<Cow<'x, str>>) -> Self {
21 Self {
22 c_type: c_type.into(),
23 attributes: Vec::new(),
24 }
25 }
26
27 pub fn attribute(
29 mut self,
30 key: impl Into<Cow<'x, str>>,
31 value: impl Into<Cow<'x, str>>,
32 ) -> Self {
33 self.attributes.push((key.into(), value.into()));
34 self
35 }
36
37 pub fn is_text(&self) -> bool {
39 self.c_type.starts_with("text/")
40 }
41
42 pub fn is_attachment(&self) -> bool {
44 self.c_type == "attachment"
45 }
46}
47
48impl Header for ContentType<'_> {
49 fn write_header(
50 &self,
51 mut output: impl std::io::Write,
52 mut bytes_written: usize,
53 ) -> std::io::Result<usize> {
54 output.write_all(self.c_type.as_bytes())?;
55 bytes_written += self.c_type.len();
56 if !self.attributes.is_empty() {
57 output.write_all(b"; ")?;
58 bytes_written += 2;
59 for (pos, (key, value)) in self.attributes.iter().enumerate() {
60 if bytes_written + key.len() + value.len() + 3 >= 76 {
61 output.write_all(b"\r\n\t")?;
62 bytes_written = 1;
63 }
64
65 output.write_all(key.as_bytes())?;
66 output.write_all(b"=")?;
67 bytes_written += rfc2047_encode(value, &mut output)? + key.len() + 1;
68 if pos < self.attributes.len() - 1 {
69 output.write_all(b"; ")?;
70 bytes_written += 2;
71 }
72 }
73 }
74 output.write_all(b"\r\n")?;
75 Ok(0)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn build(content_type: ContentType<'_>) -> String {
84 let mut output = Vec::new();
85 content_type.write_header(&mut output, 14).unwrap();
86 String::from_utf8(output).unwrap()
87 }
88
89 #[test]
90 fn encoded_parameter_value_stays_quoted() {
91 let header =
92 build(ContentType::new("attachment").attribute("filename", "Jahresabschluß, 2024.pdf"));
93 assert!(header.contains("filename=\"=?"), "{header:?}");
94 assert!(header.contains("?=\""), "{header:?}");
95 }
96
97 #[test]
98 fn plain_parameter_value_is_quoted_and_escaped() {
99 let header =
100 build(ContentType::new("attachment").attribute("filename", "report \"final\".pdf"));
101 assert!(header.contains(r#""report \"final\".pdf""#), "{header:?}");
102 }
103}