Skip to main content

reinhardt_core/validators/
email.rs

1//! Email validator
2
3use super::lazy_patterns::EMAIL_REGEX;
4use super::{ValidationError, ValidationResult, Validator};
5
6/// Email address validator
7pub struct EmailValidator {
8	message: Option<String>,
9}
10
11impl EmailValidator {
12	/// Creates a new EmailValidator with RFC 5322 compliant validation
13	///
14	/// This implementation follows RFC 5322 specifications with the following rules:
15	/// - Local part (before @):
16	///   - Can contain alphanumeric characters, dots, underscores, percent signs, plus and minus signs
17	///   - Cannot start or end with a dot
18	///   - Cannot have consecutive dots
19	///   - Maximum 64 characters
20	/// - Domain part (after @):
21	///   - Can contain alphanumeric characters, dots, and hyphens
22	///   - Cannot start or end with a dot or hyphen
23	///   - Must have at least one dot
24	///   - Each label must be 1-63 characters
25	///   - TLD must be at least 2 characters
26	///   - Maximum 255 characters
27	/// - Total length must not exceed 320 characters (64 + @ + 255)
28	///
29	/// # Examples
30	///
31	/// ```
32	/// use reinhardt_core::validators::EmailValidator;
33	///
34	/// let validator = EmailValidator::new();
35	/// ```
36	pub fn new() -> Self {
37		Self { message: None }
38	}
39
40	/// Sets a custom error message for validation failures.
41	///
42	/// # Examples
43	///
44	/// ```
45	/// use reinhardt_core::validators::{EmailValidator, Validator};
46	///
47	/// let validator = EmailValidator::new().with_message("Invalid email address");
48	/// let result = validator.validate("not-an-email");
49	/// assert!(result.is_err());
50	/// ```
51	pub fn with_message(mut self, message: impl Into<String>) -> Self {
52		self.message = Some(message.into());
53		self
54	}
55
56	/// Validates an email address with additional RFC 5322 length constraints
57	fn validate_with_length_check(&self, email: &str) -> bool {
58		// Check total length (max 320 characters: 64 for local + 1 for @ + 255 for domain)
59		if email.len() > 320 {
60			return false;
61		}
62
63		// Split email into local and domain parts
64		let parts: Vec<&str> = email.split('@').collect();
65		if parts.len() != 2 {
66			return false;
67		}
68
69		let local_part = parts[0];
70		let domain_part = parts[1];
71
72		// Check local part length (max 64 characters)
73		if local_part.is_empty() || local_part.len() > 64 {
74			return false;
75		}
76
77		// Check domain part length (max 255 characters)
78		if domain_part.is_empty() || domain_part.len() > 255 {
79			return false;
80		}
81
82		// Check for consecutive dots in local part
83		if local_part.contains("..") {
84			return false;
85		}
86
87		// Check each domain label length (max 63 characters per label)
88		for label in domain_part.split('.') {
89			if label.is_empty() || label.len() > 63 {
90				return false;
91			}
92		}
93
94		// Finally, check against the regex pattern
95		EMAIL_REGEX.is_match(email)
96	}
97}
98
99impl Default for EmailValidator {
100	fn default() -> Self {
101		Self::new()
102	}
103}
104
105impl Validator<String> for EmailValidator {
106	fn validate(&self, value: &String) -> ValidationResult<()> {
107		if self.validate_with_length_check(value) {
108			Ok(())
109		} else if let Some(ref msg) = self.message {
110			Err(ValidationError::Custom(msg.clone()))
111		} else {
112			Err(ValidationError::InvalidEmail(value.clone()))
113		}
114	}
115}
116
117impl Validator<str> for EmailValidator {
118	fn validate(&self, value: &str) -> ValidationResult<()> {
119		if self.validate_with_length_check(value) {
120			Ok(())
121		} else if let Some(ref msg) = self.message {
122			Err(ValidationError::Custom(msg.clone()))
123		} else {
124			Err(ValidationError::InvalidEmail(value.to_string()))
125		}
126	}
127}
128
129#[cfg(test)]
130mod tests {
131	use super::*;
132
133	#[test]
134	fn test_valid_emails() {
135		let validator = EmailValidator::new();
136		let valid_emails = vec![
137			"test@example.com",
138			"user.name@example.com",
139			"user+tag@example.co.uk",
140			"user_name@example.com",
141			"user%test@example.com",
142			"user-name@sub.example.com",
143			"a@example.com",
144			"test@sub.sub.example.com",
145			"123@example.com",
146		];
147
148		for email in valid_emails {
149			assert!(
150				validator.validate(email).is_ok(),
151				"Expected {} to be valid",
152				email
153			);
154		}
155	}
156
157	#[test]
158	fn test_invalid_emails() {
159		let validator = EmailValidator::new();
160		let invalid_emails = vec![
161			"invalid-email",          // No @ symbol
162			"@example.com",           // No local part
163			"user@",                  // No domain
164			"user..name@example.com", // Consecutive dots
165			".user@example.com",      // Starts with dot
166			"user.@example.com",      // Ends with dot
167			"user@-example.com",      // Domain starts with hyphen
168			"user@example-.com",      // Domain label ends with hyphen
169			"user@example",           // No TLD
170			"user@example.c",         // TLD too short
171			"user name@example.com",  // Space in local part
172			"user@exam ple.com",      // Space in domain
173			"user@@example.com",      // Double @
174			"user@.example.com",      // Domain starts with dot
175			"user@example.com.",      // Domain ends with dot
176		];
177
178		for email in invalid_emails {
179			assert!(
180				validator.validate(email).is_err(),
181				"Expected {} to be invalid",
182				email
183			);
184		}
185	}
186
187	#[test]
188	fn test_length_constraints() {
189		let validator = EmailValidator::new();
190
191		// Local part too long (> 64 characters)
192		let long_local = format!("{}@example.com", "a".repeat(65));
193		assert!(validator.validate(&long_local).is_err());
194
195		// Domain too long (> 255 characters)
196		let long_domain = format!("user@{}.com", "a".repeat(252));
197		assert!(validator.validate(&long_domain).is_err());
198
199		// Total length too long (> 320 characters)
200		let very_long_email = format!("{}@{}.com", "a".repeat(64), "b".repeat(252));
201		assert!(validator.validate(&very_long_email).is_err());
202
203		// Domain label too long (> 63 characters)
204		let long_label = format!("user@{}.example.com", "a".repeat(64));
205		assert!(validator.validate(&long_label).is_err());
206
207		// Valid at maximum lengths
208		let max_local = format!("{}@example.com", "a".repeat(64));
209		assert!(validator.validate(&max_local).is_ok());
210	}
211
212	#[test]
213	fn test_case_insensitivity() {
214		let validator = EmailValidator::new();
215		assert!(validator.validate("Test@Example.COM").is_ok());
216		assert!(validator.validate("USER@EXAMPLE.COM").is_ok());
217	}
218
219	// Additional tests based on Django validators/tests.py - TestValidatorEquality::test_email_equality
220	#[test]
221	fn test_email_validator_with_numbers() {
222		let validator = EmailValidator::new();
223		assert!(validator.validate("123@example.com").is_ok());
224		assert!(validator.validate("user123@example.com").is_ok());
225		assert!(validator.validate("123user@example123.com").is_ok());
226	}
227
228	#[test]
229	fn test_email_validator_with_special_characters() {
230		let validator = EmailValidator::new();
231		// Valid special characters
232		assert!(validator.validate("user+tag@example.com").is_ok());
233		assert!(validator.validate("user_name@example.com").is_ok());
234		assert!(validator.validate("user-name@example.com").is_ok());
235		assert!(validator.validate("user.name@example.com").is_ok());
236		assert!(validator.validate("user%test@example.com").is_ok());
237	}
238
239	#[test]
240	fn test_email_validator_subdomains() {
241		let validator = EmailValidator::new();
242		assert!(validator.validate("user@mail.example.com").is_ok());
243		assert!(validator.validate("user@sub.mail.example.com").is_ok());
244		assert!(validator.validate("user@a.b.c.d.example.com").is_ok());
245	}
246
247	#[test]
248	fn test_email_validator_tld_variations() {
249		let validator = EmailValidator::new();
250		assert!(validator.validate("user@example.co").is_ok());
251		assert!(validator.validate("user@example.com").is_ok());
252		assert!(validator.validate("user@example.org").is_ok());
253		assert!(validator.validate("user@example.net").is_ok());
254		assert!(validator.validate("user@example.info").is_ok());
255		assert!(validator.validate("user@example.museum").is_ok());
256	}
257
258	#[test]
259	fn test_email_validator_edge_cases() {
260		let validator = EmailValidator::new();
261		// Single character local and domain parts
262		assert!(validator.validate("a@b.co").is_ok());
263
264		// Numbers in domain
265		assert!(validator.validate("user@123.com").is_ok());
266		assert!(validator.validate("user@example123.com").is_ok());
267	}
268
269	#[test]
270	fn test_email_validator_invalid_formats() {
271		let validator = EmailValidator::new();
272		// Multiple @ symbols
273		assert!(validator.validate("user@domain@example.com").is_err());
274
275		// Missing parts
276		assert!(validator.validate("@").is_err());
277		assert!(validator.validate("user@").is_err());
278		assert!(validator.validate("@domain.com").is_err());
279
280		// Invalid characters
281		assert!(validator.validate("user name@example.com").is_err());
282		assert!(validator.validate("user@exam ple.com").is_err());
283		assert!(validator.validate("user@example,com").is_err());
284	}
285
286	#[test]
287	fn test_email_validator_dot_rules() {
288		let validator = EmailValidator::new();
289		// Consecutive dots in local part
290		assert!(validator.validate("user..name@example.com").is_err());
291
292		// Starting with dot
293		assert!(validator.validate(".user@example.com").is_err());
294
295		// Ending with dot
296		assert!(validator.validate("user.@example.com").is_err());
297
298		// Valid dot usage
299		assert!(validator.validate("user.name.test@example.com").is_ok());
300	}
301
302	#[test]
303	fn test_email_validator_hyphen_rules() {
304		let validator = EmailValidator::new();
305		// Hyphens in domain are allowed in middle
306		assert!(validator.validate("user@my-domain.com").is_ok());
307		assert!(validator.validate("user@my-long-domain-name.com").is_ok());
308
309		// But not at start or end of domain labels
310		assert!(validator.validate("user@-invalid.com").is_err());
311		assert!(validator.validate("user@invalid-.com").is_err());
312		assert!(validator.validate("user@invalid.-com").is_err());
313		assert!(validator.validate("user@invalid.com-").is_err());
314	}
315
316	#[test]
317	fn test_email_validator_returns_correct_error() {
318		let validator = EmailValidator::new();
319		let invalid_email = "invalid";
320		match validator.validate(invalid_email) {
321			Err(ValidationError::InvalidEmail(email)) => {
322				assert_eq!(email, invalid_email);
323			}
324			_ => panic!("Expected InvalidEmail error"),
325		}
326	}
327
328	#[test]
329	fn test_email_validator_with_string_type() {
330		let validator = EmailValidator::new();
331		let email = String::from("test@example.com");
332		assert!(validator.validate(&email).is_ok());
333
334		let invalid = String::from("invalid");
335		assert!(validator.validate(&invalid).is_err());
336	}
337}