mockforge_smtp/
fixtures.rs

1//! SMTP fixture definitions and loading
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// An SMTP fixture defining how to handle emails
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SmtpFixture {
9    /// Unique identifier for this fixture
10    pub identifier: String,
11
12    /// Human-readable name
13    pub name: String,
14
15    /// Description of what this fixture does
16    #[serde(default)]
17    pub description: String,
18
19    /// Matching criteria for emails
20    pub match_criteria: MatchCriteria,
21
22    /// Response configuration
23    pub response: SmtpResponse,
24
25    /// Auto-reply configuration
26    #[serde(default)]
27    pub auto_reply: Option<AutoReply>,
28
29    /// Storage configuration
30    #[serde(default)]
31    pub storage: StorageConfig,
32
33    /// Behavior simulation
34    #[serde(default)]
35    pub behavior: BehaviorConfig,
36}
37
38/// Criteria for matching incoming emails
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40pub struct MatchCriteria {
41    /// Match by recipient pattern (regex)
42    #[serde(default)]
43    pub recipient_pattern: Option<String>,
44
45    /// Match by sender pattern (regex)
46    #[serde(default)]
47    pub sender_pattern: Option<String>,
48
49    /// Match by subject pattern (regex)
50    #[serde(default)]
51    pub subject_pattern: Option<String>,
52
53    /// Match all (default fixture)
54    #[serde(default)]
55    pub match_all: bool,
56}
57
58/// SMTP response configuration
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SmtpResponse {
61    /// SMTP status code (250 = success, 550 = reject)
62    pub status_code: u16,
63
64    /// Status message
65    pub message: String,
66
67    /// Delay before responding (milliseconds)
68    #[serde(default)]
69    pub delay_ms: u64,
70}
71
72/// Auto-reply email configuration
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AutoReply {
75    /// Enable auto-reply
76    pub enabled: bool,
77
78    /// From address
79    pub from: String,
80
81    /// To address (supports template: {{metadata.from}})
82    pub to: String,
83
84    /// Email subject
85    pub subject: String,
86
87    /// Email body (supports templates)
88    pub body: String,
89
90    /// Optional HTML body
91    #[serde(default)]
92    pub html_body: Option<String>,
93
94    /// Custom headers
95    #[serde(default)]
96    pub headers: HashMap<String, String>,
97}
98
99/// Email storage configuration
100#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct StorageConfig {
102    /// Save emails to mailbox
103    #[serde(default)]
104    pub save_to_mailbox: bool,
105
106    /// Export to file
107    #[serde(default)]
108    pub export_to_file: Option<ExportConfig>,
109}
110
111/// File export configuration
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ExportConfig {
114    /// Enable export
115    pub enabled: bool,
116
117    /// File path (supports templates)
118    pub path: String,
119}
120
121/// Behavior simulation configuration
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct BehaviorConfig {
124    /// Failure rate (0.0 - 1.0)
125    #[serde(default)]
126    pub failure_rate: f64,
127
128    /// Latency range
129    #[serde(default)]
130    pub latency: Option<LatencyConfig>,
131}
132
133/// Latency configuration
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct LatencyConfig {
136    /// Minimum latency in milliseconds
137    pub min_ms: u64,
138
139    /// Maximum latency in milliseconds
140    pub max_ms: u64,
141}
142
143/// Stored email message
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct StoredEmail {
146    /// Unique ID
147    pub id: String,
148
149    /// From address
150    pub from: String,
151
152    /// To addresses
153    pub to: Vec<String>,
154
155    /// Subject
156    pub subject: String,
157
158    /// Body content
159    pub body: String,
160
161    /// Headers
162    pub headers: HashMap<String, String>,
163
164    /// Received timestamp
165    pub received_at: chrono::DateTime<chrono::Utc>,
166
167    /// Raw email data
168    #[serde(default)]
169    pub raw: Option<Vec<u8>>,
170}
171
172impl SmtpFixture {
173    /// Check if this fixture matches the given email criteria
174    pub fn matches(&self, from: &str, to: &str, subject: &str) -> bool {
175        use regex::Regex;
176
177        // If match_all is true, this fixture matches everything
178        if self.match_criteria.match_all {
179            return true;
180        }
181
182        // Check recipient pattern
183        if let Some(pattern) = &self.match_criteria.recipient_pattern {
184            if let Ok(re) = Regex::new(pattern) {
185                if !re.is_match(to) {
186                    return false;
187                }
188            }
189        }
190
191        // Check sender pattern
192        if let Some(pattern) = &self.match_criteria.sender_pattern {
193            if let Ok(re) = Regex::new(pattern) {
194                if !re.is_match(from) {
195                    return false;
196                }
197            }
198        }
199
200        // Check subject pattern
201        if let Some(pattern) = &self.match_criteria.subject_pattern {
202            if let Ok(re) = Regex::new(pattern) {
203                if !re.is_match(subject) {
204                    return false;
205                }
206            }
207        }
208
209        true
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_fixture_matching() {
219        let fixture = SmtpFixture {
220            identifier: "test".to_string(),
221            name: "Test Fixture".to_string(),
222            description: "".to_string(),
223            match_criteria: MatchCriteria {
224                recipient_pattern: Some(r"^user.*@example\.com$".to_string()),
225                sender_pattern: None,
226                subject_pattern: None,
227                match_all: false,
228            },
229            response: SmtpResponse {
230                status_code: 250,
231                message: "OK".to_string(),
232                delay_ms: 0,
233            },
234            auto_reply: None,
235            storage: StorageConfig::default(),
236            behavior: BehaviorConfig::default(),
237        };
238
239        assert!(fixture.matches("sender@test.com", "user123@example.com", "Test"));
240        assert!(!fixture.matches("sender@test.com", "admin@example.com", "Test"));
241    }
242
243    #[test]
244    fn test_match_all_fixture() {
245        let fixture = SmtpFixture {
246            identifier: "default".to_string(),
247            name: "Default Fixture".to_string(),
248            description: "".to_string(),
249            match_criteria: MatchCriteria {
250                recipient_pattern: None,
251                sender_pattern: None,
252                subject_pattern: None,
253                match_all: true,
254            },
255            response: SmtpResponse {
256                status_code: 250,
257                message: "OK".to_string(),
258                delay_ms: 0,
259            },
260            auto_reply: None,
261            storage: StorageConfig::default(),
262            behavior: BehaviorConfig::default(),
263        };
264
265        assert!(fixture.matches("any@sender.com", "any@recipient.com", "Any Subject"));
266    }
267}