Skip to main content

relay_core/identity/
mod.rs

1mod address;
2mod agent;
3mod inbox;
4mod user;
5
6pub use address::Address;
7pub use agent::AgentId;
8pub use inbox::InboxId;
9pub use user::UserId;
10
11#[derive(Debug, PartialEq, Eq, thiserror::Error)]
12pub enum IdentityError {
13    #[error("invalid agent id")]
14    InvalidAgent,
15    #[error("invalid user id")]
16    InvalidUser,
17    #[error("invalid inbox")]
18    InvalidInbox,
19    #[error("invalid address")]
20    InvalidAddress,
21    #[error("invalid identity string")]
22    InvalidIdentityString,
23    #[error("invalid fqdn string")]
24    InvalidFqdnString,
25}
26
27/// Allowed special characters in identity strings.
28/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity
29pub const ALLOWED_SPECIAL_CHARS: &str = ".!$%&'*+-/=?^_`{}~";
30
31/// Check if the string passes the validation for identities.
32/// Only allows alphanumeric characters and a set of special characters.
33/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity
34pub fn is_valid_identity_string(input: &str) -> bool {
35    input
36        .chars()
37        .all(|c| c.is_ascii_alphanumeric() || ALLOWED_SPECIAL_CHARS.contains(c))
38}
39
40/// Convert the identity string to its canonical form by lowercasing it.
41/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
42pub fn canonical_identity_string(input: &str) -> String {
43    input.to_lowercase()
44}
45
46/// Check if a DNS label is valid according to RFC 1035.
47pub fn is_valid_dns_label(label: &str) -> bool {
48    let len = label.len();
49    if len == 0 || len > 63 {
50        return false;
51    }
52
53    let bytes = label.as_bytes();
54
55    if !bytes[0].is_ascii_alphanumeric() || !bytes[len - 1].is_ascii_alphanumeric() {
56        return false;
57    }
58
59    bytes
60        .iter()
61        .all(|b| b.is_ascii_alphanumeric() || *b == b'-')
62}
63
64/// Check if a fully qualified domain name (FQDN) is valid according to RFC 1035.
65pub fn is_valid_fqdn(input: &str) -> bool {
66    if input.len() > 253 {
67        return false;
68    }
69
70    if input.ends_with('.') {
71        return false;
72    }
73
74    let labels: Vec<&str> = input.split('.').collect();
75    if labels.len() < 2 {
76        return false;
77    }
78
79    labels.iter().all(|label| is_valid_dns_label(label))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    mod identity_string {
87        use super::*;
88
89        // BEFORE CHANGING: Make sure that the allowed characters match the documentation!
90        // https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity
91        #[test]
92        fn correct_allowed_chars() {
93            let test_str = "abcXYZ0123456789.!$%&'*+-/=?^_`{}~";
94            assert!(is_valid_identity_string(test_str));
95        }
96
97        #[test]
98        fn rejects_common_invalid_chars() {
99            assert!(!is_valid_identity_string("#"));
100            assert!(!is_valid_identity_string("@"));
101            assert!(!is_valid_identity_string("|"));
102            assert!(!is_valid_identity_string(" "));
103        }
104
105        #[test]
106        fn canonicalizes_properly() {
107            let input = "AbC.!$%&'*+-/=?^_`{}~XyZ0123456789";
108            let expected = "abc.!$%&'*+-/=?^_`{}~xyz0123456789";
109            assert_eq!(canonical_identity_string(input), expected);
110        }
111    }
112
113    mod dns_label {
114        use super::*;
115
116        #[test]
117        fn valid_labels() {
118            assert!(is_valid_dns_label("example"));
119            assert!(is_valid_dns_label("ex-ample"));
120            assert!(is_valid_dns_label("e"));
121            assert!(is_valid_dns_label("a".repeat(63).as_str()));
122        }
123
124        #[test]
125        fn invalid_labels() {
126            assert!(!is_valid_dns_label(""));
127            assert!(!is_valid_dns_label("a".repeat(64).as_str()));
128            assert!(!is_valid_dns_label("-example"));
129            assert!(!is_valid_dns_label("example-"));
130            assert!(!is_valid_dns_label("ex_ample"));
131            assert!(!is_valid_dns_label("ex ample"));
132        }
133    }
134
135    mod fqdn {
136        use super::*;
137
138        #[test]
139        fn valid_fqdns() {
140            assert!(is_valid_fqdn("example.org"));
141            assert!(is_valid_fqdn("sub.example.org"));
142            assert!(is_valid_fqdn(
143                "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z"
144            ));
145
146            let labels = [
147                "a".repeat(63),
148                "b".repeat(63),
149                "c".repeat(63),
150                "d".repeat(61),
151            ];
152            let fqdn = labels.join(".");
153            assert_eq!(fqdn.len(), 253);
154            assert!(is_valid_fqdn(&fqdn));
155        }
156
157        #[test]
158        fn invalid_fqdns() {
159            assert!(!is_valid_fqdn(""));
160            assert!(!is_valid_fqdn("example."));
161            assert!(!is_valid_fqdn("ex ample.org"));
162            assert!(!is_valid_fqdn("example_org"));
163            assert!(!is_valid_fqdn(&format!("{}.org", "a".repeat(247))));
164            assert!(!is_valid_fqdn("a..b.org"));
165            assert!(!is_valid_fqdn("a.-b.org"));
166        }
167    }
168}