Skip to main content

relay_core/identity/
agent.rs

1use serde::{Deserialize, Serialize};
2
3use crate::prelude::Address;
4
5use super::{
6    IdentityError as Err, canonical_identity_string, is_valid_fqdn, is_valid_identity_string,
7};
8
9/// A unique identifier for an Agent within an agent's domain.
10/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#agent-identity
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct AgentId(String);
13
14impl From<Address> for AgentId {
15    fn from(address: Address) -> Self {
16        address.agent().clone()
17    }
18}
19
20impl TryFrom<&str> for AgentId {
21    type Error = Err;
22
23    fn try_from(value: &str) -> Result<Self, Err> {
24        AgentId::parse(value)
25    }
26}
27
28impl TryFrom<String> for AgentId {
29    type Error = Err;
30
31    fn try_from(value: String) -> Result<Self, Err> {
32        AgentId::parse(value)
33    }
34}
35
36impl AgentId {
37    /// Parse an AgentId from a string according to the identity rules.
38    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#agent-identity
39    ///
40    /// # Example
41    /// ```
42    /// let agent_id = relay_core::id::AgentId::parse("US.east.example.org").unwrap();
43    /// assert_eq!(agent_id.canonical(), "us.east.example.org");
44    /// ```
45    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
46        let input = input.as_ref();
47
48        if input.is_empty() {
49            return Err(Err::InvalidAgent);
50        }
51        if !is_valid_identity_string(input) {
52            return Err(Err::InvalidIdentityString);
53        }
54        if !is_valid_fqdn(input) {
55            return Err(Err::InvalidFqdnString);
56        }
57
58        let canonical = canonical_identity_string(input);
59
60        Ok(Self(canonical))
61    }
62
63    /// Replace the current AgentId 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 AgentId. 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 AgentId {
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!(
89            AgentId::parse("US.example.org").unwrap().canonical(),
90            "us.example.org"
91        );
92    }
93
94    #[test]
95    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidAgent")]
96    fn rejects_empty() {
97        let _ = AgentId::parse("").unwrap();
98    }
99
100    #[test]
101    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
102    fn rejects_invalid_chars() {
103        let _ = AgentId::parse("Invalid Agent!").unwrap();
104    }
105
106    #[test]
107    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
108    fn reject_untrimmed() {
109        let _ = AgentId::parse("  trim_me  ").unwrap();
110    }
111
112    #[test]
113    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
114    fn rejects_non_ascii() {
115        let _ = AgentId::parse("调试输出").unwrap();
116    }
117
118    #[test]
119    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
120    fn rejects_homoglyphs() {
121        let _ = AgentId::parse("us.exaмple.org").unwrap(); // Cyrillic 'м'
122    }
123
124    #[test]
125    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidFqdnString")]
126    fn rejects_invalid_fqdn() {
127        let _ = AgentId::parse("us_east.example.org").unwrap();
128    }
129}