Skip to main content

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}