Skip to main content

relay_core/identity/
inbox.rs

1use serde::{Deserialize, Serialize};
2
3use crate::prelude::Address;
4
5use super::{IdentityError as Err, canonical_identity_string, is_valid_identity_string};
6
7/// A unique identifier for an Inbox.
8/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#inbox
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct InboxId(String);
11
12impl TryFrom<Address> for InboxId {
13    type Error = Err;
14
15    fn try_from(address: Address) -> Result<Self, Err> {
16        match address.inbox {
17            Some(inbox) => Ok(inbox),
18            None => Err(Err::InvalidInbox),
19        }
20    }
21}
22
23impl TryFrom<&str> for InboxId {
24    type Error = Err;
25
26    fn try_from(value: &str) -> Result<Self, Err> {
27        InboxId::parse(value)
28    }
29}
30
31impl TryFrom<String> for InboxId {
32    type Error = Err;
33
34    fn try_from(value: String) -> Result<Self, Err> {
35        InboxId::parse(value)
36    }
37}
38
39impl InboxId {
40    /// Parse an InboxId from a string according to the identity rules.
41    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#inbox
42    ///
43    /// # Example
44    /// ```
45    /// let inbox_id = relay_core::id::InboxId::parse("Work").unwrap();
46    /// assert_eq!(inbox_id.canonical(), "work");
47    /// ```
48    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
49        let input = input.as_ref();
50
51        if input.is_empty() {
52            return Err(Err::InvalidInbox);
53        }
54        if !is_valid_identity_string(input) {
55            return Err(Err::InvalidIdentityString);
56        }
57
58        let canonical = canonical_identity_string(input);
59
60        Ok(Self(canonical))
61    }
62
63    /// Replace the current InboxId with a new value.
64    pub fn replace(&mut self, new_value: impl AsRef<str>) -> Result<(), Err> {
65        *self = Self::parse(new_value)?;
66        Ok(())
67    }
68
69    /// The canonical form of the InboxId. The value is already stored as such.
70    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
71    pub fn canonical(&self) -> &str {
72        &self.0
73    }
74}
75
76impl std::fmt::Display for InboxId {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.canonical())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn parses_and_canonicalizes() {
88        assert_eq!(InboxId::parse("Work").unwrap().canonical(), "work");
89    }
90
91    #[test]
92    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidInbox")]
93    fn rejects_empty() {
94        let _ = InboxId::parse("").unwrap();
95    }
96
97    #[test]
98    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
99    fn rejects_invalid_chars() {
100        let _ = InboxId::parse("Invalid Inbox!").unwrap();
101    }
102
103    #[test]
104    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
105    fn reject_untrimmed() {
106        let _ = InboxId::parse("  trim_me  ").unwrap();
107    }
108
109    #[test]
110    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
111    fn rejects_non_ascii() {
112        let _ = InboxId::parse("调试输出").unwrap();
113    }
114
115    #[test]
116    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
117    fn rejects_homoglyphs() {
118        let _ = InboxId::parse("wоrk").unwrap(); // Cyrillic 'о'
119    }
120}