Skip to main content

mail_builder/headers/
text.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{
8    Header,
9    fold::{FoldWriter, write_b_words, write_q_words, write_unstructured},
10};
11use crate::{
12    encoders::encode::{EncodingType, get_encoding_type},
13    writer::Writer,
14};
15use std::borrow::Cow;
16
17/// Unstructured text e-mail header.
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
19pub struct Text<'x> {
20    pub text: Cow<'x, str>,
21}
22
23impl<'x> Text<'x> {
24    /// Create a new unstructured text header
25    pub fn new(text: impl Into<Cow<'x, str>>) -> Self {
26        Self { text: text.into() }
27    }
28}
29
30impl<'x, T> From<T> for Text<'x>
31where
32    T: Into<Cow<'x, str>>,
33{
34    fn from(value: T) -> Self {
35        Self::new(value)
36    }
37}
38
39impl Header for Text<'_> {
40    fn write_header(&self, output: &mut impl Writer, column: usize) {
41        let mut folder = FoldWriter::new(output, column);
42
43        match get_encoding_type(self.text.as_bytes(), true, false) {
44            EncodingType::Base64 => write_b_words(&mut folder, &self.text, b""),
45            EncodingType::QuotedPrintable(is_ascii) => {
46                write_q_words::<_, false>(&mut folder, &self.text, is_ascii, b"")
47            }
48            EncodingType::None => write_unstructured(&mut folder, self.text.as_bytes()),
49        }
50
51        folder.finish();
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use mail_parser::MessageParser;
59
60    #[test]
61    fn test_utf8_q_encoding_boundaries() {
62        let mut buf = b"Subject: ".to_vec();
63
64        let mut input = String::new();
65
66        for _ in 0..20000 {
67            input += "x";
68        }
69        for _ in 0..600 {
70            input += "δ";
71        }
72
73        input += "x";
74        for _ in 0..600 {
75            input += "δ";
76        }
77
78        let header = Text::new(input.clone());
79        header.write_header(&mut buf, "Subject: ".len());
80
81        let output = str::from_utf8(&buf).unwrap();
82
83        for line in output.lines() {
84            assert!(
85                line.trim().len() <= 78,
86                "Line exceeds 78 characters: {}",
87                line
88            );
89        }
90        let message = MessageParser::new()
91            .parse_headers(output.as_bytes())
92            .unwrap();
93        assert_eq!(message.subject().unwrap(), input);
94
95        assert!(output.starts_with("Subject: =?utf-8?Q?xxx"));
96
97        assert!(!output.contains("CE?="));
98        assert!(!output.contains("=?utf-8?Q?=B4"));
99    }
100
101    fn b_encoded_input() -> String {
102        let mut input = String::new();
103
104        for _ in 0..600 {
105            input += "δ";
106        }
107        input += "x";
108        for _ in 0..600 {
109            input += "δ";
110        }
111        input
112    }
113
114    #[test]
115    fn test_utf8_b_encoding_boundaries() {
116        let mut buf = b"Subject: ".to_vec();
117
118        let input = b_encoded_input();
119
120        let header = Text::new(input.clone());
121        header.write_header(&mut buf, "Subject: ".len());
122
123        let output = str::from_utf8(&buf).unwrap();
124        for line in output.lines() {
125            assert!(
126                line.trim().len() <= 78,
127                "Line exceeds 78 characters: {}",
128                line
129            );
130        }
131        let message = MessageParser::new()
132            .parse_headers(output.as_bytes())
133            .unwrap();
134        assert_eq!(message.subject().unwrap(), input);
135
136        assert!(output.starts_with("Subject: =?utf-8?B?zrTOtM60zrTOtM60"));
137
138        assert!(!output.contains("zg==?="));
139        assert!(!output.contains("?B?tM60zr"));
140
141        assert!(output.ends_with("\r\n"));
142    }
143
144    #[test]
145    fn test_utf8_b_encoding_large_bytes_written() {
146        let mut buf = Vec::new();
147
148        let input = b_encoded_input();
149
150        let header = Text::new(input);
151
152        let bytes_written = 500;
153        header.write_header(&mut buf, bytes_written);
154
155        let output = str::from_utf8(&buf).unwrap();
156
157        for line in output.lines() {
158            assert!(
159                line.trim().len() <= 78,
160                "Line exceeds 78 characters: {}",
161                line
162            );
163        }
164
165        assert!(
166            output.starts_with("\r\n =?utf-8?B?zrTOtM60zrTOtM60"),
167            "{output:?}"
168        );
169    }
170}