10

I have a string coming from server and want to check whether it contains expressions like phone numbers, mail address and email. I got success in case of phone number and mail address, but not email. I am using NSDataDetector for this purpose. eg

NSString *string = sourceNode.label; //coming from server

//Phone number
NSDataDetector *phoneDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypePhoneNumber error:nil]; 
NSArray *phoneMatches = [phoneDetector matchesInString:string options:0 range:NSMakeRange(0, [string length])];

for (NSTextCheckingResult *match in phoneMatches) {

    if ([match resultType] == NSTextCheckingTypePhoneNumber) {
        NSString *matchingStringPhone = [match description];
        NSLog(@"found URL: %@", matchingStringPhone);
    }
}  

But how to do the same for email?

Stunner
  • 12,025
  • 12
  • 86
  • 145
Nitish
  • 13,845
  • 28
  • 135
  • 263
  • 1
    You're out of luck there is no `NSTextCheckingType` for email your could try `NSTextCheckingTypeLink` but it might not yield the desired results. – rckoenes Jan 13 '12 at 13:31
  • 1
    wouldnt it be possible to use a regex instead? http://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html – govi Jan 13 '12 at 13:40
  • Perhaps then regex is the only option left out. Or is there something else I can do? – Nitish Jan 13 '12 at 13:42
  • There is a real solution to your question here: http://stackoverflow.com/q/15525117/1633251 – David H Mar 21 '13 at 14:38

7 Answers7

32
if (result.resultType == NSTextCheckingTypeLink)
{
    if ([result.URL.scheme.locaseString isEqualToString:@"mailto"])
    {
        // email link
    }
    else
    {
        // url
    }
}

Email address falls into NSTextCheckingTypeLink. Simply look for "mailto:" in the URL found and you will know it is an email or URL.

DawnSong
  • 4,752
  • 2
  • 38
  • 38
mkto
  • 4,584
  • 5
  • 41
  • 65
  • 2
    Rather than checking the `absoluteString` it'd be safer to do `[match.URL.scheme isEqualToString:@"mailto"]` – hypercrypt May 15 '14 at 22:35
  • 1
    Be careful here, checking only for `mailto` (in `absoluteString` or `scheme`), will generate a lot of false positives. The data detector will convert `test@example.com,test2@example.com` to just `mailto:test@example.com` and will appear as a match, when the actual full string is not a match since it has 2 addresses in it. Similar to `test@example.com{whitespace}`. See http://stackoverflow.com/a/35076728/144857 for a complete answer. – Dave Wood Oct 07 '16 at 16:45
7

Try following code, see if it works for you :

NSString * mail = so@so.com
NSDataDetector * dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSTextCheckingResult * firstMatch = [dataDetector firstMatchInString:mail options:0 range:NSMakeRange(0, [mail length])];
BOOL result = [firstMatch.URL isKindOfClass:[NSURL class]] && [firstMatch.URL.scheme isEqualToString:@"mailto"];
Hitesh Savaliya
  • 1,336
  • 13
  • 15
  • Nice idea, but this has a couple of problems. It only checks the first match--there could be multiple matches in the string, including links of other kinds. Additionally, strings like "mailto:so@so.com" will pass as well. – Cameron Spickert Nov 27 '12 at 17:10
  • 1
    I think this is the best answer given the question, the question asks to do it using dataDetectors, not looking for its alternative. – Omer Oct 16 '15 at 13:44
7

EDIT:

my answer has been accepted in 2012 and is pretty outdated. Please read please this one instead.

Original post:

In apple documentation, it seems that recognised types does not include email : http://developer.apple.com/library/IOs/#documentation/AppKit/Reference/NSTextCheckingResult_Class/Reference/Reference.html#//apple_ref/c/tdef/NSTextCheckingType

So I suggest you to use a Regexp. It would be like :

NSString* pattern = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]+";

NSPredicate* predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
if ([predicate evaluateWithObject:@"johndoe@example.com"] == YES) {
  // Okay
} else {
  // Not found
}
Martin
  • 11,881
  • 6
  • 64
  • 110
  • 3
    -1 should be for bad answers or stupid ones. Maybe this answer is not the best one and the smartest but it works. – Martin Dec 09 '13 at 09:30
5

Here's a clean Swift version.

extension String {
    func isValidEmail() -> Bool {
        guard !self.lowercaseString.hasPrefix("mailto:") else { return false }
        guard let emailDetector = try? NSDataDetector(types: NSTextCheckingType.Link.rawValue) else { return false }
        let matches = emailDetector.matchesInString(self, options: NSMatchingOptions.Anchored, range: NSRange(location: 0, length: self.characters.count))
        guard matches.count == 1 else { return false }
        return matches[0].URL?.absoluteString == "mailto:\(self)"
    }
}

Swift 3.0 Version:

extension String {
    func isValidEmail() -> Bool {
        guard !self.lowercased().hasPrefix("mailto:") else { return false }
        guard let emailDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { return false }
        let matches = emailDetector.matches(in: self, options: NSRegularExpression.MatchingOptions.anchored, range: NSRange(location: 0, length: self.characters.count))
        guard matches.count == 1 else { return false }
        return matches[0].url?.absoluteString == "mailto:\(self)"
    }
}

Objective-C:

@implementation NSString (EmailValidator)

- (BOOL)isValidEmail {
    if ([self.lowercaseString hasPrefix:@"mailto:"]) { return NO; }

    NSDataDetector* dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
    if (dataDetector == nil) { return NO; }

    NSArray* matches = [dataDetector matchesInString:self options:NSMatchingAnchored range:NSMakeRange(0, [self length])];
    if (matches.count != 1) { return NO; }

    NSTextCheckingResult* match = [matches firstObject];
    return match.resultType == NSTextCheckingTypeLink && [match.URL.absoluteString isEqualToString:[NSString stringWithFormat:@"mailto:%@", self]];
}

@end
Dave Wood
  • 13,143
  • 2
  • 59
  • 67
  • This doesn't work because of ```guard matches.count == 1 else { return false }``` – laynemoseley Oct 05 '16 at 15:27
  • @laynemoseley not sure what your issue is with that `guard` statement. It's just mainly ensuring you have an element in `matches` or else `matches[0]` will crash. And if count > 1, your string will not be a valid email either. – Dave Wood Oct 05 '16 at 19:51
  • oh you know what, you are right. I parsed that logic wrong. Thanks for the clarification! – laynemoseley Oct 07 '16 at 15:27
  • Why do you have a "!" on line " guard !self.lowercased().hasPrefix("mailto:") else { return false }" – Ryan Heitner Jan 08 '17 at 10:09
  • 1
    @RyanHeitner because, if the string has `mailto:` at the start, then the complete string is not a valid email address but the data detector will correctly extract the email address from the rest of the string, falsely giving you the impression the complete string is a valid address. (You don't want to try and email "mailto:something@example.com", or store that in a database etc. – Dave Wood Jan 08 '17 at 10:17
  • @DaveWood Isn't it possible to drop the `guard matches.count == 1 else { return false }` and change the last line to `return matches.first?.url?.absoluteString == "mailto:\(self)"`? As it is already being optional chained for the url anyway. – Remy Baratte Mar 28 '17 at 14:08
  • This detects email..email@domain.com as a valid email address. According to [RFC 5322 (3.2.3 - page 13)](http://www.ietf.org/rfc/rfc5322.txt) multiple periods are not allowed... – Matt Green Nov 29 '17 at 12:07
3

Here's an up to date playground compatible version that builds on top of Dave Wood's and mkto's answer:

import Foundation

func isValid(email: String) -> Bool {
  do {
    let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
    let range = NSRange(location: 0, length: email.count)
    let matches = detector.matches(in: email, options: .anchored, range: range)
    guard matches.count == 1 else { return false }
    return matches[0].url?.scheme == "mailto"
  } catch {
    return false
  }
}

extension String {
  var isValidEmail: Bool {
    isValid(email: self)
  }
}

let email = "test@mail.com"
isValid(email: email) // prints 'true'
email.isValidEmail // prints 'true'
Fero
  • 567
  • 8
  • 25
0

It seems detector now works for email?

let types = [NSTextCheckingType.Link, NSTextCheckingType.PhoneNumber] as NSTextCheckingType
responseAttributedLabel.enabledTextCheckingTypes = types.rawValue

And I am able to click on emails. I am using the TTTAttributedLabel though.

Anson Yao
  • 1,544
  • 17
  • 27
-1

Here's an email example in Swift 1.2. Might not check all edge cases, but it's a good place to start.

func isEmail(emailString : String)->Bool {

    // need optional - will be nil if successful
    var error : NSError?

    // use countElements() with Swift 1.1
    var textRange = NSMakeRange(0, count(emailString))

    // Link type includes email (mailto)
    var detector : NSDataDetector = NSDataDetector(types: NSTextCheckingType.Link.rawValue, error: &error)!

    if error == nil {

    // options value is ignored for this method, but still required! 
    var result = detector.firstMatchInString(emailString, options: NSMatchingOptions.Anchored, range: textRange)

        if result != nil {

            // check range to make sure a substring was not detected
            return result!.URL!.scheme! == "mailto" && (result!.range.location == textRange.location) && (result!.range.length == textRange.length)

        }

    } else {

        // handle error
    }

    return false
}

let validEmail = isEmail("someone@site.com") // returns true