Skip to main content

relay_core/identity/
agent.rs

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