email_addr.nx source
↩ module page · 240 lines · 7782 B
1// email_addr.nx -- RFC 5322 addr-spec validator (practical subset).
2//
3// The full RFC 5322 grammar accepts obscure forms like quoted
4// local parts (\"foo bar\"@example.com), comments ((comment)),
5// and folded whitespace -- practically never seen in real email
6// addresses. We implement the subset that matches what users
7// actually type:
8//
9// local@domain
10// local = 1+ chars from [A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]
11// with '.' not first, not last, not consecutive
12// domain = at least one DNS label separated by '.'
13// label = [A-Za-z0-9] with optional [A-Za-z0-9-] interior,
14// trailing alnum, total length 1..63
15//
16// Returns (local_off, local_len, domain_off, domain_len) on
17// success so caller can upper-case compare, hash, etc. Zero-
18// alloc.
19//
20// Use cases: signup form validation, mailing-list import
21// sanity check, abuse detection, SPF lookup prep.
22//
23// Invariants:
24// EA1 Total length cap 320 bytes (RFC 5321 ยง4.5.3.1.3).
25// EA2 Local part cap 64 bytes.
26// EA3 Domain cap 255 bytes; per-label cap 63.
27// EA4 Returns EA_ERR_* negative on violation; offsets populated
28// even on failure up to the point of failure (useful for
29// error messaging).
30
31import "syscalls.nx"
32
33const EA_MAX_TOTAL: i64 = 320
34const EA_MAX_LOCAL: i64 = 64
35const EA_MAX_DOMAIN: i64 = 255
36const EA_MAX_LABEL: i64 = 63
37
38const EA_ERR_TOTAL_LEN: i64 = -1
39const EA_ERR_NO_AT: i64 = -2
40const EA_ERR_LOCAL_EMPTY: i64 = -3
41const EA_ERR_LOCAL_LEN: i64 = -4
42const EA_ERR_LOCAL_CHAR: i64 = -5
43const EA_ERR_LOCAL_DOT: i64 = -6
44const EA_ERR_DOMAIN_EMPTY: i64 = -7
45const EA_ERR_DOMAIN_LEN: i64 = -8
46const EA_ERR_LABEL_LEN: i64 = -9
47const EA_ERR_LABEL_CHAR: i64 = -10
48const EA_ERR_LABEL_DASH: i64 = -11
49
50struct EmailAddr {
51 local_off: i64, local_len: i64,
52 domain_off: i64, domain_len: i64,
53}
54
55// Is byte allowed in local part (not counting '.' handling)?
56func ea_is_local_char(b: i64) -> i64 {
57 // Letters.
58 if b >= 0x41 {
59 if b <= 0x5A { return 1 }
60 }
61 if b >= 0x61 {
62 if b <= 0x7A { return 1 }
63 }
64 // Digits.
65 if b >= 0x30 {
66 if b <= 0x39 { return 1 }
67 }
68 // Specials: ! # $ % & ' * + - / = ? ^ _ ` { | } ~
69 if b == 0x21 { return 1 } // !
70 if b == 0x23 { return 1 } // #
71 if b == 0x24 { return 1 } // $
72 if b == 0x25 { return 1 } // %
73 if b == 0x26 { return 1 } // &
74 if b == 0x27 { return 1 } // '
75 if b == 0x2A { return 1 } // *
76 if b == 0x2B { return 1 } // +
77 if b == 0x2D { return 1 } // -
78 if b == 0x2F { return 1 } // /
79 if b == 0x3D { return 1 } // =
80 if b == 0x3F { return 1 } // ?
81 if b == 0x5E { return 1 } // ^
82 if b == 0x5F { return 1 } // _
83 if b == 0x60 { return 1 } // `
84 if b == 0x7B { return 1 } // {
85 if b == 0x7C { return 1 } // |
86 if b == 0x7D { return 1 } // }
87 if b == 0x7E { return 1 } // ~
88 return 0
89}
90
91// Is byte alnum? Interior of a domain label.
92func ea_is_alnum(b: i64) -> i64 {
93 if b >= 0x30 {
94 if b <= 0x39 { return 1 }
95 }
96 if b >= 0x41 {
97 if b <= 0x5A { return 1 }
98 }
99 if b >= 0x61 {
100 if b <= 0x7A { return 1 }
101 }
102 return 0
103}
104
105// Forward decl.
106func if_eq_or(a: i64, b: i64, v1: i64, v2: i64) -> i64;
107
108// Validate an email address. Returns 0 on success + fills e;
109// negative on failure.
110func email_addr_validate(buf: *u8, n: i64, e: *EmailAddr) -> i64 {
111 if n > EA_MAX_TOTAL { return EA_ERR_TOTAL_LEN }
112 if n == 0 { return EA_ERR_LOCAL_EMPTY }
113
114 // Find the LAST '@' -- RFC permits '@' in quoted local parts,
115 // and the last unquoted '@' is the separator in addresses
116 // without quoting (our scope).
117 var at_pos: i64 = -1
118 var i: i64 = 0
119 while i < n {
120 if buf[i] == 0x40 { at_pos = i }
121 i = i + 1
122 }
123 if at_pos < 0 { return EA_ERR_NO_AT }
124
125 e.local_off = 0
126 e.local_len = at_pos
127 e.domain_off = at_pos + 1
128 e.domain_len = n - at_pos - 1
129
130 if e.local_len == 0 { return EA_ERR_LOCAL_EMPTY }
131 if e.local_len > EA_MAX_LOCAL { return EA_ERR_LOCAL_LEN }
132 if e.domain_len == 0 { return EA_ERR_DOMAIN_EMPTY }
133 if e.domain_len > EA_MAX_DOMAIN { return EA_ERR_DOMAIN_LEN }
134
135 // Validate local part.
136 if buf[0] == 0x2E { return EA_ERR_LOCAL_DOT }
137 if buf[e.local_len - 1] == 0x2E { return EA_ERR_LOCAL_DOT }
138 i = 0
139 while i < e.local_len {
140 let b: i64 = buf[i]
141 if b == 0x2E {
142 // No consecutive dots.
143 if i + 1 < e.local_len {
144 if buf[i + 1] == 0x2E { return EA_ERR_LOCAL_DOT }
145 }
146 } else {
147 if ea_is_local_char(b) == 0 { return EA_ERR_LOCAL_CHAR }
148 }
149 i = i + 1
150 }
151
152 // Validate domain: sequence of labels separated by '.'.
153 let dom_off: i64 = e.domain_off
154 let dom_end: i64 = e.domain_off + e.domain_len
155 var label_start: i64 = dom_off
156 i = dom_off
157 while i <= dom_end {
158 var is_boundary: i64 = 0
159 if i == dom_end { is_boundary = 1 }
160 if is_boundary == 0 {
161 if buf[i] == 0x2E { is_boundary = 1 }
162 }
163 if is_boundary == 1 {
164 let lbl_len: i64 = i - label_start
165 if lbl_len == 0 { return EA_ERR_LABEL_LEN }
166 if lbl_len > EA_MAX_LABEL { return EA_ERR_LABEL_LEN }
167 // First and last must be alnum (no leading/trailing '-').
168 if ea_is_alnum(buf[label_start]) == 0 {
169 return EA_ERR_LABEL_DASH
170 }
171 if ea_is_alnum(buf[i - 1]) == 0 {
172 return EA_ERR_LABEL_DASH
173 }
174 // Interior chars: alnum or '-'.
175 var k: i64 = label_start + 1
176 while k < i - 1 {
177 let c: i64 = buf[k]
178 if ea_is_alnum(c) == 1 {
179 k = k + 1
180 continue
181 }
182 if c == 0x2D {
183 k = k + 1
184 continue
185 }
186 return EA_ERR_LABEL_CHAR
187 }
188 label_start = i + 1
189 }
190 i = i + 1
191 }
192
193 return 0
194}
195
196// Helper used above -- NishiLang needs explicit forward decl.
197func if_eq_or(a: i64, b: i64, v1: i64, v2: i64) -> i64 {
198 if a == b { return v1 }
199 return v2
200}
201
202// Compile-only smoke.
203func main() -> i64 {
204 let e_raw: *u8 = sys_mmap(64)
205 let e: *EmailAddr = e_raw as *EmailAddr
206
207 // Happy path.
208 if email_addr_validate("user@example.com", 16, e) != 0 { return 1 }
209 if e.local_len != 4 { return 2 }
210 if e.domain_len != 11 { return 3 }
211
212 // Plus-addressing.
213 if email_addr_validate("first.last+tag@sub.example.com", 30, e) != 0 {
214 return 4
215 }
216
217 // Rejected cases.
218 if email_addr_validate("", 0, e) != EA_ERR_LOCAL_EMPTY { return 5 }
219 if email_addr_validate("no-at-sign", 10, e) != EA_ERR_NO_AT { return 6 }
220 if email_addr_validate("@example.com", 12, e) != EA_ERR_LOCAL_EMPTY {
221 return 7
222 }
223 if email_addr_validate("user@", 5, e) != EA_ERR_DOMAIN_EMPTY { return 8 }
224 if email_addr_validate(".leading@example.com", 20, e) != EA_ERR_LOCAL_DOT {
225 return 9
226 }
227 if email_addr_validate("trailing.@example.com", 21, e) != EA_ERR_LOCAL_DOT {
228 return 10
229 }
230 if email_addr_validate("two..dots@example.com", 21, e) != EA_ERR_LOCAL_DOT {
231 return 11
232 }
233 if email_addr_validate("user@-bad.com", 13, e) != EA_ERR_LABEL_DASH {
234 return 12
235 }
236 if email_addr_validate("user@bad-.com", 13, e) != EA_ERR_LABEL_DASH {
237 return 13
238 }
239 return 0
240}