Skip to main content

relay_core/identity/
user.rs

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