mailbourne_core/address.rs
1//! # address — who mail is for
2//!
3//! An email address is two names joined by `@`: the **local part** (which
4//! mailbox) and the **domain** (which server's world that mailbox lives in).
5//! Everything in routing cares only about the domain; everything in delivery
6//! cares only about the local part.
7
8use std::fmt;
9
10/// A parsed email address: `local@domain`.
11///
12/// # Example
13/// ```
14/// use mailbourne_core::EmailAddress;
15///
16/// let addr = EmailAddress::parse("alice@example.com").unwrap();
17/// assert_eq!(addr.local(), "alice");
18/// assert_eq!(addr.domain(), "example.com");
19/// ```
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct EmailAddress {
22 local: String,
23 domain: String,
24}
25
26/// Why an address string could not be parsed.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum AddressError {
29 /// No `@` found, or more than one outside a quoted local part.
30 MissingAt,
31 /// The part before `@` is empty.
32 EmptyLocal,
33 /// The part after `@` is empty.
34 EmptyDomain,
35}
36
37impl EmailAddress {
38 /// Parses `local@domain` from a string.
39 ///
40 /// This is deliberately strict-and-simple for now: one `@`, non-empty
41 /// halves. Full RFC 5321 address grammar (quoted locals, literals)
42 /// arrives with the parser integration.
43 ///
44 /// # Errors
45 /// Returns an [`AddressError`] describing which structural rule failed.
46 pub fn parse(s: &str) -> Result<Self, AddressError> {
47 let (local, domain) = s.rsplit_once('@').ok_or(AddressError::MissingAt)?;
48 if local.is_empty() {
49 return Err(AddressError::EmptyLocal);
50 }
51 if domain.is_empty() {
52 return Err(AddressError::EmptyDomain);
53 }
54 Ok(Self {
55 local: local.to_string(),
56 domain: domain.to_string(),
57 })
58 }
59
60 /// The mailbox name — the part before `@`.
61 pub fn local(&self) -> &str {
62 &self.local
63 }
64
65 /// The domain — the part after `@`, and the only part routing looks at.
66 pub fn domain(&self) -> &str {
67 &self.domain
68 }
69}
70
71impl fmt::Display for EmailAddress {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}@{}", self.local, self.domain)
74 }
75}