code wiki / (root) / email_addr.nx

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}