77import dns .exception
88import idna # implements IDNA 2008; Python's codec is only IDNA 2003
99
10+ from email_validator .error_classes import *
1011
1112# Based on RFC 2822 section 3.2.4 / RFC 5322 section 3.2.3, these
1213# characters are permitted in email addresses (not taking into
4950
5051DEFAULT_TIMEOUT = 15 # secs
5152
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-
6853class ValidatedEmail (object ):
6954 """The validate_email function returns objects of this type holding the normalized form of the email address
7055 and other information."""
@@ -174,10 +159,10 @@ def as_dict(self):
174159
175160def __get_length_reason (addr , utf8 = False , limit = EMAIL_MAX_LENGTH ):
176161 diff = len (addr ) - limit
177- reason = "({}{} character{} too many)"
162+ reason_string = "({}{} character{} too many)"
178163 prefix = "at least " if utf8 else ""
179164 suffix = "s" if diff > 1 else ""
180- return reason .format (prefix , diff , suffix )
165+ return ( reason_string .format (prefix , diff , suffix ), diff )
181166
182167
183168def caching_resolver (timeout = DEFAULT_TIMEOUT , cache = None ):
@@ -208,12 +193,14 @@ def validate_email(
208193 try :
209194 email = email .decode ("ascii" )
210195 except ValueError :
211- raise EmailSyntaxError ("The email address is not valid ASCII." )
196+ raise EmailInvalidAsciiError ("The email address is not valid ASCII." )
212197
213198 # At-sign.
214199 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." )
217204
218205 # Collect return values in this instance.
219206 ret = ValidatedEmail ()
@@ -261,22 +248,22 @@ def validate_email(
261248 # See the length checks on the local part and the domain.
262249 if ret .ascii_email and len (ret .ascii_email ) > EMAIL_MAX_LENGTH :
263250 if ret .ascii_email == ret .email :
264- reason = __get_length_reason (ret .ascii_email )
251+ reason_tuple = __get_length_reason (ret .ascii_email )
265252 elif len (ret .email ) > EMAIL_MAX_LENGTH :
266253 # If there are more than 254 characters, then the ASCII
267254 # 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 )
269256 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 ] )
272259 if len (ret .email .encode ("utf8" )) > EMAIL_MAX_LENGTH :
273260 if len (ret .email ) > EMAIL_MAX_LENGTH :
274261 # If there are more than 254 characters, then the UTF-8
275262 # 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 )
277264 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 ] )
280267
281268 if check_deliverability :
282269 # 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
296283
297284 if len (local ) == 0 :
298285 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." )
300287 else :
301288 # The caller allows an empty local part. Useful for validating certain
302289 # Postfix aliases.
@@ -313,8 +300,8 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
313300 # that may not be relevant. We will check the total address length
314301 # instead.
315302 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 ] ))
318305
319306 # Check the local part against the regular expression for the older ASCII requirements.
320307 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
334321 bad_chars = ', ' .join (sorted (set (
335322 c for c in local if not re .match (u"[" + (ATEXT if not allow_smtputf8 else ATEXT_UTF8 ) + u"]" , c )
336323 )))
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 )
338325
339326 # It would be valid if internationalized characters were allowed by the caller.
340327 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." )
342329
343330 # It's valid.
344331
@@ -357,7 +344,7 @@ def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=Fals
357344def validate_email_domain_part (domain ):
358345 # Empty?
359346 if len (domain ) == 0 :
360- raise EmailSyntaxError ("There must be something after the @-sign." )
347+ raise EmailDomainPartEmptyError ("There must be something after the @-sign." )
361348
362349 # Perform UTS-46 normalization, which includes casefolding, NFC normalization,
363350 # and converting all label separators (the period/full stop, fullwidth full stop,
@@ -367,18 +354,18 @@ def validate_email_domain_part(domain):
367354 try :
368355 domain = idna .uts46_remap (domain , std3_rules = False , transitional = False )
369356 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 )
371358
372359 # Now we can perform basic checks on the use of periods (since equivalent
373360 # symbols have been mapped to periods). These checks are needed because the
374361 # IDNA library doesn't handle well domains that have empty labels (i.e. initial
375362 # dot, trailing dot, or two dots in a row).
376363 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." )
378365 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." )
380367 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." )
382369
383370 # Regardless of whether international characters are actually used,
384371 # first convert to IDNA ASCII. For ASCII-only domains, the transformation
@@ -398,8 +385,8 @@ def validate_email_domain_part(domain):
398385 # the length check is applied to a string that is different from the
399386 # one the user supplied. Also I'm not sure if the length check applies
400387 # 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 )
403390
404391 # We may have been given an IDNA ASCII domain to begin with. Check
405392 # that the domain actually conforms to IDNA. It could look like IDNA
@@ -411,7 +398,7 @@ def validate_email_domain_part(domain):
411398 try :
412399 domain_i18n = idna .decode (ascii_domain .encode ('ascii' ))
413400 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 )
415402
416403 # RFC 5321 4.5.3.1.2
417404 # We're checking the number of bytes (octets) here, which can be much
@@ -420,7 +407,7 @@ def validate_email_domain_part(domain):
420407 # as IDNA ASCII. This is also checked by idna.encode, so this exception
421408 # is never reached.
422409 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." )
424411
425412 # A "dot atom text", per RFC 2822 3.2.4, but using the restricted
426413 # characters allowed in a hostname (see ATEXT_HOSTNAME above).
@@ -430,14 +417,14 @@ def validate_email_domain_part(domain):
430417 # with idna.decode, which also checks this format.
431418 m = re .match (DOT_ATOM_TEXT + "\\ Z" , ascii_domain )
432419 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." )
434421
435422 # All publicly deliverable addresses have domain named with at least
436423 # one period. We also know that all TLDs end with a letter.
437424 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 )
439426 if not re .search (r"[A-Za-z]\Z" , ascii_domain ):
440- raise EmailSyntaxError (
427+ raise EmailDomainNoValidTldError (
441428 "The domain name %s is not valid. It is not within a valid top-level domain." % domain_i18n
442429 )
443430
@@ -509,7 +496,7 @@ def dns_resolver_resolve_shim(domain, record):
509496
510497 # If there was no MX, A, or AAAA record, then mail to
511498 # 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 )
513500
514501 except dns .exception .Timeout :
515502 # 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):
523510
524511 except Exception as e :
525512 # 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
528515 )
529516
530517 return {
0 commit comments