1use std::sync::LazyLock;
4
5use crate::simplify::remove_message_footer;
6
7#[derive(Debug)]
9pub struct PlainText {
10 pub text: String,
12
13 pub flowed: bool,
18
19 pub delsp: bool,
22}
23
24impl PlainText {
25 pub fn to_html(&self) -> String {
28 static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
29 LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
30
31 static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
32 regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
33 });
34
35 let lines: Vec<&str> = self.text.lines().collect();
36 let (lines, _footer) = remove_message_footer(&lines);
37
38 let mut ret = r#"<!DOCTYPE html>
39<html><head>
40<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
41<meta name="color-scheme" content="light dark" />
42</head><body>
43"#
44 .to_string();
45
46 for line in lines {
47 let is_quote = line.starts_with('>');
48
49 let line = line.to_string().replace('\r', "");
54
55 let mut line = LINKIFY_MAIL_RE
56 .replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
57 .as_ref()
58 .to_string();
59
60 line = LINKIFY_URL_RE
61 .replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
62 .as_ref()
63 .to_string();
64
65 line = escaper::encode_minimal(&line);
67
68 line = line.replace("\rLT", "<");
70 line = line.replace("\rGT", ">");
71 line = line.replace("\rQUOT", "\"");
72
73 if self.flowed {
74 line = line.strip_prefix(' ').unwrap_or(&line).to_string();
78 if is_quote {
79 line = "<em>".to_owned() + &line + "</em>";
80 }
81
82 if line.ends_with(' ') && !is_quote {
86 if self.delsp {
87 line.pop();
88 }
89 } else {
90 line += "<br/>\n";
91 }
92 } else {
93 if is_quote {
95 line = "<em>".to_owned() + &line + "</em>";
96 }
97 line += "<br/>\n";
98 }
99
100 let len_with_indentation = line.len();
101 let line = line.trim_start_matches(' ');
102 for _ in line.len()..len_with_indentation {
103 ret += " ";
104 }
105 ret += line;
106 }
107 ret += "</body></html>\n";
108 ret
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_plain_to_html() {
118 let html = PlainText {
119 text: r##"line 1
120line 2
121line with https://link-mid-of-line.org and http://link-end-of-line.com/file?foo=bar%20
122http://link-at-start-of-line.org
123"##
124 .to_string(),
125 flowed: false,
126 delsp: false,
127 }
128 .to_html();
129 assert_eq!(
130 html,
131 r#"<!DOCTYPE html>
132<html><head>
133<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
134<meta name="color-scheme" content="light dark" />
135</head><body>
136line 1<br/>
137line 2<br/>
138line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
139<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
140</body></html>
141"#
142 );
143 }
144
145 #[test]
146 fn test_plain_remove_signature() {
147 let html = PlainText {
148 text: "Foo\nbar\n-- \nSignature here".to_string(),
149 flowed: false,
150 delsp: false,
151 }
152 .to_html();
153 assert_eq!(
154 html,
155 r#"<!DOCTYPE html>
156<html><head>
157<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
158<meta name="color-scheme" content="light dark" />
159</head><body>
160Foo<br/>
161bar<br/>
162</body></html>
163"#
164 );
165 }
166
167 #[test]
168 fn test_plain_to_html_encapsulated() {
169 let html = PlainText {
170 text: r#"line with <http://encapsulated.link/?foo=_bar> here!"#.to_string(),
171 flowed: false,
172 delsp: false,
173 }
174 .to_html();
175 assert_eq!(
176 html,
177 r#"<!DOCTYPE html>
178<html><head>
179<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
180<meta name="color-scheme" content="light dark" />
181</head><body>
182line with <<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>> here!<br/>
183</body></html>
184"#
185 );
186 }
187
188 #[test]
189 fn test_plain_to_html_nolink() {
190 let html = PlainText {
191 text: r#"line with nohttp://no.link here"#.to_string(),
192 flowed: false,
193 delsp: false,
194 }
195 .to_html();
196 assert_eq!(
197 html,
198 r#"<!DOCTYPE html>
199<html><head>
200<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
201<meta name="color-scheme" content="light dark" />
202</head><body>
203line with nohttp://no.link here<br/>
204</body></html>
205"#
206 );
207 }
208
209 #[test]
210 fn test_plain_to_html_mailto() {
211 let html = PlainText {
212 text: r#"just an address: foo@bar.org another@one.de"#.to_string(),
213 flowed: false,
214 delsp: false,
215 }
216 .to_html();
217 assert_eq!(
218 html,
219 r#"<!DOCTYPE html>
220<html><head>
221<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
222<meta name="color-scheme" content="light dark" />
223</head><body>
224just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/>
225</body></html>
226"#
227 );
228 }
229
230 #[test]
231 fn test_plain_to_html_flowed() {
232 let html = PlainText {
233 text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
234 flowed: true,
235 delsp: false,
236 }
237 .to_html();
238 assert_eq!(
239 html,
240 r#"<!DOCTYPE html>
241<html><head>
242<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
243<meta name="color-scheme" content="light dark" />
244</head><body>
245line still line<br/>
246<em>>quote </em><br/>
247<em>>still quote</em><br/>
248>no quote<br/>
249</body></html>
250"#
251 );
252 }
253
254 #[test]
255 fn test_plain_to_html_flowed_delsp() {
256 let html = PlainText {
257 text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
258 flowed: true,
259 delsp: true,
260 }
261 .to_html();
262 assert_eq!(
263 html,
264 r#"<!DOCTYPE html>
265<html><head>
266<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
267<meta name="color-scheme" content="light dark" />
268</head><body>
269linestill line<br/>
270<em>>quote </em><br/>
271<em>>still quote</em><br/>
272>no quote<br/>
273</body></html>
274"#
275 );
276 }
277
278 #[test]
279 fn test_plain_to_html_fixed() {
280 let html = PlainText {
281 text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
282 flowed: false,
283 delsp: false,
284 }
285 .to_html();
286 assert_eq!(
287 html,
288 r#"<!DOCTYPE html>
289<html><head>
290<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
291<meta name="color-scheme" content="light dark" />
292</head><body>
293line <br/>
294still line<br/>
295<em>>quote </em><br/>
296<em>>still quote</em><br/>
297 >no quote<br/>
298</body></html>
299"#
300 );
301 }
302
303 #[test]
304 fn test_plain_to_html_indentation() {
305 let html = PlainText {
306 text: "def foo():\n pass\n\ndef bar(x):\n return x + 5".to_string(),
307 flowed: false,
308 delsp: false,
309 }
310 .to_html();
311 assert_eq!(
312 html,
313 r#"<!DOCTYPE html>
314<html><head>
315<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
316<meta name="color-scheme" content="light dark" />
317</head><body>
318def foo():<br/>
319 pass<br/>
320<br/>
321def bar(x):<br/>
322 return x + 5<br/>
323</body></html>
324"#
325 );
326 }
327}