Skip to main content

relay_core/identity/
address.rs

1use serde::{Deserialize, Serialize};
2
3use super::{AgentId, IdentityError as Err, InboxId, UserId};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub struct Address {
7    pub inbox: Option<InboxId>,
8    pub user: UserId,
9    pub agent: AgentId,
10}
11
12impl TryFrom<&str> for Address {
13    type Error = Err;
14
15    fn try_from(value: &str) -> Result<Self, Err> {
16        Address::parse(value)
17    }
18}
19
20impl TryFrom<String> for Address {
21    type Error = Err;
22
23    fn try_from(value: String) -> Result<Self, Err> {
24        Address::parse(value)
25    }
26}
27
28impl Address {
29    /// Creates a new address.
30    pub fn new(inbox: Option<InboxId>, user: UserId, agent: AgentId) -> Self {
31        Address { inbox, user, agent }
32    }
33
34    /// Parse an address from a string.
35    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#address
36    /// General format: `[inbox]#user@agent`
37    ///
38    /// # Example
39    /// ```
40    /// use relay_core::id::{Address, AgentId, InboxId, UserId};
41    ///
42    /// let address = Address::parse("work#alice.smith@example.com").unwrap();
43    /// assert_eq!(address.inbox, Some(InboxId::parse("work").unwrap()));
44    /// assert_eq!(address.user, UserId::parse("alice.smith").unwrap());
45    /// assert_eq!(address.agent, AgentId::parse("example.com").unwrap());
46    /// ```
47    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
48        let input = input.as_ref();
49        if input.trim() != input {
50            return Err(Err::InvalidAddress);
51        }
52
53        let (left, agent_str) = input.rsplit_once('@').ok_or(Err::InvalidAddress)?;
54        let agent = AgentId::parse(agent_str)?;
55
56        let (inbox, user_str) = if let Some((inbox_str, user_str)) = left.rsplit_once('#') {
57            let inbox = if inbox_str.is_empty() {
58                None
59            } else {
60                Some(InboxId::parse(inbox_str)?)
61            };
62            (inbox, user_str)
63        } else {
64            (None, left)
65        };
66        let user = UserId::parse(user_str)?;
67
68        Ok(Address { inbox, user, agent })
69    }
70
71    /// Get the canonical string representation of the address.
72    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
73    pub fn canonical(&self) -> String {
74        match &self.inbox {
75            Some(inbox) => format!(
76                "{}#{}@{}",
77                inbox.canonical(),
78                self.user.canonical(),
79                self.agent.canonical()
80            ),
81            None => format!("#{}@{}", self.user.canonical(), self.agent.canonical()),
82        }
83    }
84
85    /// Get inbox id.
86    pub fn inbox(&self) -> Option<&InboxId> {
87        self.inbox.as_ref()
88    }
89
90    /// Get user id.
91    pub fn user(&self) -> &UserId {
92        &self.user
93    }
94
95    /// Get agent id.
96    pub fn agent(&self) -> &AgentId {
97        &self.agent
98    }
99}
100
101impl std::fmt::Display for Address {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        write!(f, "{}", self.canonical())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn parses_and_canonicalizes() {
113        assert_eq!(
114            Address::parse("#Bob@example.org").unwrap().canonical(),
115            "#bob@example.org"
116        );
117        assert_eq!(
118            Address::parse("Bob@example.org").unwrap().canonical(),
119            "#bob@example.org"
120        );
121        assert_eq!(
122            Address::parse("wORk#Bob@example.ORG").unwrap().canonical(),
123            "work#bob@example.org"
124        );
125    }
126
127    #[test]
128    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidAddress")]
129    fn rejects_empty() {
130        let _ = Address::parse("").unwrap();
131    }
132
133    #[test]
134    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidAddress")]
135    fn rejects_invalid_chars() {
136        let _ = Address::parse("Invalid Address!").unwrap();
137    }
138
139    #[test]
140    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidAddress")]
141    fn reject_untrimmed() {
142        let _ = Address::parse("  trim_me  ").unwrap();
143    }
144
145    #[test]
146    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidAddress")]
147    fn rejects_non_ascii() {
148        let _ = Address::parse("调试输出").unwrap();
149    }
150
151    #[test]
152    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
153    fn rejects_homoglyphs() {
154        let _ = Address::parse("wоrk#аlice@us.exaмple.org").unwrap(); // Cyrillic 'о', 'а', 'м'
155    }
156}