Validates the syntax of the local part of an email address.
(local: str, allow_smtputf8: bool = True, allow_empty_local: bool = False,
quoted_local_part: bool = False, strict: bool = False)
| 229 | |
| 230 | |
| 231 | def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_empty_local: bool = False, |
| 232 | quoted_local_part: bool = False, strict: bool = False) -> LocalPartValidationResult: |
| 233 | """Validates the syntax of the local part of an email address.""" |
| 234 | |
| 235 | if len(local) == 0: |
| 236 | if not allow_empty_local: |
| 237 | raise EmailSyntaxError("There must be something before the @-sign.") |
| 238 | |
| 239 | # The caller allows an empty local part. Useful for validating certain |
| 240 | # Postfix aliases. |
| 241 | return { |
| 242 | "local_part": local, |
| 243 | "ascii_local_part": local, |
| 244 | "smtputf8": False, |
| 245 | } |
| 246 | |
| 247 | # Check the length of the local part by counting characters. |
| 248 | # (RFC 5321 4.5.3.1.1) |
| 249 | # We're checking the number of characters here. If the local part |
| 250 | # is ASCII-only, then that's the same as bytes (octets). If it's |
| 251 | # internationalized, then the UTF-8 encoding may be longer, but |
| 252 | # that may not be relevant. We will check the total address length |
| 253 | # instead. |
| 254 | if strict and len(local) > LOCAL_PART_MAX_LENGTH: |
| 255 | reason = get_length_reason(local, limit=LOCAL_PART_MAX_LENGTH) |
| 256 | raise EmailSyntaxError(f"The email address is too long before the @-sign {reason}.") |
| 257 | |
| 258 | # Check the local part against the non-internationalized regular expression. |
| 259 | # Most email addresses match this regex so it's probably fastest to check this first. |
| 260 | # (RFC 5322 3.2.3) |
| 261 | # All local parts matching the dot-atom rule are also valid as a quoted string |
| 262 | # so if it was originally quoted (quoted_local_part is True) and this regex matches, |
| 263 | # it's ok. |
| 264 | # (RFC 5321 4.1.2 / RFC 5322 3.2.4). |
| 265 | if DOT_ATOM_TEXT.match(local): |
| 266 | # It's valid. And since it's just the permitted ASCII characters, |
| 267 | # it's normalized and safe. If the local part was originally quoted, |
| 268 | # the quoting was unnecessary and it'll be returned as normalized to |
| 269 | # non-quoted form. |
| 270 | |
| 271 | # Return the local part and flag that SMTPUTF8 is not needed. |
| 272 | return { |
| 273 | "local_part": local, |
| 274 | "ascii_local_part": local, |
| 275 | "smtputf8": False, |
| 276 | } |
| 277 | |
| 278 | # The local part failed the basic dot-atom check. Try the extended character set |
| 279 | # for internationalized addresses. It's the same pattern but with additional |
| 280 | # characters permitted. |
| 281 | # RFC 6531 section 3.3. |
| 282 | valid: Optional[str] = None |
| 283 | requires_smtputf8 = False |
| 284 | if DOT_ATOM_TEXT_INTL.match(local): |
| 285 | # But international characters in the local part may not be permitted. |
| 286 | if not allow_smtputf8: |
| 287 | # Check for invalid characters against the non-internationalized |
| 288 | # permitted character set. |
no test coverage detected