1use crate::error::{MailError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct MailAddress {
11 local_part: String,
12 domain: Domain,
13}
14
15impl MailAddress {
16 pub fn new(local_part: impl Into<String>, domain: Domain) -> Result<Self> {
18 let local_part = local_part.into();
19
20 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 pub fn local_part(&self) -> &str {
39 &self.local_part
40 }
41
42 pub fn domain(&self) -> &Domain {
44 &self.domain
45 }
46
47 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct Domain(String);
78
79impl Domain {
80 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub struct Username(String);
133
134impl Username {
135 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 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}