1 /// Check if input is a valid domain label
is_label(input: &str) -> bool2 pub fn is_label(input: &str) -> bool {
3 let mut chars = input.chars();
4
5 // we need at least one char
6 let first = match chars.next() {
7 None => {
8 return false;
9 }
10 Some(c) => c,
11 };
12
13 // it can start with an alphanumeric character
14 if !first.is_ascii_alphanumeric() {
15 return false;
16 }
17
18 // then optionally be followed by any combination of
19 // alphanumeric characters and dashes
20 let last_index = input.len() - 2.min(input.len());
21 for (index, c) in chars.enumerate() {
22 // before finally ending with an alphanumeric character
23 if !c.is_ascii_alphanumeric() && (index == last_index || c != '-') {
24 return false;
25 }
26 }
27
28 true
29 }
30
31 /// Check the local part of an email address (before @)
is_email_local(input: &str) -> bool32 pub fn is_email_local(input: &str) -> bool {
33 let mut chars = input.chars();
34
35 // we need at least one char
36 let first = match chars.next() {
37 None => {
38 return false;
39 }
40 Some(c) => c,
41 };
42
43 let last_index = input.len() - 2.min(input.len());
44 if first == ' ' {
45 return false;
46 } else if first == '"' {
47 // quoted
48 if input.len() == 1 {
49 return false;
50 }
51 for (index, c) in chars.enumerate() {
52 if index == last_index {
53 if c != '"' {
54 return false;
55 }
56 } else if !is_combined(c) && !is_quoted(c) {
57 return false;
58 }
59 }
60 } else {
61 // not quoted
62 if first == '.' {
63 return false;
64 }
65 for (index, c) in chars.enumerate() {
66 if !is_combined(c) && (index == last_index || c != '.') {
67 return false;
68 }
69 }
70 }
71
72 true
73 }
74
75 // these characters can be anywhere in the expresion
76 // [[:alnum:]!#$%&'*+/=?^_`{|}~-]
is_global(c: char) -> bool77 fn is_global(c: char) -> bool {
78 c.is_ascii_alphanumeric()
79 || c == '-'
80 || c == '!'
81 || c == '#'
82 || c == '$'
83 || c == '%'
84 || c == '&'
85 || c == '\''
86 || c == '*'
87 || c == '+'
88 || c == '/'
89 || c == '='
90 || c == '?'
91 || c == '^'
92 || c == '_'
93 || c == '`'
94 || c == '{'
95 || c == '|'
96 || c == '}'
97 || c == '~'
98 }
is_non_ascii(c: char) -> bool99 fn is_non_ascii(c: char) -> bool {
100 c as u32 > 0x7f // non-ascii characters (can also be unquoted)
101 }
is_quoted(c: char) -> bool102 fn is_quoted(c: char) -> bool {
103 // ["(),\\:;<>@\[\]. ]
104 c == '"'
105 || c == '.'
106 || c == ' '
107 || c == '('
108 || c == ')'
109 || c == ','
110 || c == '\\'
111 || c == ':'
112 || c == ';'
113 || c == '<'
114 || c == '>'
115 || c == '@'
116 || c == '['
117 || c == ']'
118 }
is_combined(c: char) -> bool119 fn is_combined(c: char) -> bool {
120 is_global(c) || is_non_ascii(c)
121 }
122
123 #[cfg(test)]
124 mod test {
125 use super::*;
126
127 #[test]
is_label_correct()128 fn is_label_correct() {
129 for l in &["a", "ab", "a-b", "a--b", "0Z"] {
130 assert!(is_label(l));
131 }
132 }
133
134 #[test]
is_label_incorrect()135 fn is_label_incorrect() {
136 for l in &["", "-", "a-", "-b", "$"] {
137 assert!(!is_label(l));
138 }
139 }
140
141 #[test]
is_email_local_correct()142 fn is_email_local_correct() {
143 for l in &[
144 "a",
145 "ab",
146 "a.b",
147 "a\u{0080}",
148 "$",
149 "\"\"\"",
150 "\"a b\"",
151 "\" \"",
152 "\"a<>@\"",
153 ] {
154 assert!(is_email_local(l));
155 }
156 }
157
158 #[test]
is_email_local_incorrect()159 fn is_email_local_incorrect() {
160 for l in &["", " a", "a ", "a.", ".b", "a\x7f", "\"", "\"a", "a\""] {
161 assert!(!is_email_local(l));
162 }
163 }
164 }
165