1use std::{collections::HashMap, error::Error};
9
10pub fn is_mime_valid(raw: &str) -> bool {
41 for line in raw.lines() {
42 if line.trim().is_empty() {
43 break;
44 }
45 if line.trim().starts_with("MIME-Version:") {
46 return true;
47 }
48 }
49 false
50}
51
52pub fn parse_mime_headers(raw: &str) -> Result<HashMap<String, String>, Box<dyn Error>> {
79 let mut headers = HashMap::new();
80 for line in raw.lines() {
81 if line.trim().is_empty() {
82 break;
83 }
84 if let Some((key, value)) = line.split_once(':') {
85 headers.insert(key.trim().to_string(), value.trim().to_string());
86 }
87 }
88 Ok(headers)
89}
90
91pub fn parse_raw_headers(raw: &str) -> (Vec<(String, String)>, &str) {
108 let mut headers = Vec::new();
109 let mut pos = 0;
110
111 for line in raw.lines() {
112 let line_len = line.len();
113 let end = pos + line_len;
114 let consumed = if raw[end..].starts_with("\r\n") {
115 end + 2
116 } else if raw[end..].starts_with('\n') {
117 end + 1
118 } else {
119 end
120 };
121
122 if line.trim().is_empty() {
123 pos = consumed;
124 break;
125 }
126
127 if let Some((key, value)) = line.split_once(':') {
128 headers.push((key.trim().to_string(), value.trim().to_string()));
129 } else {
130 break;
132 }
133
134 pos = consumed;
135 }
136
137 (headers, &raw[pos..])
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_is_mime_valid_with_mime_version() {
146 assert!(is_mime_valid(
147 "MIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nBody"
148 ));
149 }
150
151 #[test]
152 fn test_is_mime_valid_without_mime_version() {
153 assert!(!is_mime_valid("Subject: Hello\r\n\r\nBody"));
154 }
155
156 #[test]
157 fn test_is_mime_valid_mime_in_body_only() {
158 assert!(!is_mime_valid("Subject: Hello\r\n\r\nMIME-Version: 1.0"));
159 }
160
161 #[test]
162 fn test_is_mime_valid_empty_input() {
163 assert!(!is_mime_valid(""));
164 }
165
166 #[test]
167 fn test_is_mime_valid_no_headers() {
168 assert!(!is_mime_valid("Plain text without headers"));
169 }
170
171 #[test]
172 fn test_is_mime_valid_mime_version_among_headers() {
173 assert!(is_mime_valid(
174 "From: a@b.com\r\nMIME-Version: 1.0\r\nTo: c@d.com\r\n\r\nBody"
175 ));
176 }
177
178 #[test]
179 fn test_parse_mime_headers_basic() {
180 let headers =
181 parse_mime_headers("From: test@example.com\r\nTo: test@example.com\r\n").unwrap();
182 assert_eq!(headers.len(), 2);
183 assert_eq!(headers["From"], "test@example.com");
184 assert_eq!(headers["To"], "test@example.com");
185 }
186
187 #[test]
188 fn test_parse_mime_headers_with_body() {
189 let headers =
190 parse_mime_headers("From: a@b.com\r\nSubject: Test\r\n\r\nBody content here").unwrap();
191 assert_eq!(headers.len(), 2);
192 assert_eq!(headers["Subject"], "Test");
193 assert!(!headers.contains_key("Body content here"));
194 }
195
196 #[test]
197 fn test_parse_mime_headers_empty_input() {
198 let headers = parse_mime_headers("").unwrap();
199 assert!(headers.is_empty());
200 }
201
202 #[test]
203 fn test_parse_mime_headers_no_headers() {
204 let headers = parse_mime_headers("\r\nBody only").unwrap();
205 assert!(headers.is_empty());
206 }
207
208 #[test]
209 fn test_parse_mime_headers_duplicate_keys() {
210 let headers = parse_mime_headers("X-Custom: first\r\nX-Custom: second\r\n").unwrap();
211 assert_eq!(headers.len(), 1);
212 assert_eq!(headers["X-Custom"], "second");
213 }
214
215 #[test]
216 fn test_parse_raw_headers_basic() {
217 let (headers, content) =
218 parse_raw_headers("From: alice@example.com\r\nTo: bob@example.com\r\n\r\nHello!");
219 assert_eq!(headers.len(), 2);
220 assert_eq!(
221 headers[0],
222 ("From".to_string(), "alice@example.com".to_string())
223 );
224 assert_eq!(
225 headers[1],
226 ("To".to_string(), "bob@example.com".to_string())
227 );
228 assert_eq!(content, "Hello!");
229 }
230
231 #[test]
232 fn test_parse_raw_headers_preserves_order() {
233 let (headers, _) = parse_raw_headers("Z-Last: z\r\nA-First: a\r\nM-Middle: m\r\n\r\nBody");
234 assert_eq!(headers[0].0, "Z-Last");
235 assert_eq!(headers[1].0, "A-First");
236 assert_eq!(headers[2].0, "M-Middle");
237 }
238
239 #[test]
240 fn test_parse_raw_headers_duplicate_headers() {
241 let (headers, _) = parse_raw_headers("Received: first\r\nReceived: second\r\n\r\nBody");
242 assert_eq!(headers.len(), 2);
243 assert_eq!(headers[0].1, "first");
244 assert_eq!(headers[1].1, "second");
245 }
246
247 #[test]
248 fn test_parse_raw_headers_empty_input() {
249 let (headers, content) = parse_raw_headers("");
250 assert!(headers.is_empty());
251 assert_eq!(content, "");
252 }
253
254 #[test]
255 fn test_parse_raw_headers_no_headers() {
256 let (headers, content) = parse_raw_headers("Plain text body");
257 assert!(headers.is_empty());
258 assert_eq!(content, "Plain text body");
259 }
260
261 #[test]
262 fn test_parse_raw_headers_empty_body() {
263 let (headers, content) = parse_raw_headers("Subject: Test\r\n\r\n");
264 assert_eq!(headers.len(), 1);
265 assert_eq!(content, "");
266 }
267
268 #[test]
269 fn test_parse_raw_headers_lf_line_endings() {
270 let (headers, content) = parse_raw_headers("From: a@b.com\nTo: c@d.com\n\nBody");
271 assert_eq!(headers.len(), 2);
272 assert_eq!(content, "Body");
273 }
274
275 #[test]
276 fn test_parse_raw_headers_trims_values() {
277 let (headers, _) = parse_raw_headers("Subject: spaced value \r\n\r\nBody");
278 assert_eq!(headers[0].1, "spaced value");
279 }
280}