Skip to main content

relay_core/identity/
address.rs

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