elif_validation/validators/
email.rs

1//! Email format validator
2
3use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use regex::Regex;
7use serde_json::Value;
8
9/// Validator for email address format
10#[derive(Debug, Clone)]
11pub struct EmailValidator {
12    /// Custom error message
13    pub message: Option<String>,
14    /// Allow international domain names
15    pub allow_unicode: bool,
16    /// Require TLD (top-level domain)
17    pub require_tld: bool,
18    /// Custom regex pattern (overrides default)
19    pub custom_pattern: Option<Regex>,
20}
21
22impl EmailValidator {
23    /// Create a new email validator with default settings
24    pub fn new() -> Self {
25        Self {
26            message: None,
27            allow_unicode: false,
28            require_tld: true,
29            custom_pattern: None,
30        }
31    }
32
33    /// Set custom error message
34    pub fn message(mut self, message: impl Into<String>) -> Self {
35        self.message = Some(message.into());
36        self
37    }
38
39    /// Allow unicode characters in domain names (internationalized domains)
40    pub fn allow_unicode(mut self, allow: bool) -> Self {
41        self.allow_unicode = allow;
42        self
43    }
44
45    /// Require top-level domain (e.g., .com, .org)
46    pub fn require_tld(mut self, require: bool) -> Self {
47        self.require_tld = require;
48        self
49    }
50
51    /// Use custom regex pattern for validation
52    pub fn custom_pattern(mut self, pattern: Regex) -> Self {
53        self.custom_pattern = Some(pattern);
54        self
55    }
56
57    /// Get the appropriate regex pattern based on configuration
58    fn get_pattern(&self) -> Result<Regex, regex::Error> {
59        if let Some(ref pattern) = self.custom_pattern {
60            return Ok(pattern.clone());
61        }
62
63        // Basic email regex pattern
64        // This is a simplified pattern that catches most common cases
65        // For production use, consider using a dedicated email validation library
66        let pattern = if self.allow_unicode {
67            if self.require_tld {
68                // Unicode-aware with TLD requirement
69                r"^[^\s@.]+[^\s@]*@[^\s@.]+[^\s@]*\.[^\s@]+$"
70            } else {
71                // Unicode-aware without TLD requirement
72                r"^[^\s@.]+[^\s@]*@[^\s@.]+[^\s@]*$"
73            }
74        } else if self.require_tld {
75            // ASCII-only with TLD requirement (no consecutive dots)
76            r"^[a-zA-Z0-9]([a-zA-Z0-9._%+-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$"
77        } else {
78            // ASCII-only without TLD requirement (no consecutive dots)
79            r"^[a-zA-Z0-9]([a-zA-Z0-9._%+-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$"
80        };
81
82        Regex::new(pattern)
83    }
84
85    /// Validate email format
86    fn validate_email_format(&self, email: &str) -> bool {
87        // Basic checks first
88        if email.is_empty() {
89            return false;
90        }
91
92        // Must contain exactly one @ symbol
93        let at_count = email.matches('@').count();
94        if at_count != 1 {
95            return false;
96        }
97
98        // Split into local and domain parts
99        let parts: Vec<&str> = email.split('@').collect();
100        if parts.len() != 2 {
101            return false;
102        }
103
104        let local_part = parts[0];
105        let domain_part = parts[1];
106
107        // Local part cannot be empty
108        if local_part.is_empty() {
109            return false;
110        }
111
112        // Domain part cannot be empty
113        if domain_part.is_empty() {
114            return false;
115        }
116
117        // Local part length check (RFC 5321 limit)
118        if local_part.len() > 64 {
119            return false;
120        }
121
122        // Domain part length check
123        if domain_part.len() > 255 {
124            return false;
125        }
126
127        // Use regex for detailed format validation
128        match self.get_pattern() {
129            Ok(regex) => regex.is_match(email),
130            Err(_) => false, // If regex compilation fails, consider invalid
131        }
132    }
133}
134
135impl Default for EmailValidator {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141#[async_trait]
142impl ValidationRule for EmailValidator {
143    async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()> {
144        // Skip validation for null values
145        if value.is_null() {
146            return Ok(());
147        }
148
149        let email = match value.as_str() {
150            Some(email) => email,
151            None => {
152                return Err(ValidationError::with_code(
153                    field,
154                    format!("{} must be a string for email validation", field),
155                    "invalid_type",
156                ).into());
157            }
158        };
159
160        if !self.validate_email_format(email) {
161            let message = self
162                .message.clone()
163                .unwrap_or_else(|| format!("{} must be a valid email address", field));
164
165            return Err(ValidationError::with_code(field, message, "invalid_email").into());
166        }
167
168        Ok(())
169    }
170
171    fn rule_name(&self) -> &'static str {
172        "email"
173    }
174
175    fn parameters(&self) -> Option<Value> {
176        let mut params = serde_json::Map::new();
177        
178        if let Some(ref message) = self.message {
179            params.insert("message".to_string(), Value::String(message.clone()));
180        }
181        params.insert("allow_unicode".to_string(), Value::Bool(self.allow_unicode));
182        params.insert("require_tld".to_string(), Value::Bool(self.require_tld));
183
184        if !params.is_empty() {
185            Some(Value::Object(params))
186        } else {
187            None
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[tokio::test]
197    async fn test_email_validator_valid_emails() {
198        let validator = EmailValidator::new();
199        
200        let valid_emails = vec![
201            "test@example.com",
202            "user.name@domain.co.uk",
203            "first+last@subdomain.example.org",
204            "user123@test-domain.com",
205            "a@b.co",
206        ];
207
208        for email in valid_emails {
209            let result = validator.validate(&Value::String(email.to_string()), "email").await;
210            assert!(result.is_ok(), "Email '{}' should be valid", email);
211        }
212    }
213
214    #[tokio::test]
215    async fn test_email_validator_invalid_emails() {
216        let validator = EmailValidator::new();
217        
218        let toolong_email = format!("toolong{}@domain.com", "a".repeat(60));
219        let invalid_emails = vec![
220            "",              // Empty
221            "plainaddress",  // No @
222            "@missingdomain.com", // No local part
223            "missing@.com",  // No domain name
224            "double@@domain.com", // Double @
225            "spaces @domain.com", // Spaces
226            &toolong_email, // Local part too long
227            "test@",         // No domain
228            "test@domain",   // No TLD (when required)
229        ];
230
231        for email in invalid_emails {
232            let result = validator.validate(&Value::String(email.to_string()), "email").await;
233            assert!(result.is_err(), "Email '{}' should be invalid", email);
234        }
235    }
236
237    #[tokio::test]
238    async fn test_email_validator_without_tld_requirement() {
239        let validator = EmailValidator::new().require_tld(false);
240        
241        // Should be valid without TLD requirement
242        let result = validator.validate(&Value::String("test@localhost".to_string()), "email").await;
243        assert!(result.is_ok());
244        
245        let result = validator.validate(&Value::String("admin@intranet".to_string()), "email").await;
246        assert!(result.is_ok());
247    }
248
249    #[tokio::test]
250    async fn test_email_validator_unicode_domain() {
251        let validator = EmailValidator::new().allow_unicode(true);
252        
253        // Should handle unicode domains when allowed
254        let result = validator.validate(&Value::String("test@тест.рф".to_string()), "email").await;
255        assert!(result.is_ok());
256    }
257
258    #[tokio::test]
259    async fn test_email_validator_custom_pattern() {
260        let custom_regex = Regex::new(r"^[a-z]+@company\.com$").unwrap();
261        let validator = EmailValidator::new().custom_pattern(custom_regex);
262        
263        // Should match custom pattern
264        let result = validator.validate(&Value::String("john@company.com".to_string()), "email").await;
265        assert!(result.is_ok());
266        
267        // Should not match custom pattern
268        let result = validator.validate(&Value::String("john@otherdomain.com".to_string()), "email").await;
269        assert!(result.is_err());
270        
271        let result = validator.validate(&Value::String("John@company.com".to_string()), "email").await;
272        assert!(result.is_err()); // Uppercase not allowed in custom pattern
273    }
274
275    #[tokio::test]
276    async fn test_email_validator_custom_message() {
277        let validator = EmailValidator::new().message("Please enter a valid email address");
278        
279        let result = validator.validate(&Value::String("invalid-email".to_string()), "email").await;
280        assert!(result.is_err());
281        
282        let errors = result.unwrap_err();
283        let field_errors = errors.get_field_errors("email").unwrap();
284        assert_eq!(field_errors[0].message, "Please enter a valid email address");
285    }
286
287    #[tokio::test]
288    async fn test_email_validator_with_null() {
289        let validator = EmailValidator::new();
290        
291        // Null values should be skipped
292        let result = validator.validate(&Value::Null, "email").await;
293        assert!(result.is_ok());
294    }
295
296    #[tokio::test]
297    async fn test_email_validator_invalid_type() {
298        let validator = EmailValidator::new();
299        
300        // Numbers should fail type validation
301        let result = validator.validate(&Value::Number(serde_json::Number::from(42)), "email").await;
302        assert!(result.is_err());
303        
304        let errors = result.unwrap_err();
305        let field_errors = errors.get_field_errors("email").unwrap();
306        assert_eq!(field_errors[0].code, "invalid_type");
307    }
308
309    #[tokio::test]
310    async fn test_email_validator_edge_cases() {
311        let validator = EmailValidator::new();
312        
313        // Test some edge cases  
314        let edge_cases = vec![
315            ("aa@bb.cc", true),     // Minimal valid email
316            ("test@test@test.com", false), // Multiple @ symbols
317            ("test@domain.com", true), // Valid email
318        ];
319
320        for (email, should_be_valid) in edge_cases {
321            let result = validator.validate(&Value::String(email.to_string()), "email").await;
322            if should_be_valid {
323                assert!(result.is_ok(), "Email '{}' should be valid", email);
324            } else {
325                assert!(result.is_err(), "Email '{}' should be invalid", email);
326            }
327        }
328    }
329}