1use crate::fixtures::{SmtpFixture, StoredEmail};
4use mockforge_core::protocol_abstraction::{
5 Protocol, ProtocolRequest, ProtocolResponse, ResponseStatus, SpecOperation, SpecRegistry,
6 ValidationError, ValidationResult,
7};
8use mockforge_core::Result;
9use regex::Regex;
10use std::collections::HashMap;
11use std::path::Path;
12use std::sync::RwLock;
13use tracing::{debug, error, info, warn};
14
15#[derive(Debug, Clone, Default)]
17pub struct EmailSearchFilters {
18 pub sender: Option<String>,
19 pub recipient: Option<String>,
20 pub subject: Option<String>,
21 pub body: Option<String>,
22 pub since: Option<chrono::DateTime<chrono::Utc>>,
23 pub until: Option<chrono::DateTime<chrono::Utc>>,
24 pub use_regex: bool,
25 pub case_sensitive: bool,
26}
27
28pub struct SmtpSpecRegistry {
30 fixtures: Vec<SmtpFixture>,
32 mailbox: RwLock<Vec<StoredEmail>>,
34 max_mailbox_size: usize,
36}
37
38impl SmtpSpecRegistry {
39 pub fn new() -> Self {
41 Self {
42 fixtures: Vec::new(),
43 mailbox: RwLock::new(Vec::new()),
44 max_mailbox_size: 1000,
45 }
46 }
47
48 pub fn with_mailbox_size(max_size: usize) -> Self {
50 Self {
51 fixtures: Vec::new(),
52 mailbox: RwLock::new(Vec::new()),
53 max_mailbox_size: max_size,
54 }
55 }
56
57 pub fn load_fixtures<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
59 let path = path.as_ref();
60
61 if !path.exists() {
62 warn!("Fixtures directory does not exist: {:?}", path);
63 return Ok(());
64 }
65
66 let entries = std::fs::read_dir(path)?;
67
68 for entry in entries {
69 let entry = entry?;
70 let path = entry.path();
71
72 if path.is_file() {
73 let extension = path.extension().and_then(|s| s.to_str());
74
75 match extension {
76 Some("yaml") | Some("yml") => {
77 self.load_fixture_file(&path)?;
78 }
79 Some("json") => {
80 self.load_fixture_file_json(&path)?;
81 }
82 _ => {
83 debug!("Skipping non-fixture file: {:?}", path);
84 }
85 }
86 }
87 }
88
89 info!("Loaded {} SMTP fixtures", self.fixtures.len());
90 Ok(())
91 }
92
93 fn load_fixture_file(&mut self, path: &Path) -> Result<()> {
95 let content = std::fs::read_to_string(path)?;
96 let fixture: SmtpFixture = serde_yaml::from_str(&content).map_err(|e| {
97 mockforge_core::Error::generic(format!(
98 "Failed to parse fixture file {:?}: {}",
99 path, e
100 ))
101 })?;
102
103 debug!("Loaded fixture: {} from {:?}", fixture.name, path);
104 self.fixtures.push(fixture);
105
106 Ok(())
107 }
108
109 fn load_fixture_file_json(&mut self, path: &Path) -> Result<()> {
111 let content = std::fs::read_to_string(path)?;
112 let fixture: SmtpFixture = serde_json::from_str(&content).map_err(|e| {
113 mockforge_core::Error::generic(format!(
114 "Failed to parse JSON fixture file {:?}: {}",
115 path, e
116 ))
117 })?;
118
119 debug!("Loaded fixture: {} from {:?}", fixture.name, path);
120 self.fixtures.push(fixture);
121
122 Ok(())
123 }
124
125 pub fn find_matching_fixture(
127 &self,
128 from: &str,
129 to: &str,
130 subject: &str,
131 ) -> Option<&SmtpFixture> {
132 for fixture in &self.fixtures {
134 if !fixture.match_criteria.match_all && fixture.matches(from, to, subject) {
135 return Some(fixture);
136 }
137 }
138
139 self.fixtures.iter().find(|f| f.match_criteria.match_all)
141 }
142
143 pub fn store_email(&self, email: StoredEmail) -> Result<()> {
145 let mut mailbox = self.mailbox.write().map_err(|e| {
146 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
147 })?;
148
149 if mailbox.len() >= self.max_mailbox_size {
151 warn!("Mailbox is full, removing oldest email");
152 mailbox.remove(0);
153 }
154
155 mailbox.push(email);
156 Ok(())
157 }
158
159 pub fn get_emails(&self) -> Result<Vec<StoredEmail>> {
161 let mailbox = self.mailbox.read().map_err(|e| {
162 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
163 })?;
164
165 Ok(mailbox.clone())
166 }
167
168 pub fn get_email_by_id(&self, id: &str) -> Result<Option<StoredEmail>> {
170 let mailbox = self.mailbox.read().map_err(|e| {
171 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
172 })?;
173
174 Ok(mailbox.iter().find(|e| e.id == id).cloned())
175 }
176
177 pub fn clear_mailbox(&self) -> Result<()> {
179 let mut mailbox = self.mailbox.write().map_err(|e| {
180 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
181 })?;
182
183 mailbox.clear();
184 info!("Mailbox cleared");
185 Ok(())
186 }
187
188 pub fn get_mailbox_stats(&self) -> Result<MailboxStats> {
190 let mailbox = self.mailbox.read().map_err(|e| {
191 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
192 })?;
193
194 Ok(MailboxStats {
195 total_emails: mailbox.len(),
196 max_capacity: self.max_mailbox_size,
197 })
198 }
199
200 pub fn search_emails(&self, filters: EmailSearchFilters) -> Result<Vec<StoredEmail>> {
202 let mailbox = self.mailbox.read().map_err(|e| {
203 mockforge_core::Error::generic(format!("Failed to acquire mailbox lock: {}", e))
204 })?;
205
206 let mut results: Vec<StoredEmail> = mailbox
207 .iter()
208 .filter(|email| {
209 let matches_filter = |field: &str, filter: &Option<String>| -> bool {
211 if let Some(ref f) = filter {
212 let field_cmp = if filters.case_sensitive {
213 field.to_string()
214 } else {
215 field.to_lowercase()
216 };
217 let filter_cmp = if filters.case_sensitive {
218 f.clone()
219 } else {
220 f.to_lowercase()
221 };
222
223 if filters.use_regex {
224 if let Ok(re) = Regex::new(&filter_cmp) {
225 re.is_match(&field_cmp)
226 } else {
227 false }
229 } else {
230 field_cmp.contains(&filter_cmp)
231 }
232 } else {
233 true
234 }
235 };
236
237 if !matches_filter(&email.from, &filters.sender) {
239 return false;
240 }
241
242 if let Some(ref recipient_filter) = filters.recipient {
244 let has_recipient = email
245 .to
246 .iter()
247 .any(|to| matches_filter(to, &Some(recipient_filter.clone())));
248 if !has_recipient {
249 return false;
250 }
251 }
252
253 if !matches_filter(&email.subject, &filters.subject) {
255 return false;
256 }
257
258 if !matches_filter(&email.body, &filters.body) {
260 return false;
261 }
262
263 if let Some(since) = filters.since {
265 if email.received_at < since {
266 return false;
267 }
268 }
269
270 if let Some(until) = filters.until {
271 if email.received_at > until {
272 return false;
273 }
274 }
275
276 true
277 })
278 .cloned()
279 .collect();
280
281 results.sort_by(|a, b| b.received_at.cmp(&a.received_at));
283
284 Ok(results)
285 }
286}
287
288#[derive(Debug, Clone)]
290pub struct MailboxStats {
291 pub total_emails: usize,
292 pub max_capacity: usize,
293}
294
295impl Default for SmtpSpecRegistry {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl SpecRegistry for SmtpSpecRegistry {
302 fn protocol(&self) -> Protocol {
303 Protocol::Smtp
304 }
305
306 fn operations(&self) -> Vec<SpecOperation> {
307 self.fixtures
308 .iter()
309 .map(|fixture| SpecOperation {
310 name: fixture.name.clone(),
311 path: fixture.identifier.clone(),
312 operation_type: "SEND".to_string(),
313 input_schema: None,
314 output_schema: None,
315 metadata: HashMap::from([
316 ("description".to_string(), fixture.description.clone()),
317 ("status_code".to_string(), fixture.response.status_code.to_string()),
318 ]),
319 })
320 .collect()
321 }
322
323 fn find_operation(&self, operation: &str, path: &str) -> Option<SpecOperation> {
324 self.fixtures
325 .iter()
326 .find(|f| f.identifier == path)
327 .map(|fixture| SpecOperation {
328 name: fixture.name.clone(),
329 path: fixture.identifier.clone(),
330 operation_type: operation.to_string(),
331 input_schema: None,
332 output_schema: None,
333 metadata: HashMap::from([
334 ("description".to_string(), fixture.description.clone()),
335 ("status_code".to_string(), fixture.response.status_code.to_string()),
336 ]),
337 })
338 }
339
340 fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult> {
341 if request.protocol != Protocol::Smtp {
343 return Ok(ValidationResult::failure(vec![ValidationError {
344 message: "Invalid protocol for SMTP registry".to_string(),
345 path: None,
346 code: Some("INVALID_PROTOCOL".to_string()),
347 }]));
348 }
349
350 let from = request.metadata.get("from");
352 let to = request.metadata.get("to");
353
354 if from.is_none() {
355 return Ok(ValidationResult::failure(vec![ValidationError {
356 message: "Missing 'from' address".to_string(),
357 path: Some("metadata.from".to_string()),
358 code: Some("MISSING_FROM".to_string()),
359 }]));
360 }
361
362 if to.is_none() {
363 return Ok(ValidationResult::failure(vec![ValidationError {
364 message: "Missing 'to' address".to_string(),
365 path: Some("metadata.to".to_string()),
366 code: Some("MISSING_TO".to_string()),
367 }]));
368 }
369
370 Ok(ValidationResult::success())
371 }
372
373 fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse> {
374 let from = request.metadata.get("from").unwrap_or(&String::new()).clone();
375 let to = request.metadata.get("to").unwrap_or(&String::new()).clone();
376 let subject = request.metadata.get("subject").unwrap_or(&String::new()).clone();
377
378 let fixture = self
380 .find_matching_fixture(&from, &to, &subject)
381 .ok_or_else(|| mockforge_core::Error::generic("No matching fixture found for email"))?;
382
383 if fixture.storage.save_to_mailbox {
385 let email = StoredEmail {
386 id: uuid::Uuid::new_v4().to_string(),
387 from: from.clone(),
388 to: to.split(',').map(|s| s.trim().to_string()).collect(),
389 subject: subject.clone(),
390 body: String::from_utf8_lossy(request.body.as_ref().unwrap_or(&Vec::new()))
391 .to_string(),
392 headers: request.metadata.clone(),
393 received_at: chrono::Utc::now(),
394 raw: request.body.clone(),
395 };
396
397 if let Err(e) = self.store_email(email) {
398 error!("Failed to store email: {}", e);
399 }
400 }
401
402 let response_message =
404 format!("{} {}\r\n", fixture.response.status_code, fixture.response.message);
405
406 Ok(ProtocolResponse {
407 status: ResponseStatus::SmtpStatus(fixture.response.status_code),
408 metadata: HashMap::new(),
409 body: response_message.into_bytes(),
410 content_type: "text/plain".to_string(),
411 })
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_registry_creation() {
421 let registry = SmtpSpecRegistry::new();
422 assert_eq!(registry.protocol(), Protocol::Smtp);
423 assert_eq!(registry.fixtures.len(), 0);
424 }
425
426 #[test]
427 fn test_registry_default() {
428 let registry = SmtpSpecRegistry::default();
429 assert_eq!(registry.protocol(), Protocol::Smtp);
430 assert_eq!(registry.max_mailbox_size, 1000);
431 }
432
433 #[test]
434 fn test_mailbox_operations() {
435 let registry = SmtpSpecRegistry::new();
436
437 let email = StoredEmail {
438 id: "test-123".to_string(),
439 from: "sender@example.com".to_string(),
440 to: vec!["recipient@example.com".to_string()],
441 subject: "Test".to_string(),
442 body: "Test body".to_string(),
443 headers: HashMap::new(),
444 received_at: chrono::Utc::now(),
445 raw: None,
446 };
447
448 registry.store_email(email.clone()).unwrap();
449
450 let emails = registry.get_emails().unwrap();
451 assert_eq!(emails.len(), 1);
452 assert_eq!(emails[0].from, "sender@example.com");
453
454 registry.clear_mailbox().unwrap();
455 let emails = registry.get_emails().unwrap();
456 assert_eq!(emails.len(), 0);
457 }
458
459 #[test]
460 fn test_mailbox_size_limit() {
461 let registry = SmtpSpecRegistry::with_mailbox_size(2);
462
463 for i in 0..5 {
464 let email = StoredEmail {
465 id: format!("test-{}", i),
466 from: "sender@example.com".to_string(),
467 to: vec!["recipient@example.com".to_string()],
468 subject: format!("Test {}", i),
469 body: "Test body".to_string(),
470 headers: HashMap::new(),
471 received_at: chrono::Utc::now(),
472 raw: None,
473 };
474
475 registry.store_email(email).unwrap();
476 }
477
478 let emails = registry.get_emails().unwrap();
479 assert_eq!(emails.len(), 2); }
481
482 #[test]
483 fn test_get_email_by_id() {
484 let registry = SmtpSpecRegistry::new();
485
486 let email = StoredEmail {
487 id: "unique-id-123".to_string(),
488 from: "sender@example.com".to_string(),
489 to: vec!["recipient@example.com".to_string()],
490 subject: "Test".to_string(),
491 body: "Test body".to_string(),
492 headers: HashMap::new(),
493 received_at: chrono::Utc::now(),
494 raw: None,
495 };
496
497 registry.store_email(email).unwrap();
498
499 let found = registry.get_email_by_id("unique-id-123").unwrap();
500 assert!(found.is_some());
501 assert_eq!(found.unwrap().id, "unique-id-123");
502
503 let not_found = registry.get_email_by_id("nonexistent").unwrap();
504 assert!(not_found.is_none());
505 }
506
507 #[test]
508 fn test_mailbox_stats() {
509 let registry = SmtpSpecRegistry::with_mailbox_size(100);
510
511 let stats = registry.get_mailbox_stats().unwrap();
512 assert_eq!(stats.total_emails, 0);
513 assert_eq!(stats.max_capacity, 100);
514
515 for i in 0..5 {
516 let email = StoredEmail {
517 id: format!("test-{}", i),
518 from: "sender@example.com".to_string(),
519 to: vec!["recipient@example.com".to_string()],
520 subject: format!("Test {}", i),
521 body: "Test body".to_string(),
522 headers: HashMap::new(),
523 received_at: chrono::Utc::now(),
524 raw: None,
525 };
526 registry.store_email(email).unwrap();
527 }
528
529 let stats = registry.get_mailbox_stats().unwrap();
530 assert_eq!(stats.total_emails, 5);
531 }
532
533 #[test]
534 fn test_email_search_filters_default() {
535 let filters = EmailSearchFilters::default();
536 assert!(filters.sender.is_none());
537 assert!(filters.recipient.is_none());
538 assert!(filters.subject.is_none());
539 assert!(filters.body.is_none());
540 assert!(!filters.use_regex);
541 assert!(!filters.case_sensitive);
542 }
543
544 #[test]
545 fn test_search_emails_by_sender() {
546 let registry = SmtpSpecRegistry::new();
547
548 for i in 0..3 {
550 let email = StoredEmail {
551 id: format!("test-{}", i),
552 from: format!("sender{}@example.com", i),
553 to: vec!["recipient@example.com".to_string()],
554 subject: "Test".to_string(),
555 body: "Test body".to_string(),
556 headers: HashMap::new(),
557 received_at: chrono::Utc::now(),
558 raw: None,
559 };
560 registry.store_email(email).unwrap();
561 }
562
563 let filters = EmailSearchFilters {
564 sender: Some("sender1".to_string()),
565 ..Default::default()
566 };
567 let results = registry.search_emails(filters).unwrap();
568 assert_eq!(results.len(), 1);
569 assert!(results[0].from.contains("sender1"));
570 }
571
572 #[test]
573 fn test_search_emails_by_subject() {
574 let registry = SmtpSpecRegistry::new();
575
576 let email1 = StoredEmail {
577 id: "test-1".to_string(),
578 from: "sender@example.com".to_string(),
579 to: vec!["recipient@example.com".to_string()],
580 subject: "Important update".to_string(),
581 body: "Test body".to_string(),
582 headers: HashMap::new(),
583 received_at: chrono::Utc::now(),
584 raw: None,
585 };
586 let email2 = StoredEmail {
587 id: "test-2".to_string(),
588 from: "sender@example.com".to_string(),
589 to: vec!["recipient@example.com".to_string()],
590 subject: "Newsletter".to_string(),
591 body: "Test body".to_string(),
592 headers: HashMap::new(),
593 received_at: chrono::Utc::now(),
594 raw: None,
595 };
596
597 registry.store_email(email1).unwrap();
598 registry.store_email(email2).unwrap();
599
600 let filters = EmailSearchFilters {
601 subject: Some("Important".to_string()),
602 ..Default::default()
603 };
604 let results = registry.search_emails(filters).unwrap();
605 assert_eq!(results.len(), 1);
606 assert!(results[0].subject.contains("Important"));
607 }
608
609 #[test]
610 fn test_search_emails_with_regex() {
611 let registry = SmtpSpecRegistry::new();
612
613 let email = StoredEmail {
614 id: "test-1".to_string(),
615 from: "admin@example.com".to_string(),
616 to: vec!["recipient@example.com".to_string()],
617 subject: "Test".to_string(),
618 body: "Test body".to_string(),
619 headers: HashMap::new(),
620 received_at: chrono::Utc::now(),
621 raw: None,
622 };
623 registry.store_email(email).unwrap();
624
625 let filters = EmailSearchFilters {
626 sender: Some(r"^admin@.*\.com$".to_string()),
627 use_regex: true,
628 ..Default::default()
629 };
630 let results = registry.search_emails(filters).unwrap();
631 assert_eq!(results.len(), 1);
632 }
633
634 #[test]
635 fn test_operations_empty() {
636 let registry = SmtpSpecRegistry::new();
637 let ops = registry.operations();
638 assert!(ops.is_empty());
639 }
640
641 #[test]
642 fn test_find_operation_not_found() {
643 let registry = SmtpSpecRegistry::new();
644 let op = registry.find_operation("SEND", "/nonexistent");
645 assert!(op.is_none());
646 }
647
648 #[test]
649 fn test_validate_request_missing_from() {
650 let registry = SmtpSpecRegistry::new();
651 let request = ProtocolRequest {
652 protocol: Protocol::Smtp,
653 pattern: mockforge_core::protocol_abstraction::MessagePattern::OneWay,
654 operation: "SEND".to_string(),
655 path: "/".to_string(),
656 topic: None,
657 routing_key: None,
658 partition: None,
659 qos: None,
660 metadata: HashMap::from([("to".to_string(), "recipient@example.com".to_string())]),
661 body: None,
662 client_ip: None,
663 };
664
665 let result = registry.validate_request(&request).unwrap();
666 assert!(!result.valid);
667 }
668
669 #[test]
670 fn test_validate_request_missing_to() {
671 let registry = SmtpSpecRegistry::new();
672 let request = ProtocolRequest {
673 protocol: Protocol::Smtp,
674 pattern: mockforge_core::protocol_abstraction::MessagePattern::OneWay,
675 operation: "SEND".to_string(),
676 path: "/".to_string(),
677 topic: None,
678 routing_key: None,
679 partition: None,
680 qos: None,
681 metadata: HashMap::from([("from".to_string(), "sender@example.com".to_string())]),
682 body: None,
683 client_ip: None,
684 };
685
686 let result = registry.validate_request(&request).unwrap();
687 assert!(!result.valid);
688 }
689
690 #[test]
691 fn test_validate_request_valid() {
692 let registry = SmtpSpecRegistry::new();
693 let request = ProtocolRequest {
694 protocol: Protocol::Smtp,
695 pattern: mockforge_core::protocol_abstraction::MessagePattern::OneWay,
696 operation: "SEND".to_string(),
697 path: "/".to_string(),
698 topic: None,
699 routing_key: None,
700 partition: None,
701 qos: None,
702 metadata: HashMap::from([
703 ("from".to_string(), "sender@example.com".to_string()),
704 ("to".to_string(), "recipient@example.com".to_string()),
705 ]),
706 body: None,
707 client_ip: None,
708 };
709
710 let result = registry.validate_request(&request).unwrap();
711 assert!(result.valid);
712 }
713
714 #[test]
715 fn test_validate_request_wrong_protocol() {
716 let registry = SmtpSpecRegistry::new();
717 let request = ProtocolRequest {
718 protocol: Protocol::Http,
719 pattern: mockforge_core::protocol_abstraction::MessagePattern::OneWay,
720 operation: "SEND".to_string(),
721 path: "/".to_string(),
722 topic: None,
723 routing_key: None,
724 partition: None,
725 qos: None,
726 metadata: HashMap::new(),
727 body: None,
728 client_ip: None,
729 };
730
731 let result = registry.validate_request(&request).unwrap();
732 assert!(!result.valid);
733 }
734
735 #[test]
736 fn test_load_fixtures_nonexistent_dir() {
737 let mut registry = SmtpSpecRegistry::new();
738 let result = registry.load_fixtures("/nonexistent/path");
739 assert!(result.is_ok());
741 assert_eq!(registry.fixtures.len(), 0);
742 }
743}