7
7
import dns .exception
8
8
import idna # implements IDNA 2008; Python's codec is only IDNA 2003
9
9
10
+ from email_validator .error_classes import *
10
11
11
12
# Based on RFC 2822 section 3.2.4 / RFC 5322 section 3.2.3, these
12
13
# characters are permitted in email addresses (not taking into
49
50
50
51
DEFAULT_TIMEOUT = 15 # secs
51
52
52
-
53
- class EmailNotValidError (ValueError ):
54
- """Parent class of all exceptions raised by this module."""
55
- pass
56
-
57
-
58
- class EmailSyntaxError (EmailNotValidError ):
59
- """Exception raised when an email address fails validation because of its form."""
60
- pass
61
-
62
-
63
- class EmailUndeliverableError (EmailNotValidError ):
64
- """Exception raised when an email address fails validation because its domain name does not appear deliverable."""
65
- pass
66
-
67
-
68
53
class ValidatedEmail (object ):
69
54
"""The validate_email function returns objects of this type holding the normalized form of the email address
70
55
and other information."""
@@ -174,10 +159,10 @@ def as_dict(self):
174
159
175
160
def __get_length_reason (addr , utf8 = False , limit = EMAIL_MAX_LENGTH ):
176
161
diff = len (addr ) - limit
177
- reason = "({}{} character{} too many)"
162
+ reason_string = "({}{} character{} too many)"
178
163
prefix = "at least " if utf8 else ""
179
164
suffix = "s" if diff > 1 else ""
180
- return reason .format (prefix , diff , suffix )
165
+ return ( reason_string .format (prefix , diff , suffix ), diff )
181
166
182
167
183
168
def caching_resolver (timeout = DEFAULT_TIMEOUT , cache = None ):
@@ -208,12 +193,14 @@ def validate_email(
208
193
try :
209
194
email = email .decode ("ascii" )
210
195
except ValueError :
211
- raise EmailSyntaxError ("The email address is not valid ASCII." )
196
+ raise EmailInvalidAsciiError ("The email address is not valid ASCII." )
212
197
213
198
# At-sign.
214
199
parts = email .split ('@' )
215
- if len (parts ) != 2 :
216
- raise EmailSyntaxError ("The email address is not valid. It must have exactly one @-sign." )
200
+ if len (parts ) < 2 :
201
+ raise EmailNoAtSignError ("The email address is not valid. It must have exactly one @-sign." )
202
+ if len (parts ) > 2 :
203
+ raise EmailMultipleAtSignsError ("The email address is not valid. It must have exactly one @-sign." )
217
204
218
205
# Collect return values in this instance.
219
206
ret = ValidatedEmail ()
@@ -261,22 +248,22 @@ def validate_email(
261
248
# See the length checks on the local part and the domain.
262
249
if ret .ascii_email and len (ret .ascii_email ) > EMAIL_MAX_LENGTH :
263
250
if ret .ascii_email == ret .email :
264
- reason = __get_length_reason (ret .ascii_email )
251
+ reason_tuple = __get_length_reason (ret .ascii_email )
265
252
elif len (ret .email ) > EMAIL_MAX_LENGTH :
266
253
# If there are more than 254 characters, then the ASCII
267
254
# form is definitely going to be too long.
268
- reason = __get_length_reason (ret .email , utf8 = True )
255
+ reason_tuple = __get_length_reason (ret .email , utf8 = True )
269
256
else :
270
- reason = "(when converted to IDNA ASCII)"
271
- raise EmailSyntaxError ("The email address is too long {}." .format (reason ) )
257
+ reason_tuple = "(when converted to IDNA ASCII)"
258
+ raise EmailTooLongAsciiError ("The email address is too long {}." .format (reason_tuple [ 0 ]), reason_tuple [ 1 ] )
272
259
if len (ret .email .encode ("utf8" )) > EMAIL_MAX_LENGTH :
273
260
if len (ret .email ) > EMAIL_MAX_LENGTH :
274
261
# If there are more than 254 characters, then the UTF-8
275
262
# encoding is definitely going to be too long.
276
- reason = __get_length_reason (ret .email , utf8 = True )
263
+ reason_tuple = __get_length_reason (ret .email , utf8 = True )
277
264
else :
278
- reason = "(when encoded in bytes)"
279
- raise EmailSyntaxError ("The email address is too long {}." .format (reason ) )
265
+ reason_tuple = "(when encoded in bytes)"
266
+ raise EmailTooLongUtf8Error ("The email address is too long {}." .format (reason_tuple [ 0 ]), reason_tuple [ 1 ] )
280
267
281
268
if check_deliverability :
282
269
# Validate the email address's deliverability and update the
@@ -296,7 +283,7 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
296
283
297
284
if len (local ) == 0 :
298
285
if not allow_empty_local :
299
- raise EmailSyntaxError ("There must be something before the @-sign." )
286
+ raise EmailDomainPartEmptyError ("There must be something before the @-sign." )
300
287
else :
301
288
# The caller allows an empty local part. Useful for validating certain
302
289
# Postfix aliases.
@@ -313,8 +300,8 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
313
300
# that may not be relevant. We will check the total address length
314
301
# instead.
315
302
if len (local ) > LOCAL_PART_MAX_LENGTH :
316
- reason = __get_length_reason (local , limit = LOCAL_PART_MAX_LENGTH )
317
- raise EmailSyntaxError ("The email address is too long before the @-sign {}." .format (reason ))
303
+ reason_tuple = __get_length_reason (local , limit = LOCAL_PART_MAX_LENGTH )
304
+ raise EmailLocalPartTooLongError ("The email address is too long before the @-sign {}." .format (reason_tuple [ 0 ] ))
318
305
319
306
# Check the local part against the regular expression for the older ASCII requirements.
320
307
m = re .match (DOT_ATOM_TEXT + "\\ Z" , local )
@@ -334,11 +321,11 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
334
321
bad_chars = ', ' .join (sorted (set (
335
322
c for c in local if not re .match (u"[" + (ATEXT if not allow_smtputf8 else ATEXT_UTF8 ) + u"]" , c )
336
323
)))
337
- raise EmailSyntaxError ("The email address contains invalid characters before the @-sign: %s." % bad_chars )
324
+ raise EmailLocalPartInvalidCharactersError ("The email address contains invalid characters before the @-sign: %s." % bad_chars )
338
325
339
326
# It would be valid if internationalized characters were allowed by the caller.
340
327
if not allow_smtputf8 :
341
- raise EmailSyntaxError ("Internationalized characters before the @-sign are not supported." )
328
+ raise EmailLocalPartInternationalizedCharactersError ("Internationalized characters before the @-sign are not supported." )
342
329
343
330
# It's valid.
344
331
@@ -357,7 +344,7 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
357
344
def validate_email_domain_part (domain ):
358
345
# Empty?
359
346
if len (domain ) == 0 :
360
- raise EmailSyntaxError ("There must be something after the @-sign." )
347
+ raise EmailDomainPartEmptyError ("There must be something after the @-sign." )
361
348
362
349
# Perform UTS-46 normalization, which includes casefolding, NFC normalization,
363
350
# and converting all label separators (the period/full stop, fullwidth full stop,
@@ -367,18 +354,18 @@ def validate_email_domain_part(domain):
367
354
try :
368
355
domain = idna .uts46_remap (domain , std3_rules = False , transitional = False )
369
356
except idna .IDNAError as e :
370
- raise EmailSyntaxError ("The domain name %s contains invalid characters (%s)." % (domain , str (e )))
357
+ raise EmailDomainInvalidIdnaError ("The domain name %s contains invalid characters (%s)." % (domain , str (e )), e )
371
358
372
359
# Now we can perform basic checks on the use of periods (since equivalent
373
360
# symbols have been mapped to periods). These checks are needed because the
374
361
# IDNA library doesn't handle well domains that have empty labels (i.e. initial
375
362
# dot, trailing dot, or two dots in a row).
376
363
if domain .endswith ("." ):
377
- raise EmailSyntaxError ("An email address cannot end with a period." )
364
+ raise EmailDomainEndsWithPeriodError ("An email address cannot end with a period." )
378
365
if domain .startswith ("." ):
379
- raise EmailSyntaxError ("An email address cannot have a period immediately after the @-sign." )
366
+ raise EmailDomainStartsWithPeriodError ("An email address cannot have a period immediately after the @-sign." )
380
367
if ".." in domain :
381
- raise EmailSyntaxError ("An email address cannot have two periods in a row." )
368
+ raise EmailDomainMultiplePeriodsInARowError ("An email address cannot have two periods in a row." )
382
369
383
370
# Regardless of whether international characters are actually used,
384
371
# first convert to IDNA ASCII. For ASCII-only domains, the transformation
@@ -398,8 +385,8 @@ def validate_email_domain_part(domain):
398
385
# the length check is applied to a string that is different from the
399
386
# one the user supplied. Also I'm not sure if the length check applies
400
387
# to the internationalized form, the IDNA ASCII form, or even both!
401
- raise EmailSyntaxError ("The email address is too long after the @-sign." )
402
- raise EmailSyntaxError ("The domain name %s contains invalid characters (%s)." % (domain , str (e )))
388
+ raise EmailDomainTooLongError ("The email address is too long after the @-sign." )
389
+ raise EmailDomainInvalidIdnaError ("The domain name %s contains invalid characters (%s)." % (domain , str (e )), e )
403
390
404
391
# We may have been given an IDNA ASCII domain to begin with. Check
405
392
# that the domain actually conforms to IDNA. It could look like IDNA
@@ -411,7 +398,7 @@ def validate_email_domain_part(domain):
411
398
try :
412
399
domain_i18n = idna .decode (ascii_domain .encode ('ascii' ))
413
400
except idna .IDNAError as e :
414
- raise EmailSyntaxError ("The domain name %s is not valid IDNA (%s)." % (ascii_domain , str (e )))
401
+ raise EmailDomainInvalidIdnaError ("The domain name %s is not valid IDNA (%s)." % (ascii_domain , str (e )), e )
415
402
416
403
# RFC 5321 4.5.3.1.2
417
404
# We're checking the number of bytes (octets) here, which can be much
@@ -420,7 +407,7 @@ def validate_email_domain_part(domain):
420
407
# as IDNA ASCII. This is also checked by idna.encode, so this exception
421
408
# is never reached.
422
409
if len (ascii_domain ) > DOMAIN_MAX_LENGTH :
423
- raise EmailSyntaxError ("The email address is too long after the @-sign." )
410
+ raise EmailDomainTooLongError ("The email address is too long after the @-sign." )
424
411
425
412
# A "dot atom text", per RFC 2822 3.2.4, but using the restricted
426
413
# characters allowed in a hostname (see ATEXT_HOSTNAME above).
@@ -430,14 +417,14 @@ def validate_email_domain_part(domain):
430
417
# with idna.decode, which also checks this format.
431
418
m = re .match (DOT_ATOM_TEXT + "\\ Z" , ascii_domain )
432
419
if not m :
433
- raise EmailSyntaxError ("The email address contains invalid characters after the @-sign." )
420
+ raise EmailDomainInvalidCharactersError ("The email address contains invalid characters after the @-sign." )
434
421
435
422
# All publicly deliverable addresses have domain named with at least
436
423
# one period. We also know that all TLDs end with a letter.
437
424
if "." not in ascii_domain :
438
- raise EmailSyntaxError ("The domain name %s is not valid. It should have a period." % domain_i18n )
425
+ raise EmailDomainNoPeriodError ("The domain name %s is not valid. It should have a period." % domain_i18n )
439
426
if not re .search (r"[A-Za-z]\Z" , ascii_domain ):
440
- raise EmailSyntaxError (
427
+ raise EmailDomainNoValidTldError (
441
428
"The domain name %s is not valid. It is not within a valid top-level domain." % domain_i18n
442
429
)
443
430
@@ -509,7 +496,7 @@ def dns_resolver_resolve_shim(domain, record):
509
496
510
497
# If there was no MX, A, or AAAA record, then mail to
511
498
# this domain is not deliverable.
512
- raise EmailUndeliverableError ("The domain name %s does not exist." % domain_i18n )
499
+ raise EmailDomainNameDoesNotExistError ("The domain name %s does not exist." % domain_i18n )
513
500
514
501
except dns .exception .Timeout :
515
502
# A timeout could occur for various reasons, so don't treat it as a failure.
@@ -523,8 +510,8 @@ def dns_resolver_resolve_shim(domain, record):
523
510
524
511
except Exception as e :
525
512
# Unhandled conditions should not propagate.
526
- raise EmailUndeliverableError (
527
- "There was an error while checking if the domain name in the email address is deliverable: " + str (e )
513
+ raise EmailDomainUnhandledDnsExceptionError (
514
+ "There was an error while checking if the domain name in the email address is deliverable: " + str (e ), e
528
515
)
529
516
530
517
return {
0 commit comments