Skip to main content

rusmes_proto/
address.rs

1//! Email address types
2
3use crate::error::{MailError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8/// Represents a valid email address
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct MailAddress {
11    local_part: String,
12    domain: Domain,
13}
14
15impl MailAddress {
16    /// Create a new email address with validation
17    pub fn new(local_part: impl Into<String>, domain: Domain) -> Result<Self> {
18        let local_part = local_part.into();
19
20        // Basic validation
21        if local_part.is_empty() || local_part.len() > 64 {
22            return Err(MailError::InvalidAddress(format!(
23                "Local part length must be 1-64 characters, got {}",
24                local_part.len()
25            )));
26        }
27
28        if local_part.contains('@') {
29            return Err(MailError::InvalidAddress(
30                "Local part cannot contain '@'".to_string(),
31            ));
32        }
33
34        Ok(Self { local_part, domain })
35    }
36
37    /// Get the local part (before @)
38    pub fn local_part(&self) -> &str {
39        &self.local_part
40    }
41
42    /// Get the domain part
43    pub fn domain(&self) -> &Domain {
44        &self.domain
45    }
46
47    /// Get the full email address as a string
48    pub fn as_string(&self) -> String {
49        format!("{}@{}", self.local_part, self.domain.as_str())
50    }
51}
52
53impl FromStr for MailAddress {
54    type Err = MailError;
55
56    fn from_str(s: &str) -> Result<Self> {
57        let parts: Vec<&str> = s.rsplitn(2, '@').collect();
58        if parts.len() != 2 {
59            return Err(MailError::InvalidAddress("Missing @ separator".to_string()));
60        }
61
62        let domain = Domain::new(parts[0])?;
63        let local_part = parts[1];
64
65        Self::new(local_part, domain)
66    }
67}
68
69impl fmt::Display for MailAddress {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}@{}", self.local_part, self.domain.as_str())
72    }
73}
74
75/// Represents a domain name
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct Domain(String);
78
79impl Domain {
80    /// Create a new domain with validation
81    pub fn new(domain: impl Into<String>) -> Result<Self> {
82        let domain = domain.into();
83
84        if domain.is_empty() || domain.len() > 255 {
85            return Err(MailError::InvalidDomain(format!(
86                "Domain length must be 1-255 characters, got {}",
87                domain.len()
88            )));
89        }
90
91        // Basic validation - check for valid characters
92        if !domain
93            .chars()
94            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
95        {
96            return Err(MailError::InvalidDomain(
97                "Domain contains invalid characters".to_string(),
98            ));
99        }
100
101        if domain.starts_with('.') || domain.ends_with('.') {
102            return Err(MailError::InvalidDomain(
103                "Domain cannot start or end with '.'".to_string(),
104            ));
105        }
106
107        Ok(Self(domain.to_lowercase()))
108    }
109
110    /// Get the domain as a string slice
111    pub fn as_str(&self) -> &str {
112        &self.0
113    }
114}
115
116impl FromStr for Domain {
117    type Err = MailError;
118
119    fn from_str(s: &str) -> Result<Self> {
120        Self::new(s)
121    }
122}
123
124impl fmt::Display for Domain {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "{}", self.0)
127    }
128}
129
130/// Represents a username for authentication
131#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub struct Username(String);
133
134impl Username {
135    /// Create a new username with validation
136    pub fn new(username: impl Into<String>) -> Result<Self> {
137        let username = username.into();
138
139        if username.is_empty() || username.len() > 128 {
140            return Err(MailError::InvalidUsername(format!(
141                "Username length must be 1-128 characters, got {}",
142                username.len()
143            )));
144        }
145
146        Ok(Self(username))
147    }
148
149    /// Get the username as a string slice
150    pub fn as_str(&self) -> &str {
151        &self.0
152    }
153}
154
155impl FromStr for Username {
156    type Err = MailError;
157
158    fn from_str(s: &str) -> Result<Self> {
159        Self::new(s)
160    }
161}
162
163impl fmt::Display for Username {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(f, "{}", self.0)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_valid_mail_address() {
175        let domain = Domain::new("example.com").unwrap();
176        let addr = MailAddress::new("user", domain).unwrap();
177        assert_eq!(addr.as_string(), "user@example.com");
178    }
179
180    #[test]
181    fn test_mail_address_from_str() {
182        let addr: MailAddress = "user@example.com".parse().unwrap();
183        assert_eq!(addr.local_part(), "user");
184        assert_eq!(addr.domain().as_str(), "example.com");
185    }
186
187    #[test]
188    fn test_invalid_address_no_at() {
189        let result: Result<MailAddress> = "userexample.com".parse();
190        assert!(result.is_err());
191    }
192
193    #[test]
194    fn test_domain_case_normalization() {
195        let domain1 = Domain::new("Example.COM").unwrap();
196        let domain2 = Domain::new("example.com").unwrap();
197        assert_eq!(domain1, domain2);
198    }
199}