Ad

Our DNA is written in Swift
Jump

E-Mail Validation

For the ProductLayer sign-up form I wanted to make sure that the user can only send a sign up if the email address is valid. If you google for ways to validate an email address you most often solutions involving regular expressions. But since I don’t trust a RegEx unless I know it by heart, I implemented the validation with Apple’s own NSDataDetector for links.

The most highly voted result on a related question on Stack Overflow looks like this:

- (BOOL)validateEmail:(NSString *)candidate 
{
    NSString *emailRegex =
    @"(?:[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[a-z0-9!#$%\\&'*+/=?\\^_`{|}"
    @"~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\"
    @"x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-"
    @"z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5"
    @"]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-"
    @"9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21"
    @"-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; 
    NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES[c] %@", 
                              emailRegex]; 
    return [emailTest evaluateWithObject:candidate];
}

The code might be readable, but such a regular expression makes shivers run up and down my spine.

NSDataDetector is a subclass of NSRegularExpression which internally knows the patterns for a great variety of things it can detect: Phone numbers, addresses, dates, transit information and many more. This is also the way how Apple detects mail addresses and links in Safari or Mail.app.

You can create a data detector for type NSTextCheckingTypeLink and it will include both web addresses as well as email addresses. All that remains are some cursory checks to further narrow down the validation.

- (BOOL)_isValidEmail:(NSString *)email
{
   if (![email length])
   {
      return NO;
   }
 
   NSRange entireRange = NSMakeRange(0, [email length]);
   NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink
                                                              error:NULL];
   NSArray *matches = [detector matchesInString:email options:0 range:entireRange];
 
   // should only a single match
   if ([matches count]!=1)
   {
      return NO;
   }
 
   NSTextCheckingResult *result = [matches firstObject];
 
   // result should be a link
   if (result.resultType != NSTextCheckingTypeLink)
   {
      return NO;
   }
 
   // result should be a recognized mail address
   if (![result.URL.scheme isEqualToString:@"mailto"])
   {
      return NO;
   }
 
   // match must be entire string
   if (!NSEqualRanges(result.range, entireRange))
   {
      return NO;
   }
 
   // but schould not have the mail URL scheme
   if ([email hasPrefix:@"mailto:"])
   {
      return NO;
   }
 
   // no complaints, string is valid email address
   return YES;
}

The data detector infers the mailto: URL scheme and possible multiple link matches in the passed string. The ifs make sure that “mailto:something” is not recognized as a valid email address.

Granted the above is more code to copy/paste but I have a better feeling using the on-board method for detecting a valid email address. Here I have some hope that Apple will make sure the underlying regular expressions are solid.


Categories: Recipes

6 Comments »

  1. I have doubts about `if (![matches count]==1)` part

    `![matches count]` (if none found) would convert 0 (NO) to 1 (YES) which would turn condition to true, so if one match is found then you get a false negative.

    May be you meant `if ([matches count] !=1 )` which would match 0 or more than 1 match being found?

  2. Eimantas Vaičiūnas, thanks for spotting my mistake.

  3. This code is not spotting the error in case of ab..c@example.com . Please help!