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