1use std::{collections::HashSet, net::IpAddr};
10
11use uuid::Uuid;
12
13use crate::parse_raw_headers;
14
15#[derive(Debug, Clone)]
22pub struct EmailMessage {
23 pub message_id: String,
25
26 pub from: String,
28
29 pub to: String,
31
32 pub client_ip: Option<IpAddr>,
34
35 pub helo_domain: Option<String>,
37
38 headers: Vec<(String, String)>,
40
41 body: String,
43
44 raw: String,
47
48 original_raw: String,
51}
52
53impl EmailMessage {
54 pub fn new(from: String, to: String, raw: String) -> Self {
55 let message_id = Uuid::new_v4().to_string();
56 let (headers, content) = parse_raw_headers(&raw);
57 Self {
58 message_id,
59 from,
60 to,
61 headers,
62 body: content.to_string(),
63 raw: raw.clone(),
64 original_raw: raw,
65 client_ip: None,
66 helo_domain: None,
67 }
68 }
69
70 pub fn from_raw(from: &str, to: &str, raw: &str) -> Self {
71 Self::new(from.to_string(), to.to_string(), raw.to_string())
72 }
73
74 pub fn with_id(
75 message_id: String,
76 from: String,
77 to: String,
78 subject: String,
79 raw: String,
80 ) -> Self {
81 let (headers, content) = parse_raw_headers(&raw);
82 let has_subject = headers
83 .iter()
84 .any(|(k, _)| k.eq_ignore_ascii_case("Subject"));
85 let mut msg = Self {
86 message_id,
87 from,
88 to,
89 headers,
90 body: content.to_string(),
91 raw: raw.clone(),
92 original_raw: raw,
93 client_ip: None,
94 helo_domain: None,
95 };
96 if !has_subject && !subject.is_empty() {
97 msg.prepend_header("Subject", &subject);
98 msg.rebuild();
99 }
100 msg
101 }
102
103 pub fn header(&self, name: &str) -> Option<&str> {
105 self.headers
106 .iter()
107 .find(|(k, _)| k.eq_ignore_ascii_case(name))
108 .map(|(_, v)| v.as_str())
109 }
110
111 pub fn subject(&self) -> &str {
113 self.header("Subject").unwrap_or_default()
114 }
115
116 pub fn raw(&self) -> &str {
121 &self.raw
122 }
123
124 pub fn original_raw(&self) -> &str {
129 &self.original_raw
130 }
131
132 pub fn body(&self) -> &str {
134 &self.body
135 }
136
137 pub fn has_headers(&self) -> bool {
139 !self.headers.is_empty()
140 }
141
142 pub fn headers(&self) -> &[(String, String)] {
144 &self.headers
145 }
146
147 pub fn prepend_header(&mut self, name: &str, value: &str) {
153 self.headers
154 .insert(0, (name.to_string(), value.to_string()));
155 }
156
157 pub fn rebuild(&mut self) {
165 let headers_len: usize = self
166 .headers
167 .iter()
168 .map(|(k, v)| k.len() + 2 + v.len() + 2)
169 .sum();
170
171 let capacity = headers_len + if self.headers.is_empty() { 0 } else { 2 } + self.body.len();
172
173 let mut raw = String::with_capacity(capacity);
174
175 for (key, value) in &self.headers {
176 raw.push_str(key);
177 raw.push_str(": ");
178 raw.push_str(value);
179 raw.push_str("\r\n");
180 }
181
182 if !self.headers.is_empty() {
183 raw.push_str("\r\n");
184 }
185
186 raw.push_str(&self.body);
187
188 self.raw = raw;
189 }
190}
191
192#[derive(Debug, Clone)]
197pub struct IncomingMessage {
198 pub from: String,
200
201 pub rcpts: HashSet<String>,
203
204 pub raw: String,
206
207 pub client_ip: Option<IpAddr>,
209
210 pub helo_domain: Option<String>,
212}
213
214impl IncomingMessage {
215 pub fn to_email_message(&self, rcpt: &str) -> EmailMessage {
217 let mut message = EmailMessage::new(self.from.clone(), rcpt.to_string(), self.raw.clone());
218 message.client_ip = self.client_ip;
219 message.helo_domain = self.helo_domain.clone();
220 message
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use std::collections::HashSet;
227
228 use super::*;
229
230 #[test]
231 fn test_email_message_new() {
232 let message = EmailMessage::new(
233 "sender@example.com".to_string(),
234 "recipient@example.com".to_string(),
235 "Subject: Hello\r\n\r\nBody text".to_string(),
236 );
237
238 assert_eq!(message.from, "sender@example.com");
239 assert_eq!(message.to, "recipient@example.com");
240 assert_eq!(message.subject(), "Hello");
241 assert_eq!(message.body(), "Body text");
242 assert_eq!(message.raw(), "Subject: Hello\r\n\r\nBody text");
243 }
244
245 #[test]
246 fn test_email_message_from_raw() {
247 let message = EmailMessage::from_raw(
248 "sender@example.com",
249 "recipient@example.com",
250 "Subject: Test\r\n\r\nContent",
251 );
252
253 assert_eq!(message.subject(), "Test");
254 assert_eq!(message.body(), "Content");
255 }
256
257 #[test]
258 fn test_email_message_with_id() {
259 let message = EmailMessage::with_id(
260 "custom-id".to_string(),
261 "sender@example.com".to_string(),
262 "recipient@example.com".to_string(),
263 "My Subject".to_string(),
264 "Body only".to_string(),
265 );
266
267 assert_eq!(message.message_id, "custom-id");
268 assert_eq!(message.subject(), "My Subject");
269 assert!(message.raw().contains("Subject: My Subject"));
270 assert!(message.raw().contains("Body only"));
271 }
272
273 #[test]
274 fn test_email_message_with_id_existing_subject() {
275 let message = EmailMessage::with_id(
276 "id".to_string(),
277 "from@test.com".to_string(),
278 "to@test.com".to_string(),
279 "Ignored".to_string(),
280 "Subject: Existing\r\n\r\nBody".to_string(),
281 );
282
283 assert_eq!(message.subject(), "Existing");
284 }
285
286 #[test]
287 fn test_email_message_no_headers() {
288 let message = EmailMessage::from_raw("from@test.com", "to@test.com", "Plain text body");
289
290 assert!(!message.has_headers());
291 assert_eq!(message.subject(), "");
292 assert_eq!(message.body(), "Plain text body");
293 }
294
295 #[test]
296 fn test_email_message_prepend_header_and_rebuild() {
297 let mut message =
298 EmailMessage::from_raw("from@test.com", "to@test.com", "Subject: Test\r\n\r\nBody");
299
300 message.prepend_header("X-Custom", "value");
301 message.rebuild();
302
303 assert!(message.raw().starts_with("X-Custom: value\r\n"));
304 assert!(message.raw().contains("Subject: Test"));
305 assert!(message.raw().ends_with("Body"));
306 }
307
308 #[test]
309 fn test_email_message_original_raw_preserved() {
310 let mut message =
311 EmailMessage::from_raw("from@test.com", "to@test.com", "Subject: Test\r\n\r\nBody");
312 let original = message.original_raw().to_string();
313
314 message.prepend_header("X-New", "header");
315 message.rebuild();
316
317 assert_eq!(message.original_raw(), original);
318 assert_ne!(message.raw(), message.original_raw());
319 }
320
321 #[test]
322 fn test_email_message_headers_accessor() {
323 let message = EmailMessage::from_raw(
324 "from@test.com",
325 "to@test.com",
326 "From: a@b.com\r\nTo: c@d.com\r\n\r\nBody",
327 );
328
329 assert_eq!(message.headers().len(), 2);
330 assert_eq!(message.headers()[0].0, "From");
331 assert_eq!(message.headers()[1].0, "To");
332 }
333
334 #[test]
335 fn test_incoming_message_to_email_message() {
336 let incoming = IncomingMessage {
337 from: "sender@example.com".to_string(),
338 rcpts: HashSet::from(["rcpt@example.com".to_string()]),
339 raw: "Subject: Test\r\n\r\nBody".to_string(),
340 client_ip: Some("127.0.0.1".parse().unwrap()),
341 helo_domain: Some("mail.example.com".to_string()),
342 };
343
344 let message = incoming.to_email_message("rcpt@example.com");
345
346 assert_eq!(message.from, "sender@example.com");
347 assert_eq!(message.to, "rcpt@example.com");
348 assert_eq!(message.raw(), "Subject: Test\r\n\r\nBody");
349 assert_eq!(message.client_ip, Some("127.0.0.1".parse().unwrap()));
350 assert_eq!(message.helo_domain, Some("mail.example.com".to_string()));
351 }
352
353 #[test]
354 fn test_incoming_message_to_email_message_without_metadata() {
355 let incoming = IncomingMessage {
356 from: "sender@example.com".to_string(),
357 rcpts: HashSet::from(["rcpt@example.com".to_string()]),
358 raw: "Hello".to_string(),
359 client_ip: None,
360 helo_domain: None,
361 };
362
363 let message = incoming.to_email_message("rcpt@example.com");
364
365 assert_eq!(message.from, "sender@example.com");
366 assert_eq!(message.to, "rcpt@example.com");
367 assert_eq!(message.raw(), "Hello");
368 assert!(message.client_ip.is_none());
369 assert!(message.helo_domain.is_none());
370 }
371
372 #[test]
373 fn test_incoming_message_to_email_message_different_recipient() {
374 let incoming = IncomingMessage {
375 from: "sender@example.com".to_string(),
376 rcpts: HashSet::from([
377 "alice@example.com".to_string(),
378 "bob@example.com".to_string(),
379 ]),
380 raw: "Subject: Multi\r\n\r\nBody".to_string(),
381 client_ip: Some("10.0.0.1".parse().unwrap()),
382 helo_domain: Some("smtp.example.com".to_string()),
383 };
384
385 let msg_alice = incoming.to_email_message("alice@example.com");
386 let msg_bob = incoming.to_email_message("bob@example.com");
387
388 assert_eq!(msg_alice.to, "alice@example.com");
389 assert_eq!(msg_bob.to, "bob@example.com");
390 assert_eq!(msg_alice.from, msg_bob.from);
391 assert_eq!(msg_alice.raw(), msg_bob.raw());
392 assert_ne!(msg_alice.message_id, msg_bob.message_id);
393 }
394
395 #[test]
396 fn test_incoming_message_to_email_message_parses_subject() {
397 let incoming = IncomingMessage {
398 from: "sender@example.com".to_string(),
399 rcpts: HashSet::from(["rcpt@example.com".to_string()]),
400 raw: "Subject: Important Update\r\n\r\nBody content".to_string(),
401 client_ip: None,
402 helo_domain: None,
403 };
404
405 let message = incoming.to_email_message("rcpt@example.com");
406
407 assert_eq!(message.subject(), "Important Update");
408 }
409
410 #[test]
411 fn test_incoming_message_to_email_message_generates_unique_ids() {
412 let incoming = IncomingMessage {
413 from: "sender@example.com".to_string(),
414 rcpts: HashSet::from(["rcpt@example.com".to_string()]),
415 raw: "Body".to_string(),
416 client_ip: None,
417 helo_domain: None,
418 };
419
420 let msg1 = incoming.to_email_message("rcpt@example.com");
421 let msg2 = incoming.to_email_message("rcpt@example.com");
422
423 assert_ne!(msg1.message_id, msg2.message_id);
424 }
425}