├── Mintfile ├── docs └── screenshots │ ├── screenshot_1.png │ ├── screenshot_2.png │ └── screenshot_3.png ├── SpotHeroEmailValidatorDemo ├── SpotHeroEmailValidatorDemo │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ ├── SceneDelegate.swift │ ├── AppDelegate.swift │ ├── ViewController.swift │ ├── Info.plist │ └── Base.lproj │ │ └── Main.storyboard └── SpotHeroEmailValidatorDemo.xcodeproj │ ├── xcshareddata │ └── xcschemes │ │ └── SpotHeroEmailValidatorDemo.xcscheme │ └── project.pbxproj ├── scripts ├── swift_build.sh ├── git-hooks │ └── pre-commit ├── danger_lint.sh ├── xcode_build.sh ├── swiftlint_run_script.sh ├── onboard.sh └── fetch_iana_list.rb ├── RunScriptHelper.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── Gemfile ├── SpotHeroEmailValidator.xcworkspace ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── xcschemes │ │ └── SpotHeroEmailValidator.xcscheme └── contents.xcworkspacedata ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── Sources └── SpotHeroEmailValidator │ ├── AutocorrectSuggestionViewDelegate.swift │ ├── SHValidationResult.swift │ ├── Email.swift │ ├── String+LevenshteinDistance.swift │ ├── EmailTextFieldDelegate.swift │ ├── EmailComponents.swift │ ├── SHEmailValidationTextField.swift │ ├── SpotHeroEmailValidator.swift │ ├── SHAutocorrectSuggestionView.swift │ └── Data │ └── DomainData.plist ├── Package.swift ├── Tests └── SpotHeroEmailValidatorTests │ ├── LevenshteinDistanceTests.swift │ ├── EmailComponentsTests.swift │ └── SpotHeroEmailValidatorTests.swift ├── .swiftformat ├── Dangerfile-Lint ├── .gitignore ├── README.md ├── Gemfile.lock ├── .swiftlint.yml └── LICENSE /Mintfile: -------------------------------------------------------------------------------- 1 | nicklockwood/SwiftFormat@0.48.0 2 | spothero/Zinc@0.8.0 3 | Realm/SwiftLint@0.43.1 4 | -------------------------------------------------------------------------------- /docs/screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spothero/SpotHeroEmailValidator-iOS/HEAD/docs/screenshots/screenshot_1.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spothero/SpotHeroEmailValidator-iOS/HEAD/docs/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spothero/SpotHeroEmailValidator-iOS/HEAD/docs/screenshots/screenshot_3.png -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /scripts/swift_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir $DEPLOY_DIRECTORY 4 | 5 | swift test -c debug --enable-test-discovery --enable-code-coverage 6 | cp $(swift test --show-codecov-path) deploy/codecov.json 7 | -------------------------------------------------------------------------------- /RunScriptHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If there are any files with the .swift extension, run swiftformat on them and add to the commit 4 | git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do 5 | mint run swiftformat "${line}" --quiet; 6 | git add "$line"; 7 | done 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'cocoapods', '~> 1.10.1' 6 | gem 'danger', '~> 8.2.3' 7 | gem 'danger-swiftformat', '~> 0.8.1' 8 | gem 'danger-swiftlint', '~> 0.26.0' 9 | 10 | # Used by fetch_iana_list.rb 11 | gem 'httparty', '~> 0.18.1' 12 | gem 'plist', '~> 3.6.0' 13 | -------------------------------------------------------------------------------- /SpotHeroEmailValidator.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RunScriptHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # Source: https://blog.github.com/2017-07-06-introducing-code-owners/ 4 | # Source: https://help.github.com/articles/about-codeowners/ 5 | 6 | # These owners will be the default owners for everything in the repo. 7 | * @spothero/ios -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/AutocorrectSuggestionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | #if canImport(UIKit) 4 | 5 | import Foundation 6 | 7 | protocol AutocorrectSuggestionViewDelegate: AnyObject { 8 | func suggestionView(_ suggestionView: SHAutocorrectSuggestionView, wasDismissedWithAccepted accepted: Bool) 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /SpotHeroEmailValidator.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/danger_lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if mint is installed 4 | if [ -x "$(command -v mint)" ]; then 5 | # Set Danger's SwiftLint version to match our Mintfile 6 | export SWIFTLINT_VERSION=$(mint run swiftlint version) 7 | # Set Danger's SwiftFormat version and binary to match our Mintfile 8 | export SWIFTFORMAT_VERSION=$(mint run swiftformat --version) 9 | export SWIFTFORMAT_PATH=$(mint which swiftformat) 10 | fi 11 | 12 | # Run Danger 13 | bundle exec danger --dangerfile=Dangerfile-Lint --fail-on-errors=true --remove-previous-comments --new-comment --verbose 14 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/SHValidationResult.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public class SHValidationResult: NSObject { 6 | /// Indicates whether or not the email address being analyzed is in valid syntax and format. 7 | public let passedValidation: Bool 8 | 9 | /// The autocorrect suggestion to be applied. Nil if there no suggestion. 10 | public let autocorrectSuggestion: String? 11 | 12 | public init(passedValidation: Bool, autocorrectSuggestion: String?) { 13 | self.passedValidation = passedValidation 14 | self.autocorrectSuggestion = autocorrectSuggestion 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/xcode_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir $DEPLOY_DIRECTORY 4 | 5 | set -o pipefail && 6 | env NSUnbufferedIO=YES \ 7 | xcodebuild \ 8 | -workspace "$XCODEBUILD_WORKSPACE" \ 9 | -scheme "$XCODEBUILD_SCHEME" \ 10 | -destination "$1" \ 11 | -resultBundlePath "./deploy/Test.xcresult" \ 12 | -enableCodeCoverage YES \ 13 | clean test \ 14 | | tee "./deploy/xcodebuild.log" \ 15 | | xcpretty 16 | 17 | # Bitrise also calls the following: 18 | 19 | # COMPILER_INDEX_STORE_ENABLE=NO \ 20 | # GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES \ 21 | # GCC_GENERATE_TEST_COVERAGE_FILES=YES \ 22 | # xcpretty -color --report html --output output/xcode-test-results-UtilityBelt.html 23 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import UIKit 4 | 5 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 6 | var window: UIWindow? 7 | 8 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 9 | guard scene as? UIWindowScene != nil else { 10 | return 11 | } 12 | } 13 | 14 | func sceneDidDisconnect(_ scene: UIScene) {} 15 | 16 | func sceneDidBecomeActive(_ scene: UIScene) {} 17 | 18 | func sceneWillResignActive(_ scene: UIScene) {} 19 | 20 | func sceneWillEnterForeground(_ scene: UIScene) {} 21 | 22 | func sceneDidEnterBackground(_ scene: UIScene) {} 23 | } 24 | -------------------------------------------------------------------------------- /scripts/swiftlint_run_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Only run linting scripts locally, Danger will handle linting on CI 4 | if ! [ -z "$CI" ]; then 5 | echo "The SwiftLint Run Script does not run on CI. Don't worry, Danger will handle it! (swiftlint_run_script)" 6 | exit 0 7 | fi 8 | 9 | # The workspace directory should be the git repo root 10 | workspace_directory=$(git rev-parse --show-toplevel) 11 | 12 | # Allow passing in a file path for .swiftlint.yml, otherwise it looks in the workspace directory 13 | if [ -z "$0" ]; then 14 | swiftlint_yml_path=$0 15 | else 16 | swiftlint_yml_path="${workspace_directory}/.swiftlint.yml" 17 | fi 18 | 19 | # Set the command 20 | command="xcrun --sdk macosx mint run swiftlint --config ${swiftlint_yml_path}" 21 | 22 | # Call the command! 23 | eval $command 24 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/Email.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | /// A validated email address. 6 | public struct Email { 7 | /// A `String` representing the email. 8 | public let string: String 9 | 10 | /// The components of an email address. 11 | public let components: EmailComponents 12 | 13 | /// Construct an `Email` from a string if it's valid. Returns `nil` otherwise. 14 | /// 15 | /// For detailed errors use ``EmailComponents.init(email:)`` 16 | /// - Parameter string: The string containing the email. 17 | public init?(string: String) { 18 | guard let components = try? EmailComponents(email: string) else { 19 | return nil 20 | } 21 | 22 | self.components = components 23 | self.string = components.string 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | func application(_ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | 12 | // MARK: UISceneSession Lifecycle 13 | 14 | func application(_ application: UIApplication, 15 | configurationForConnecting connectingSceneSession: UISceneSession, 16 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | 20 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} 21 | } 22 | -------------------------------------------------------------------------------- /scripts/onboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify the script has been executed 4 | echo "Running onboarding script..." 5 | 6 | ONBOARD_DOCUMENTATION="https://github.com/spothero/Shared-iOS/main/ONBOARDING.md" 7 | 8 | # 1 - Install and Verify Build Tools 9 | 10 | # ensure that homebrew is installed 11 | # TODO: Add check for version of homebrew 12 | command -v brew >/dev/null 2>&1 || { echo >&2 "Homebrew is not installed."; exit 1; } 13 | 14 | # ensure that mint is installed 15 | # TODO: Add check for version of mint 16 | command -v mint >/dev/null 2>&1 || { echo >&2 "Mint is not installed."; exit 1; } 17 | 18 | # TODO: Add items from SpotHero-iOS (bundler, ruby, rvm, fastlane, etc.) 19 | 20 | # 2 - Install Git Hooks 21 | 22 | # if there are any scripts in the scripts/git-hooks folder, 23 | # copy them into the .git/hooks folder which is not source controlled 24 | # This will replace any existing files 25 | cp -R -a scripts/git-hooks/. .git/hooks 26 | 27 | # Verify the script has completed 28 | echo "Onboarding complete." 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | // Copyright © 2021 SpotHero, Inc. All rights reserved. 4 | 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "SpotHeroEmailValidator", 9 | platforms: [ 10 | .iOS(.v9), // minimum supported version via SPM 11 | .macOS(.v10_10), // minimum supported version via SPM 12 | .tvOS(.v9), // minimum supported version via SPM 13 | // watchOS is unsupported 14 | ], 15 | products: [ 16 | .library(name: "SpotHeroEmailValidator", targets: ["SpotHeroEmailValidator"]), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SpotHeroEmailValidator", 21 | dependencies: [], 22 | resources: [ 23 | .copy("Data/DomainData.plist"), 24 | ] 25 | ), 26 | .testTarget( 27 | name: "SpotHeroEmailValidatorTests", 28 | dependencies: [ 29 | .target(name: "SpotHeroEmailValidator"), 30 | ] 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Tests/SpotHeroEmailValidatorTests/LevenshteinDistanceTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | @testable import SpotHeroEmailValidator 4 | import XCTest 5 | 6 | class LevenshteinDistanceTests: XCTestCase { 7 | struct LevenshteinDistanceTestModel { 8 | var stringA: String 9 | var stringB: String 10 | var distance: Int 11 | } 12 | 13 | func testDistanceCategory() { 14 | let tests = [ 15 | LevenshteinDistanceTestModel(stringA: "kitten", stringB: "sitting", distance: 3), 16 | LevenshteinDistanceTestModel(stringA: "testing", stringB: "lev", distance: 6), 17 | LevenshteinDistanceTestModel(stringA: "book", stringB: "back", distance: 2), 18 | LevenshteinDistanceTestModel(stringA: "spot", stringB: "hero", distance: 4), 19 | LevenshteinDistanceTestModel(stringA: "parking", stringB: "rules", distance: 6), 20 | LevenshteinDistanceTestModel(stringA: "lame", stringB: "same", distance: 1), 21 | LevenshteinDistanceTestModel(stringA: "same", stringB: "same", distance: 0), 22 | ] 23 | 24 | for test in tests { 25 | XCTAssertEqual(test.stringA.levenshteinDistance(from: test.stringB), test.distance) 26 | XCTAssertEqual(test.stringB.levenshteinDistance(from: test.stringA), test.distance) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/fetch_iana_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright © 2019 SpotHero, Inc. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'plist' 19 | require 'httparty' 20 | 21 | puts("Updating IANA TLD list.") 22 | 23 | # Call from the scripts directory, so it can be called from any location 24 | scripts_directory = File.dirname(__FILE__) 25 | 26 | plistPath = "#{scripts_directory}/../Sources/SpotHeroEmailValidator/data/DomainData.plist" 27 | domainDataPlist = Plist::parse_xml(plistPath) 28 | 29 | # Fetch latest IANA TLDs. 30 | response = HTTParty.get('http://data.iana.org/TLD/tlds-alpha-by-domain.txt') 31 | tldArray = response.body.split("\n").map {|e| e.downcase} 32 | tldArray.shift 33 | 34 | domainDataPlist['IANARegisteredTLDs'] = tldArray 35 | 36 | File.open(plistPath, 'w') { |f| 37 | f.write(domainDataPlist.to_plist) 38 | } 39 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import SpotHeroEmailValidator 4 | import UIKit 5 | 6 | class ViewController: UIViewController { 7 | @IBOutlet private var explanationLabel: UILabel! 8 | @IBOutlet private var emailTextField: SHEmailValidationTextField! 9 | @IBOutlet private var passwordTextField: UITextField! 10 | 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | 14 | // swiftlint:disable:next line_length 15 | self.explanationLabel.text = "Enter an email address into the UITextField, then proceed to the password field for validation. Addresses such as test@gamil.con will produce an autocorrect suggestion." 16 | 17 | self.emailTextField.delegate = self 18 | self.emailTextField.setMessage("Email address syntax is invalid.", forErrorCode: Int(SpotHeroEmailValidator.Error.invalidSyntax.rawValue)) 19 | 20 | // Uncomment these lines to configure the look and feel of the suggestion popup 21 | // self.emailTextField.bubbleFillColor = .white 22 | // self.emailTextField.bubbleTitleColor = .black 23 | // self.emailTextField.bubbleSuggestionColor = .red 24 | } 25 | 26 | override func willAnimateRotation(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) { 27 | self.emailTextField.hostWillAnimateRotation(to: toInterfaceOrientation) 28 | } 29 | } 30 | 31 | extension ViewController: UITextFieldDelegate { 32 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 33 | guard textField == self.emailTextField else { 34 | return false 35 | } 36 | 37 | return self.passwordTextField.becomeFirstResponder() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/String+LevenshteinDistance.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | public extension String { 6 | /// Returns the minimum number of operations to edit this string into another string. 7 | /// 8 | /// Source: [Minimum Edit Distance - Swift Algorithm Club](https://github.com/raywenderlich/swift-algorithm-club/tree/main/Minimum%20Edit%20Distance) 9 | /// - Parameter other: The other string to compare with. 10 | func levenshteinDistance(from other: String) -> Int { 11 | // swiftlint:disable identifier_name 12 | let m = self.count 13 | let n = other.count 14 | var matrix = [[Int]](repeating: [Int](repeating: 0, count: n + 1), count: m + 1) 15 | 16 | // initialize matrix 17 | for index in 1 ... m { 18 | // the distance of any first string to an empty second string 19 | matrix[index][0] = index 20 | } 21 | 22 | for index in 1 ... n { 23 | // the distance of any second string to an empty first string 24 | matrix[0][index] = index 25 | } 26 | 27 | // compute Levenshtein distance 28 | for (i, selfChar) in self.enumerated() { 29 | for (j, otherChar) in other.enumerated() { 30 | if otherChar == selfChar { 31 | // substitution of equal symbols with cost 0 32 | matrix[i + 1][j + 1] = matrix[i][j] 33 | } else { 34 | // minimum of the cost of insertion, deletion, or substitution 35 | // added to the already computed costs in the corresponding cells 36 | matrix[i + 1][j + 1] = Swift.min(matrix[i][j] + 1, matrix[i + 1][j] + 1, matrix[i][j + 1] + 1) 37 | } 38 | } 39 | } 40 | // swiftlint:enable identifier_name 41 | return matrix[m][n] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # Updated for v0.40.11 2 | 3 | #---------------------# 4 | # SwiftFormat Options # 5 | #---------------------# 6 | 7 | --swiftversion 5.3 8 | 9 | #-------------------# 10 | # Whitelisted Rules # 11 | #-------------------# 12 | 13 | # Use this section to opt-in to rules explicitly 14 | # When rules in this list are enabled, no other rules will be run 15 | 16 | # --rules redundantSelf 17 | # --rules trailingSpace 18 | 19 | #--------------------# 20 | # Rule Configuration # 21 | #--------------------# 22 | 23 | # makes sure the self. prefix is added where appropriate 24 | --self insert 25 | 26 | # only strips unused arguments (replacing with _) in closures, not methods 27 | --stripunusedargs closure-only 28 | 29 | # sets the header block to supplied text 30 | --header "Copyright © {year} SpotHero, Inc. All rights reserved." 31 | 32 | # only trims whitespace on nonblank-lines to avoid xcode inconsistencies 33 | --trimwhitespace nonblank-lines 34 | 35 | # this removes the underscore (_) separation in large numbers 36 | --binarygrouping none 37 | --decimalgrouping none 38 | --hexgrouping none 39 | --octalgrouping none 40 | --exponentgrouping disabled 41 | --fractiongrouping disabled 42 | 43 | #----------------# 44 | # Disabled Rules # 45 | #----------------# 46 | 47 | # Enforces consistent ordering for member specifiers 48 | # Disabled because this rule is non-configurable 49 | --disable specifiers 50 | 51 | # Removes return within closures as well as the new Swift 5 implicit return 52 | # Disabled because this rule is non-configurable 53 | --disable redundantReturn 54 | 55 | # Wrap the opening brace of multiline statements 56 | # Disabled because this rule is non-configurable 57 | --disable wrapMultilineStatementBraces 58 | 59 | #-----------------# 60 | # File Exclusions # 61 | #-----------------# 62 | 63 | --exclude Pods 64 | --exclude .build 65 | --exclude .swiftpm 66 | --exclude Package.swift 67 | --exclude */Package.swift 68 | --exclude Tests/LinuxMain.swift 69 | --exclude "Tests/*/XCTestManifests.swift" 70 | --exclude "**/*/*+CoreDataProperties.swift" 71 | -------------------------------------------------------------------------------- /Dangerfile-Lint: -------------------------------------------------------------------------------- 1 | # Updated for v8.0.0 2 | 3 | # frozen_string_literal: true 4 | 5 | # ============= # 6 | # Constants # 7 | # ============= # 8 | 9 | SWIFTLINT_INLINE_MODE = true 10 | IS_DEBUGGING = false 11 | 12 | # =========== # 13 | # Linting # 14 | # =========== # 15 | 16 | # Sources for all third-party tools can be found here: 17 | # https://danger.systems/ruby/ 18 | # Scroll down and select the bubble that has the tool you're looking for 19 | 20 | # Runs the Danger-SwiftFormat plugin and fail the build if there are pending formatting changes 21 | if defined?(swiftformat) && File.exist?('.swiftformat') 22 | # Gets the version of SwiftFormat used by Danger 23 | swiftformat_version = ENV['SWIFTFORMAT_VERSION'] || `swiftformat --version` 24 | 25 | # Prints the version of SwiftFormat used to help with debugging violations and keeping our projects in sync. 26 | message "Formatted with SwiftFormat v#{swiftformat_version.strip}." 27 | 28 | # Sets the path of the SwiftFormat binary if the env var has been set 29 | swiftformat.binary_path = ENV['SWIFTFORMAT_PATH'] if ENV.key?('SWIFTFORMAT_PATH') 30 | 31 | # Run SwiftFormat 32 | swiftformat.check_format(fail_on_error: true) 33 | end 34 | 35 | # Runs the Danger-SwiftLint plugin and make inline comments with any warnings or errors 36 | if defined?(swiftlint) && File.exist?('.swiftlint.yml') 37 | # Gets the version of SwiftLint used by Danger 38 | swiftlint_version = ENV['SWIFTLINT_VERSION'] || `danger-swiftlint swiftlint_version` 39 | 40 | # Prints the version of SwiftLint used to help with debugging violations and keeping our projects in sync. 41 | message "Linted with SwiftLint v#{swiftlint_version.strip}." 42 | 43 | swiftlint.verbose = IS_DEBUGGING 44 | 45 | # Run SwiftLint 46 | swiftlint.lint_files(inline_mode: SWIFTLINT_INLINE_MODE) 47 | 48 | fail "SwiftLint found #{swiftlint.issues.length} violations." if swiftlint.issues.length > 0 49 | end 50 | 51 | # We need a pat on the back sometimes! 52 | message '👋 👋 Great job!' if status_report[:errors].empty? && status_report[:warnings].empty? 53 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/EmailTextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | #if canImport(UIKit) 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | class EmailTextFieldDelegate: NSObject { 9 | weak var target: SHEmailValidationTextField? 10 | weak var subDelegate: UITextFieldDelegate? 11 | 12 | init(target: SHEmailValidationTextField) { 13 | self.target = target 14 | } 15 | } 16 | 17 | extension EmailTextFieldDelegate: UITextFieldDelegate { 18 | func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { 19 | self.target?.dismissSuggestionView() 20 | 21 | return self.subDelegate?.textFieldShouldBeginEditing?(textField) ?? true 22 | } 23 | 24 | func textFieldDidBeginEditing(_ textField: UITextField) { 25 | self.subDelegate?.textFieldDidBeginEditing?(textField) 26 | } 27 | 28 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 29 | self.target?.validateInput() 30 | 31 | return self.subDelegate?.textFieldShouldEndEditing?(textField) ?? true 32 | } 33 | 34 | func textFieldDidEndEditing(_ textField: UITextField) { 35 | self.subDelegate?.textFieldDidEndEditing?(textField) 36 | } 37 | 38 | func textField(_ textField: UITextField, 39 | shouldChangeCharactersIn range: NSRange, 40 | replacementString string: String) -> Bool { 41 | return self.subDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true 42 | } 43 | 44 | func textFieldShouldClear(_ textField: UITextField) -> Bool { 45 | return self.subDelegate?.textFieldShouldClear?(textField) ?? true 46 | } 47 | 48 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 49 | return self.subDelegate?.textFieldShouldReturn?(textField) ?? true 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Exclude OS X folder attributes 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | deploy/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | .build/ 45 | 46 | # .swiftpm/ is generated when editing a Swift package in Xcode 11 47 | .swiftpm/ 48 | 49 | # For SPM-only projects, .xcworkspace or .xcodeproj files should never be created or generated, except for use with versions earlier than Xcode 11 50 | # *.xcworkspace 51 | # *.xcodeproj 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | 61 | # Carthage 62 | # 63 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 64 | # Carthage/Checkouts 65 | Carthage/Build 66 | 67 | # jazzy 68 | undocumented.json 69 | *.tgz 70 | 71 | # fastlane 72 | # 73 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 74 | # screenshots whenever they are needed. 75 | # For more information about the recommended setup visit: 76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 77 | 78 | fastlane/test_output 79 | fastlane/[Pp]rovisioning 80 | fastlane/report.xml 81 | fastlane/[Bb]uild 82 | fastlane/DerivedData 83 | fastlane/screenshots 84 | fastlane/[Pp]review.html 85 | 86 | # dotenv 87 | # A .env.local file contains keys that should never be checked into the repository 88 | .env.local -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | UIInterfaceOrientationPortraitUpsideDown 56 | 57 | UISupportedInterfaceOrientations~ipad 58 | 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationPortraitUpsideDown 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/EmailComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | /// A structure that parses emails into and constructs emails from their constituent parts. 6 | public struct EmailComponents { 7 | /// The portion of an email before the @ 8 | public let username: String 9 | 10 | /// The hostname of the domain 11 | public let hostname: String 12 | 13 | /// The top level domain. 14 | public let tld: String 15 | 16 | /// An email created from the components 17 | public let string: String 18 | 19 | /// Initializes an email using its constituent parts. 20 | /// - Parameters: 21 | /// - username: The portion of an email before the @ 22 | /// - hostname: The hostname of the domain 23 | /// - tld: The top level domain. 24 | public init(username: String, hostname: String, tld: String) { 25 | self.username = username 26 | self.hostname = hostname 27 | self.tld = tld 28 | self.string = "\(username)@\(hostname)\(tld)" 29 | } 30 | 31 | /// Parses an email and exposes its constituent parts. 32 | public init(email: String) throws { 33 | // Ensure there is exactly one @ symbol. 34 | guard email.filter({ $0 == "@" }).count == 1 else { 35 | throw SpotHeroEmailValidator.Error.invalidSyntax 36 | } 37 | 38 | let emailAddressParts = email.split(separator: "@") 39 | 40 | // Extract the username from the email address parts 41 | let username = String(emailAddressParts.first ?? "") 42 | // Extract the full domain (including TLD) from the email address parts 43 | let fullDomain = String(emailAddressParts.last ?? "") 44 | // Split the domain parts for evaluation 45 | let domainParts = fullDomain.split(separator: ".") 46 | 47 | guard domainParts.count >= 2 else { 48 | // There are no periods found in the domain, throw an error 49 | throw SpotHeroEmailValidator.Error.invalidDomain 50 | } 51 | 52 | // TODO: This logic is wrong and doesn't take subdomains into account. We should compare TLDs against the commonTLDs list." 53 | 54 | // Extract the domain from the domain parts 55 | let domain = domainParts.first?.lowercased() ?? "" 56 | 57 | // Extract the TLD from the domain parts, which are all the remaining parts joined with a period again 58 | let tld = domainParts.dropFirst().joined(separator: ".") 59 | 60 | // Complete initialization 61 | self.username = username 62 | self.hostname = domain 63 | self.tld = tld 64 | self.string = email 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/SpotHeroEmailValidatorTests/EmailComponentsTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | @testable import SpotHeroEmailValidator 4 | import XCTest 5 | 6 | // swiftlint:disable nesting 7 | class EmailComponentsTests: XCTestCase { 8 | struct TestModel { 9 | enum ExpectedResult { 10 | case `throw`(SpotHeroEmailValidator.Error) 11 | case `return`(username: String, hostname: String, tld: String) 12 | } 13 | 14 | /// The email under test. 15 | let email: String 16 | 17 | /// The result expected when ran through ``EmailComponents``. 18 | let expectedResult: ExpectedResult 19 | 20 | init(email emailUnderTest: String, expectedTo result: ExpectedResult) { 21 | self.email = emailUnderTest 22 | self.expectedResult = result 23 | } 24 | } 25 | 26 | func testEmailComponentsInit() { 27 | let tests = [ 28 | // Successful Examples 29 | TestModel(email: "test@email.com", 30 | expectedTo: .return(username: "test", hostname: "email", tld: "com")), 31 | 32 | TestModel(email: "TEST@EMAIL.COM", 33 | expectedTo: .return(username: "TEST", hostname: "email", tld: "COM")), 34 | 35 | TestModel(email: "test+-.test@email.com", 36 | expectedTo: .return(username: "test+-.test", hostname: "email", tld: "com")), 37 | 38 | TestModel(email: #""JohnDoe"@email.com"#, 39 | expectedTo: .return(username: #""JohnDoe""#, hostname: "email", tld: "com")), 40 | 41 | // Failing Examples 42 | TestModel(email: "t@st@email.com", expectedTo: .throw(.invalidSyntax)), 43 | TestModel(email: "test.com", expectedTo: .throw(.invalidSyntax)), 44 | 45 | // Domain Tests 46 | TestModel(email: "test@email", expectedTo: .throw(.invalidDomain)), 47 | ] 48 | 49 | for test in tests { 50 | switch test.expectedResult { 51 | case .throw: 52 | XCTAssertThrowsError(try EmailComponents(email: test.email)) { error in 53 | XCTAssertEqual(error.localizedDescription, 54 | error.localizedDescription, 55 | "Test failed for email address: \(test.email)") 56 | } 57 | case let .return(username, hostname, tld): 58 | do { 59 | let actualComponents = try EmailComponents(email: test.email) 60 | XCTAssertEqual(actualComponents.username, username) 61 | XCTAssertEqual(actualComponents.hostname, hostname) 62 | XCTAssertEqual(actualComponents.tld, tld) 63 | } catch { 64 | XCTFail("Test failed for email address: \(test.email). \(error.localizedDescription)") 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SpotHeroEmailValidator.xcworkspace/xcshareddata/xcschemes/SpotHeroEmailValidator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | PACKAGE_NAME: SpotHeroEmailValidator 11 | XCODEBUILD_WORKSPACE: SpotHeroEmailValidator.xcworkspace 12 | XCODEBUILD_SCHEME: SpotHeroEmailValidator 13 | DEPLOY_DIRECTORY: deploy 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | runs-on: macos-11 19 | permissions: 20 | pull-requests: write 21 | env: 22 | DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | - name: Install Dependencies 27 | run: | 28 | bundle install 29 | brew install swiftformat 30 | - name: Run Danger 31 | run: sh ./scripts/danger_lint.sh 32 | iOS: 33 | name: iOS ${{ matrix.os }} ${{ matrix.device_name }} 34 | runs-on: macos-11 35 | needs: [lint] 36 | strategy: 37 | matrix: 38 | device_name: ["iPhone 12 Pro", "iPad Pro (11-inch) (2nd generation)"] 39 | os: ["15.0"] 40 | xcode_version: ["13.0"] 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v2 44 | - name: Switch Xcode Version 45 | run: sudo xcode-select -switch "/Applications/Xcode_${{ matrix.xcode_version }}.app" 46 | - name: Run Tests 47 | run: sh ./scripts/xcode_build.sh "name=${{ matrix.device_name }},OS=${{ matrix.os }},platform=iOS Simulator" 48 | - name: Upload Step Output 49 | uses: actions/upload-artifact@v1 50 | with: 51 | name: "iOS ${{ matrix.os }} ${{ matrix.device_name }} Output" 52 | path: ${{ env.DEPLOY_DIRECTORY }} 53 | macOS: 54 | name: macOS 55 | runs-on: macos-11 56 | needs: [lint] 57 | strategy: 58 | matrix: 59 | xcode_version: ["13.0"] 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Switch Xcode Version 64 | run: sudo xcode-select -switch "/Applications/Xcode_${{ matrix.xcode_version }}.app" 65 | - name: Run Tests 66 | run: sh ./scripts/xcode_build.sh "platform=macOS" 67 | - name: Upload Step Output 68 | uses: actions/upload-artifact@v1 69 | with: 70 | name: "macOS 11 Output" 71 | path: ${{ env.DEPLOY_DIRECTORY }} 72 | tvOS: 73 | name: tvOS ${{ matrix.os }} ${{ matrix.device_name }} 74 | runs-on: macos-11 75 | needs: [lint] 76 | strategy: 77 | matrix: 78 | device_name: ["Apple TV 4K"] 79 | os: ["15.0"] 80 | xcode_version: ["13.0"] 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@v2 84 | - name: Switch Xcode Version 85 | run: sudo xcode-select -switch "/Applications/Xcode_${{ matrix.xcode_version }}.app" 86 | - name: Run Tests 87 | run: sh ./scripts/xcode_build.sh "name=${{ matrix.device_name }},OS=${{ matrix.os }},platform=tvOS Simulator" 88 | - name: Upload Step Output 89 | uses: actions/upload-artifact@v1 90 | with: 91 | name: "tvOS ${{ matrix.os }} ${{ matrix.device_name }} Output" 92 | path: ${{ env.DEPLOY_DIRECTORY }} 93 | spm: 94 | name: SPM (${{ matrix.os }}) 95 | runs-on: ${{ matrix.os }} 96 | needs: [lint] 97 | strategy: 98 | matrix: 99 | os: [macos-11] 100 | xcode_version: ["13.0"] 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v2 104 | - name: Switch Xcode Version 105 | run: sudo xcode-select -switch "/Applications/Xcode_${{ matrix.xcode_version }}.app" 106 | - name: Run Tests 107 | run: sh ./scripts/swift_build.sh 108 | - name: Upload Step Output 109 | uses: actions/upload-artifact@v1 110 | with: 111 | name: SPM Output 112 | path: ${{ env.DEPLOY_DIRECTORY }} 113 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo.xcodeproj/xcshareddata/xcschemes/SpotHeroEmailValidatorDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 70 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpotHeroEmailValidator 2 | 3 | [![CI Status](https://github.com/spothero/SpotHeroEmailValidator-iOS/workflows/CI/badge.svg)](https://github.com/spothero/SpotHeroEmailValidator-iOS/actions?query=workflow%3A%22CI%22) 4 | [![Latest Release](https://img.shields.io/github/v/tag/spothero/SpotHeroEmailValidator-iOS?color=blue&label=latest)](https://github.com/spothero/SpotHeroEmailValidator-iOS/releases) 5 | [![Swift Version](https://img.shields.io/static/v1?label=swift&message=5.3&color=red&logo=swift&logoColor=white)](https://developer.apple.com/swift) 6 | [![Platform Support](https://img.shields.io/static/v1?label=platform&message=iOS%20|%20macOS&color=darkgray)](https://github.com/spothero/SpotHeroEmailValidator-iOS/blob/main/Package.swift) 7 | [![License](https://img.shields.io/github/license/spothero/SpotHeroEmailValidator-iOS)](https://github.com/spothero/SpotHeroEmailValidator-iOS/blob/main/LICENSE) 8 | 9 | An iOS library that will provide basic email syntax validation as well as provide suggestions for possible typos (for example, test@gamil.con would be corrected to test@gmail.com). 10 | 11 | ## Screenshots 12 | ![Typo correction suggestion](docs/screenshots/screenshot_1.png "Typo correction suggestion") 13 | ![Basic syntax validation](docs/screenshots/screenshot_2.png "Basic syntax validation") 14 | ![Typo correction suggestion](docs/screenshots/screenshot_3.png "Typo correction suggestion") 15 | 16 | ## Usage 17 | ### UITextField subclass 18 | The SHEmailValidationTextField class is a pre-built UITextField subclass that will automatically validate its own input when it loses focus (such as when the user goes from the email field to the password field). If a syntax error is detected, a popup will appear informing the user that the email address they entered is invalid. If a probably typo is detected, a popup will appear that allows the user to accept the suggestion or dismiss the popup. Using this class is as simple as replacing instances of UITextField with SHEmailValidationTextField. 19 | 20 | #### Customization 21 | To customize the default error message that appear for validation errors, use the `setDefaultErrorMessage:` method. To set specific messages for specific errors, use the `setMessage:forErrorCode:` method. To customize the text that appears above a typo suggestion, use the `setMessageForSuggestion:` method. 22 | 23 | To customize the look and feel of the popup window that appears, the `fillColor`, `titleColor`, and `suggestionColor` properties can be set as desired. 24 | 25 | ### Basic syntax checking 26 | NSError *error = nil; 27 | [[[SpotHeroEmailValidator] validator] validateSyntaxOfEmailAddress:emailAddress withError:&error]; 28 | 29 | if (error) { 30 | // An error occurred 31 | switch (error.code) { 32 | case SHBlankAddressError: 33 | // Input was empty 34 | break; 35 | case SHInvalidSyntaxError: 36 | // Syntax completely wrong (probably missing @ or .) 37 | break; 38 | case SHInvalidUsernameError: 39 | // Local portion of the email address is empty or contains invalid characters 40 | break; 41 | case SHInvalidDomainError: 42 | // Domain portion of the email address is empty or contains invalid characters 43 | break; 44 | case SHInvalidTLDError: 45 | // TLD portion of the email address is empty, contains invalid characters, or is under 2 characters long 46 | break; 47 | } 48 | } else { 49 | // Basic email syntax is correct 50 | } 51 | 52 | ### Get typo correction suggestion 53 | NSError *error = nil; 54 | NSString *suggestion = [[[SpotHeroEmailValidator] validator] autocorrectSuggestionForEmailAddress:emailAddress withError:&error]; 55 | 56 | if (error) { 57 | // The syntax check failed, so no suggestions could be generated 58 | } else if (suggestion) { 59 | // A probable typo has been detected and the suggestion variable holds the suggested correction 60 | } else { 61 | // No typo was found, or no suggestions could be generated 62 | } 63 | 64 | ## Updating the IANA TLD list 65 | To fetch the latest IANA TLDs, run the following script included in the root directory: 66 | fetch_iana_list.rb 67 | 68 | This will update the plist under SpotHeroEmailValidator/DomainData.plist 69 | The script requires the httparty and plist Ruby gems to be installed. 70 | 71 | ## ARC 72 | SpotHeroEmailValidator uses ARC. If your project is not ARC-compliant, simply set the `-fobjc-arc` flag on all SpotHeroEmailValidator source files. 73 | 74 | ## Apps Using this Library 75 | This library is used in our own [SpotHero](https://apps.apple.com/us/app/spothero-find-parking-nearby/id499097243) iOS app. If you would like to see your app listed here as well, let us know you're using it! 76 | 77 | ## License 78 | SpotHeroEmailValidator is released under the Apache 2.0 license. 79 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | activesupport (5.2.5) 6 | concurrent-ruby (~> 1.0, >= 1.0.2) 7 | i18n (>= 0.7, < 2) 8 | minitest (~> 5.1) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.5) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | claide (1.0.3) 17 | claide-plugins (0.9.2) 18 | cork 19 | nap 20 | open4 (~> 1.3) 21 | cocoapods (1.10.1) 22 | addressable (~> 2.6) 23 | claide (>= 1.0.2, < 2.0) 24 | cocoapods-core (= 1.10.1) 25 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 26 | cocoapods-downloader (>= 1.4.0, < 2.0) 27 | cocoapods-plugins (>= 1.0.0, < 2.0) 28 | cocoapods-search (>= 1.0.0, < 2.0) 29 | cocoapods-trunk (>= 1.4.0, < 2.0) 30 | cocoapods-try (>= 1.1.0, < 2.0) 31 | colored2 (~> 3.1) 32 | escape (~> 0.0.4) 33 | fourflusher (>= 2.3.0, < 3.0) 34 | gh_inspector (~> 1.0) 35 | molinillo (~> 0.6.6) 36 | nap (~> 1.0) 37 | ruby-macho (~> 1.4) 38 | xcodeproj (>= 1.19.0, < 2.0) 39 | cocoapods-core (1.10.1) 40 | activesupport (> 5.0, < 6) 41 | addressable (~> 2.6) 42 | algoliasearch (~> 1.0) 43 | concurrent-ruby (~> 1.1) 44 | fuzzy_match (~> 2.0.4) 45 | nap (~> 1.0) 46 | netrc (~> 0.11) 47 | public_suffix 48 | typhoeus (~> 1.0) 49 | cocoapods-deintegrate (1.0.4) 50 | cocoapods-downloader (1.4.0) 51 | cocoapods-plugins (1.0.0) 52 | nap 53 | cocoapods-search (1.0.0) 54 | cocoapods-trunk (1.5.0) 55 | nap (>= 0.8, < 2.0) 56 | netrc (~> 0.11) 57 | cocoapods-try (1.2.0) 58 | colored2 (3.1.2) 59 | concurrent-ruby (1.1.8) 60 | cork (0.3.0) 61 | colored2 (~> 3.1) 62 | danger (8.2.3) 63 | claide (~> 1.0) 64 | claide-plugins (>= 0.9.2) 65 | colored2 (~> 3.1) 66 | cork (~> 0.1) 67 | faraday (>= 0.9.0, < 2.0) 68 | faraday-http-cache (~> 2.0) 69 | git (~> 1.7) 70 | kramdown (~> 2.3) 71 | kramdown-parser-gfm (~> 1.0) 72 | no_proxy_fix 73 | octokit (~> 4.7) 74 | terminal-table (>= 1, < 4) 75 | danger-plugin-api (1.0.0) 76 | danger (> 2.0) 77 | danger-swiftformat (0.8.1) 78 | danger-plugin-api (~> 1.0) 79 | danger-swiftlint (0.26.0) 80 | danger 81 | rake (> 10) 82 | thor (~> 0.19) 83 | escape (0.0.4) 84 | ethon (0.13.0) 85 | ffi (>= 1.15.0) 86 | faraday (1.4.1) 87 | faraday-excon (~> 1.1) 88 | faraday-net_http (~> 1.0) 89 | faraday-net_http_persistent (~> 1.1) 90 | multipart-post (>= 1.2, < 3) 91 | ruby2_keywords (>= 0.0.4) 92 | faraday-excon (1.1.0) 93 | faraday-http-cache (2.2.0) 94 | faraday (>= 0.8) 95 | faraday-net_http (1.0.1) 96 | faraday-net_http_persistent (1.1.0) 97 | ffi (1.15.0) 98 | fourflusher (2.3.1) 99 | fuzzy_match (2.0.4) 100 | gh_inspector (1.1.3) 101 | git (1.8.1) 102 | rchardet (~> 1.8) 103 | httparty (0.18.1) 104 | mime-types (~> 3.0) 105 | multi_xml (>= 0.5.2) 106 | httpclient (2.8.3) 107 | i18n (1.8.10) 108 | concurrent-ruby (~> 1.0) 109 | json (2.5.1) 110 | kramdown (2.3.1) 111 | rexml 112 | kramdown-parser-gfm (1.1.0) 113 | kramdown (~> 2.0) 114 | mime-types (3.3.1) 115 | mime-types-data (~> 3.2015) 116 | mime-types-data (3.2020.1104) 117 | minitest (5.14.4) 118 | molinillo (0.6.6) 119 | multi_xml (0.6.0) 120 | multipart-post (2.1.1) 121 | nanaimo (0.3.0) 122 | nap (1.1.0) 123 | netrc (0.11.0) 124 | no_proxy_fix (0.1.2) 125 | octokit (4.21.0) 126 | faraday (>= 0.9) 127 | sawyer (~> 0.8.0, >= 0.5.3) 128 | open4 (1.3.4) 129 | plist (3.6.0) 130 | public_suffix (4.0.6) 131 | rake (13.0.3) 132 | rchardet (1.8.0) 133 | rexml (3.2.5) 134 | ruby-macho (1.4.0) 135 | ruby2_keywords (0.0.4) 136 | sawyer (0.8.2) 137 | addressable (>= 2.3.5) 138 | faraday (> 0.8, < 2.0) 139 | terminal-table (3.0.0) 140 | unicode-display_width (~> 1.1, >= 1.1.1) 141 | thor (0.20.3) 142 | thread_safe (0.3.6) 143 | typhoeus (1.4.0) 144 | ethon (>= 0.9.0) 145 | tzinfo (1.2.9) 146 | thread_safe (~> 0.1) 147 | unicode-display_width (1.7.0) 148 | xcodeproj (1.19.0) 149 | CFPropertyList (>= 2.3.3, < 4.0) 150 | atomos (~> 0.1.3) 151 | claide (>= 1.0.2, < 2.0) 152 | colored2 (~> 3.1) 153 | nanaimo (~> 0.3.0) 154 | 155 | PLATFORMS 156 | ruby 157 | x86_64-darwin-19 158 | 159 | DEPENDENCIES 160 | cocoapods (~> 1.10.1) 161 | danger (~> 8.2.3) 162 | danger-swiftformat (~> 0.8.1) 163 | danger-swiftlint (~> 0.26.0) 164 | httparty (~> 0.18.1) 165 | plist (~> 3.6.0) 166 | 167 | BUNDLED WITH 168 | 2.2.11 169 | -------------------------------------------------------------------------------- /Tests/SpotHeroEmailValidatorTests/SpotHeroEmailValidatorTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 SpotHero, Inc. All rights reserved. 2 | 3 | @testable import SpotHeroEmailValidator 4 | import XCTest 5 | 6 | class SpotHeroEmailValidatorTests: XCTestCase { 7 | struct ValidatorTestModel { 8 | var emailAddress: String 9 | var error: SpotHeroEmailValidator.Error? 10 | var suggestion: String? 11 | } 12 | 13 | func testSyntaxValidator() { 14 | let tests = [ 15 | // Successful Examples 16 | ValidatorTestModel(emailAddress: "test@email.com", error: nil), 17 | ValidatorTestModel(emailAddress: "TEST@EMAIL.COM", error: nil), 18 | ValidatorTestModel(emailAddress: "test+-.test@email.com", error: nil), 19 | ValidatorTestModel(emailAddress: #""JohnDoe"@email.com"#, error: nil), 20 | // General Syntax Tests 21 | ValidatorTestModel(emailAddress: "test.com", error: .invalidSyntax), 22 | ValidatorTestModel(emailAddress: #"test&*\"@email.com"#, error: .invalidSyntax), 23 | ValidatorTestModel(emailAddress: #"test&*\@email.com"#, error: .invalidSyntax), 24 | ValidatorTestModel(emailAddress: "test@email.com@", error: .invalidSyntax), 25 | // Username Tests 26 | ValidatorTestModel(emailAddress: #"John..Doe@email.com"#, error: .invalidUsername), 27 | ValidatorTestModel(emailAddress: #".JohnDoe@email.com"#, error: .invalidUsername), 28 | ValidatorTestModel(emailAddress: #"JohnDoe.@email.com"#, error: .invalidUsername), 29 | ValidatorTestModel(emailAddress: "te st@email.com", error: .invalidUsername), 30 | // Domain Tests 31 | ValidatorTestModel(emailAddress: "test@.com", error: .invalidDomain), 32 | ValidatorTestModel(emailAddress: "test@com", error: .invalidDomain), 33 | ValidatorTestModel(emailAddress: "test@email+.com", error: .invalidDomain), 34 | ] 35 | 36 | let validator = SpotHeroEmailValidator.shared 37 | 38 | for test in tests { 39 | if let testError = test.error { 40 | XCTAssertThrowsError(try validator.validateSyntax(of: test.emailAddress)) { error in 41 | XCTAssertEqual(error.localizedDescription, testError.localizedDescription, "Test failed for email address: \(test.emailAddress)") 42 | } 43 | } else { 44 | XCTAssertNoThrow(try validator.validateSyntax(of: test.emailAddress), "Test failed for email address: \(test.emailAddress)") 45 | } 46 | } 47 | } 48 | 49 | func testValidEmailAddressPassesValidation() throws { 50 | let email = "test@spothero.com" 51 | 52 | let validationResult = try SpotHeroEmailValidator.shared.validateAndAutocorrect(emailAddress: email) 53 | 54 | XCTAssertTrue(validationResult.passedValidation) 55 | XCTAssertNil(validationResult.autocorrectSuggestion) 56 | } 57 | 58 | func testInvalidEmailAddressPassesValidation() throws { 59 | let email = "test@gamil.con" 60 | 61 | let validationResult = try SpotHeroEmailValidator.shared.validateAndAutocorrect(emailAddress: email) 62 | 63 | XCTAssertTrue(validationResult.passedValidation) 64 | XCTAssertNotNil(validationResult.autocorrectSuggestion) 65 | } 66 | 67 | func testEmailSuggestions() throws { 68 | let tests = [ 69 | // Emails with NO Autocorrect Suggestions 70 | ValidatorTestModel(emailAddress: "test@gmail.com", suggestion: nil), 71 | ValidatorTestModel(emailAddress: "test@yahoo.co.uk", suggestion: nil), 72 | ValidatorTestModel(emailAddress: "test@googlemail.com", suggestion: nil), 73 | 74 | // Emails with Autocorrect Suggestions 75 | ValidatorTestModel(emailAddress: "test@gamil.con", suggestion: "test@gmail.com"), 76 | ValidatorTestModel(emailAddress: "test@yaho.com.uk", suggestion: "test@yahoo.co.uk"), 77 | ValidatorTestModel(emailAddress: "test@yahooo.co.uk", suggestion: "test@yahoo.co.uk"), 78 | ValidatorTestModel(emailAddress: "test@goglemail.coj", suggestion: "test@googlemail.com"), 79 | ValidatorTestModel(emailAddress: "test@goglemail.com", suggestion: "test@googlemail.com"), 80 | 81 | // Emails with invalid syntax 82 | ValidatorTestModel(emailAddress: "blorp", error: .invalidSyntax), 83 | ] 84 | 85 | for test in tests { 86 | do { 87 | let autocorrectSuggestion = try SpotHeroEmailValidator.shared.autocorrectSuggestion(for: test.emailAddress) 88 | XCTAssertEqual(autocorrectSuggestion, test.suggestion, "Test failed for email address: \(test.emailAddress)") 89 | } catch let error as SpotHeroEmailValidator.Error { 90 | // If the test fails with an error, make sure the error was expected 91 | XCTAssertEqual(test.error, error) 92 | } catch { 93 | XCTFail("Unexpected error has occurred: \(error.localizedDescription)") 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Updated for v0.34.0 2 | 3 | # Disabled Rules 4 | disabled_rules: 5 | - number_separator # Underscores should be used as thousand separator in large decimal numbers. 6 | - todo # TODOs and FIXMEs should be resolved. 7 | - trailing_closure # Trailing closure syntax should be used whenever possible. 8 | 9 | # Enabled Rules 10 | opt_in_rules: 11 | - anyobject_protocol # Prefer using AnyObject over class for class-only protocols. 12 | - array_init # Prefer using Array(seq) over seq.map { $0 } to convert a sequence into an Array. 13 | - attributes # Attributes should be on their own lines in functions and types, but on the same line as variables and imports. 14 | - closure_end_indentation # Closure end should have the same indentation as the line that started it. 15 | - closure_spacing # Closure expressions should have a single space inside each brace. 16 | - collection_alignment # All elements in a collection literal should be vertically aligned 17 | - conditional_returns_on_newline # Conditional statements should always return on the next line 18 | - contains_over_first_not_nil # Prefer contains over first(where:) != nil 19 | - explicit_init # Explicitly calling .init() should be avoided. 20 | - empty_count # Prefer checking `isEmpty` over comparing `count` to zero. 21 | - empty_string # Prefer checking isEmpty over comparing string to an empty string literal. 22 | - empty_xctest_method # Empty XCTest method should be avoided. 23 | - fatal_error_message # A fatalError call should have a message. 24 | - file_header # Header comments should be consistent with project patterns. The SWIFTLINT_CURRENT_FILENAME placeholder can optionally be used in the required and forbidden patterns. It will be replaced by the real file name. 25 | - file_name # File name should match a type or extension declared in the file (if any). 26 | - first_where # "Prefer using `.first(where:)` over `.filter { }.first` in collections. 27 | - force_unwrapping # Force unwrapping should be avoided. 28 | - function_default_parameter_at_end # Prefer to locate parameters with defaults toward the end of the parameter list. 29 | - function_parameter_count # Number of function parameters should be low. 30 | - identical_operands # Comparing two identical operands is likely a mistake. 31 | - implicitly_unwrapped_optional # Implicitly unwrapped optionals should be avoided when possible. 32 | - joined_default_parameter # Discouraged explicit usage of the default separator. 33 | - last_where # Prefer using .last(where:) over .filter { }.last in collections. 34 | - let_var_whitespace # Let and var should be separated from other statements by a blank line. 35 | - literal_expression_end_indentation # Array and dictionary literal end should have the same indentation as the line that started it. 36 | - lower_acl_than_parent # Ensure definitions have a lower access control level than their enclosing parent 37 | #### - missing_docs # Declarations should be documented. 38 | - modifier_order # Modifier order should be consistent. 39 | - multiline_arguments # Arguments should be either on the same line, or one per line. 40 | - multiline_function_chains # Chained function calls should be either on the same line, or one per line. 41 | - multiline_parameters # Functions and methods parameters should be either on the same line, or one per line. 42 | - operator_usage_whitespace # Operators should be surrounded by a single whitespace 43 | - overridden_super_call # Some overridden methods should always call super" eg: viewDidLoad 44 | - private_action # IBActions should be private. 45 | - private_outlet # IBOutlets should be private to avoid leaking UIKit to higher layers. 46 | - prohibited_super_call # Some methods should not call super" eg: loadView 47 | - redundant_nil_coalescing # Using nil coalescing operator with nil as rhs is redundant 48 | - redundant_type_annotation # Variables should not have redundant type annotation 49 | - sorted_first_last # Prefer using min() or max() over sorted().first or sorted().last 50 | - sorted_imports # Imports should be sorted 51 | - switch_case_on_newline # Cases inside a switch should always end on a newline 52 | - toggle_bool # Prefer `someBool.toggle()` over `someBool = !someBool`. 53 | - trailing_closure # Trailing closure syntax should be used whenever possible. 54 | - unneeded_parentheses_in_closure_argument # Parentheses are not needed when declaring closure arguments. 55 | - untyped_error_in_catch # Catch statements should not declare error variables without type casting. 56 | - unused_import # All imported modules should be required to make the file compile. 57 | - vertical_parameter_alignment_on_call # Function parameters should be aligned vertically if they're in multiple lines in a method call. 58 | 59 | # Rules run by `swiftlint analyze` (experimental) 60 | analyzer_rules: 61 | - explicit_self # Instance variables and functions should be explicitly accessed with 'self.'. 62 | - unused_declaration # Declarations should be referenced at least once within all files linted. 63 | 64 | # Rule Configurations 65 | colon: 66 | apply_to_dictionaries: false 67 | cyclomatic_complexity: 68 | ignores_case_statements: true # ignores switch statements 69 | empty_count: 70 | severity: warning 71 | file_header: 72 | required_pattern: | 73 | \/\/ Copyright © \d{4} SpotHero, Inc\. All rights reserved\. 74 | file_name: 75 | excluded: ["Enums.swift"] 76 | file_length: 77 | warning: 1000 78 | error: 2000 79 | force_cast: warning 80 | force_try: warning 81 | function_body_length: 82 | warning: 100 83 | error: 200 84 | function_parameter_count: 85 | warning: 7 86 | error: 9 87 | identifier_name: 88 | allowed_symbols: 89 | - _ 90 | excluded: 91 | - id 92 | - x 93 | - y 94 | - i 95 | large_tuple: 96 | warning: 3 97 | error: 4 98 | line_length: 99 | warning: 150 100 | error: 400 101 | ignores_urls: true 102 | nesting: 103 | type_level: 104 | warning: 2 105 | statement_level: 106 | warning: 4 107 | private_outlet: 108 | allow_private_set: true 109 | trailing_comma: 110 | mandatory_comma: true 111 | trailing_whitespace: 112 | ignores_empty_lines: true 113 | type_body_length: 114 | warning: 1000 115 | error: 2000 116 | 117 | # File and Folder Exclusions 118 | excluded: 119 | # CocoaPods 120 | - Pods 121 | # Swift Package Manager 122 | - .build 123 | - .swiftpm 124 | - Tests/LinuxMain.swift 125 | - "Tests/*/XCTestManifests.swift" 126 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/SHEmailValidationTextField.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | #if canImport(UIKit) 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | public class SHEmailValidationTextField: UITextField { 9 | var suggestionView: SHAutocorrectSuggestionView? 10 | var emailValidator: SpotHeroEmailValidator? 11 | var delegateProxy: EmailTextFieldDelegate? 12 | var messageDictionary: [Int: String]? 13 | 14 | public var defaultErrorMessage: String? 15 | public var messageForSuggestion: String? 16 | public var validationError: Error? 17 | 18 | public var bubbleFillColor: UIColor? { 19 | didSet { 20 | self.suggestionView?.fillColor = self.bubbleFillColor 21 | } 22 | } 23 | 24 | public var bubbleTitleColor: UIColor? { 25 | didSet { 26 | self.suggestionView?.titleColor = self.bubbleTitleColor 27 | } 28 | } 29 | 30 | public var bubbleSuggestionColor: UIColor? { 31 | didSet { 32 | self.suggestionView?.suggestionColor = self.bubbleSuggestionColor 33 | } 34 | } 35 | 36 | override public var delegate: UITextFieldDelegate? { 37 | get { 38 | return super.delegate 39 | } 40 | set { 41 | if newValue is EmailTextFieldDelegate { 42 | super.delegate = newValue 43 | } else { 44 | self.delegateProxy?.subDelegate = newValue 45 | } 46 | } 47 | } 48 | 49 | public convenience init() { 50 | self.init(frame: .zero) 51 | } 52 | 53 | public required init?(coder: NSCoder) { 54 | super.init(coder: coder) 55 | 56 | self.initialize() 57 | } 58 | 59 | override public init(frame: CGRect) { 60 | super.init(frame: frame) 61 | 62 | self.initialize() 63 | } 64 | 65 | private func initialize() { 66 | self.delegateProxy = EmailTextFieldDelegate(target: self) 67 | self.delegate = self.delegateProxy 68 | self.emailValidator = SpotHeroEmailValidator.shared 69 | self.autocapitalizationType = .none 70 | self.keyboardType = .emailAddress 71 | self.autocorrectionType = .no 72 | self.messageDictionary = [:] 73 | self.defaultErrorMessage = "Please enter a valid email address" 74 | self.messageForSuggestion = "Did you mean" 75 | 76 | self.bubbleFillColor = SHAutocorrectSuggestionView.defaultFillColor() 77 | self.bubbleTitleColor = SHAutocorrectSuggestionView.defaultTitleColor() 78 | self.bubbleSuggestionColor = SHAutocorrectSuggestionView.defaultSuggestionColor() 79 | } 80 | 81 | public func dismissSuggestionView() { 82 | self.suggestionView?.dismiss() 83 | } 84 | 85 | public func validateInput() { 86 | guard let text = self.text, !text.isEmpty else { 87 | return 88 | } 89 | 90 | // TODO: This method can be drastically reduced, lots of redundant code 91 | do { 92 | let validationResult = try self.emailValidator?.validateAndAutocorrect(emailAddress: text) 93 | 94 | self.validationError = nil 95 | 96 | if let autocorrectSuggestion = validationResult?.autocorrectSuggestion { 97 | self.suggestionView = SHAutocorrectSuggestionView.show(from: self, 98 | title: self.messageForSuggestion, 99 | autocorrectSuggestion: autocorrectSuggestion, 100 | withSetupBlock: { [weak self] view in 101 | view?.fillColor = self?.bubbleFillColor 102 | view?.titleColor = self?.bubbleTitleColor 103 | view?.suggestionColor = self?.bubbleSuggestionColor 104 | }) 105 | 106 | self.suggestionView?.delegate = self 107 | } 108 | } catch { 109 | self.validationError = error 110 | 111 | var message = self.messageDictionary?[(error as NSError).code] 112 | 113 | if message?.isEmpty == true { 114 | message = self.defaultErrorMessage 115 | } 116 | 117 | self.suggestionView = SHAutocorrectSuggestionView.show(from: self, 118 | title: message, 119 | autocorrectSuggestion: nil, 120 | withSetupBlock: { [weak self] view in 121 | view?.fillColor = self?.bubbleFillColor 122 | view?.titleColor = self?.bubbleTitleColor 123 | view?.suggestionColor = self?.bubbleSuggestionColor 124 | }) 125 | 126 | self.suggestionView?.delegate = self 127 | } 128 | } 129 | 130 | public func setMessage(_ message: String, forErrorCode errorCode: Int) { 131 | self.messageDictionary?[errorCode] = message 132 | } 133 | } 134 | 135 | extension SHEmailValidationTextField: AutocorrectSuggestionViewDelegate { 136 | public func suggestionView(_ suggestionView: SHAutocorrectSuggestionView, wasDismissedWithAccepted accepted: Bool) { 137 | if accepted { 138 | self.text = suggestionView.suggestedText 139 | } 140 | } 141 | } 142 | 143 | #if !os(tvOS) 144 | 145 | public extension SHEmailValidationTextField { 146 | /// Must be called manually when animating a rotation so that the suggestion view is reoriented properly. 147 | func hostWillAnimateRotation(to interfaceOrientation: UIInterfaceOrientation) { 148 | self.suggestionView?.updatePosition() 149 | } 150 | } 151 | 152 | #endif 153 | 154 | #endif 155 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/SpotHeroEmailValidator.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 SpotHero, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | // TODO: Remove NSObject when entirely converted into Swift 6 | public class SpotHeroEmailValidator: NSObject { 7 | public static let shared = SpotHeroEmailValidator() 8 | 9 | private let commonTLDs: [String] 10 | private let commonDomains: [String] 11 | private let ianaRegisteredTLDs: [String] 12 | 13 | override private init() { 14 | var dataDictionary: NSDictionary? 15 | 16 | // All TLDs registered with IANA as of October 8th, 2019 at 11:28 AM CST (latest list at: http://data.iana.org/TLD/tlds-alpha-by-domain.txt) 17 | if let plistPath = Bundle.module.path(forResource: "DomainData", ofType: "plist") { 18 | dataDictionary = NSDictionary(contentsOfFile: plistPath) 19 | } 20 | 21 | self.commonDomains = dataDictionary?["CommonDomains"] as? [String] ?? [] 22 | self.commonTLDs = dataDictionary?["CommonTLDs"] as? [String] ?? [] 23 | self.ianaRegisteredTLDs = dataDictionary?["IANARegisteredTLDs"] as? [String] ?? [] 24 | } 25 | 26 | public func validateAndAutocorrect(emailAddress: String) throws -> SHValidationResult { 27 | do { 28 | // Attempt to get an autocorrect suggestion 29 | // As long as no error is thrown, we can consider the email address to have passed validation 30 | let autocorrectSuggestion = try self.autocorrectSuggestion(for: emailAddress) 31 | 32 | return SHValidationResult(passedValidation: true, 33 | autocorrectSuggestion: autocorrectSuggestion) 34 | } catch { 35 | return SHValidationResult(passedValidation: false, autocorrectSuggestion: nil) 36 | } 37 | } 38 | 39 | public func autocorrectSuggestion(for emailAddress: String) throws -> String? { 40 | // Attempt to validate the syntax of the email address 41 | // If the email address has incorrect format or syntax, an error will be thrown 42 | try self.validateSyntax(of: emailAddress) 43 | 44 | // Split the email address into its component parts 45 | let emailParts = try EmailComponents(email: emailAddress) 46 | 47 | var suggestedTLD = emailParts.tld 48 | 49 | if !self.ianaRegisteredTLDs.contains(emailParts.tld), 50 | let closestTLD = self.closestString(for: emailParts.tld, fromArray: self.commonTLDs, withTolerance: 0.5) { 51 | suggestedTLD = closestTLD 52 | } 53 | 54 | var suggestedDomain = "\(emailParts.hostname).\(suggestedTLD)" 55 | 56 | if !self.commonDomains.contains(suggestedDomain), 57 | let closestDomain = self.closestString(for: suggestedDomain, fromArray: self.commonDomains, withTolerance: 0.25) { 58 | suggestedDomain = closestDomain 59 | } 60 | 61 | let suggestedEmailAddress = "\(emailParts.username)@\(suggestedDomain)" 62 | 63 | guard suggestedEmailAddress != emailAddress else { 64 | return nil 65 | } 66 | 67 | return suggestedEmailAddress 68 | } 69 | 70 | @discardableResult 71 | public func validateSyntax(of emailAddress: String) throws -> Bool { 72 | // Split the email address into parts 73 | let emailParts = try EmailComponents(email: emailAddress) 74 | 75 | // Ensure the username is valid by itself 76 | guard emailParts.username.isValidEmailUsername() else { 77 | throw Error.invalidUsername 78 | } 79 | 80 | // Combine the hostname and TLD into the domain" 81 | let domain = "\(emailParts.hostname).\(emailParts.tld)" 82 | 83 | // Ensure the domain is valid 84 | guard domain.isValidEmailDomain() else { 85 | throw Error.invalidDomain 86 | } 87 | 88 | // Ensure that the entire email forms a syntactically valid email 89 | guard emailAddress.isValidEmail() else { 90 | throw Error.invalidSyntax 91 | } 92 | 93 | return true 94 | } 95 | 96 | // TODO: Use better name for array parameter 97 | private func closestString(for string: String, fromArray array: [String], withTolerance tolerance: Float) -> String? { 98 | guard !array.contains(string) else { 99 | return nil 100 | } 101 | 102 | var closestString: String? 103 | var closestDistance = Int.max 104 | 105 | // TODO: Use better name for arrayString parameter 106 | for arrayString in array { 107 | let distance = Int(string.levenshteinDistance(from: arrayString)) 108 | 109 | if distance < closestDistance, Float(distance) / Float(string.count) < tolerance { 110 | closestDistance = distance 111 | closestString = arrayString 112 | } 113 | } 114 | 115 | return closestString 116 | } 117 | } 118 | 119 | // MARK: - Extensions 120 | 121 | public extension SpotHeroEmailValidator { 122 | enum Error: Int, LocalizedError { 123 | case blankAddress = 1000 124 | case invalidSyntax = 1001 125 | case invalidUsername = 1002 126 | case invalidDomain = 1003 127 | 128 | public var errorDescription: String? { 129 | switch self { 130 | case .blankAddress: 131 | return "The entered email address is blank." 132 | case .invalidDomain: 133 | return "The domain name section of the entered email address is invalid." 134 | case .invalidSyntax: 135 | return "The syntax of the entered email address is invalid." 136 | case .invalidUsername: 137 | return "The username section of the entered email address is invalid." 138 | } 139 | } 140 | } 141 | } 142 | 143 | private extension String { 144 | /// RFC 5322 Official Standard Email Regex Pattern 145 | /// 146 | /// Sources: 147 | /// - [How to validate an email address using a regular expression? (Stack Overflow)](https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression) 148 | /// - [What characters are allowed in an email address? (Stack Overflow](https://stackoverflow.com/questions/2049502/what-characters-are-allowed-in-an-email-address) 149 | private static let emailRegexPattern = "\(Self.emailUsernameRegexPattern)@\(Self.emailDomainRegexPattern)" 150 | 151 | // swiftlint:disable:next line_length 152 | private static let emailUsernameRegexPattern = #"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")"# 153 | 154 | // swiftlint:disable:next line_length 155 | private static let emailDomainRegexPattern = #"(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-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])+)\])"# 156 | 157 | func isValidEmail() -> Bool { 158 | return self.lowercased().range(of: Self.emailRegexPattern, options: .regularExpression) != nil 159 | } 160 | 161 | func isValidEmailUsername() -> Bool { 162 | return !self.hasPrefix(".") 163 | && !self.hasSuffix(".") 164 | && !self.contains(" ") 165 | && (self as NSString).range(of: "..").location == NSNotFound 166 | && self.lowercased().range(of: Self.emailUsernameRegexPattern, options: .regularExpression) != nil 167 | } 168 | 169 | func isValidEmailDomain() -> Bool { 170 | return self.lowercased().range(of: Self.emailDomainRegexPattern, options: .regularExpression) != nil 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /RunScriptHelper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 34E5ABD124620FF50070A80B /* RunScriptHelper */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 34E5ABD224620FF50070A80B /* Build configuration list for PBXAggregateTarget "RunScriptHelper" */; 13 | buildPhases = ( 14 | 34E5ABD524620FFB0070A80B /* SwiftLint Script */, 15 | ); 16 | dependencies = ( 17 | ); 18 | name = RunScriptHelper; 19 | productName = RunScriptHelper; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXGroup section */ 24 | 34B63A9D234BAF7500D4F0A9 = { 25 | isa = PBXGroup; 26 | children = ( 27 | ); 28 | sourceTree = ""; 29 | }; 30 | /* End PBXGroup section */ 31 | 32 | /* Begin PBXProject section */ 33 | 34B63A9E234BAF7500D4F0A9 /* Project object */ = { 34 | isa = PBXProject; 35 | attributes = { 36 | LastUpgradeCheck = 1220; 37 | ORGANIZATIONNAME = "SpotHero, Inc."; 38 | TargetAttributes = { 39 | 34E5ABD124620FF50070A80B = { 40 | CreatedOnToolsVersion = 11.4.1; 41 | }; 42 | }; 43 | }; 44 | buildConfigurationList = 34B63AA1234BAF7500D4F0A9 /* Build configuration list for PBXProject "RunScriptHelper" */; 45 | compatibilityVersion = "Xcode 10.0"; 46 | developmentRegion = en; 47 | hasScannedForEncodings = 0; 48 | knownRegions = ( 49 | en, 50 | Base, 51 | ); 52 | mainGroup = 34B63A9D234BAF7500D4F0A9; 53 | productRefGroup = 34B63A9D234BAF7500D4F0A9; 54 | projectDirPath = ""; 55 | projectRoot = ""; 56 | targets = ( 57 | 34E5ABD124620FF50070A80B /* RunScriptHelper */, 58 | ); 59 | }; 60 | /* End PBXProject section */ 61 | 62 | /* Begin PBXShellScriptBuildPhase section */ 63 | 34E5ABD524620FFB0070A80B /* SwiftLint Script */ = { 64 | isa = PBXShellScriptBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | inputFileListPaths = ( 69 | ); 70 | inputPaths = ( 71 | ); 72 | name = "SwiftLint Script"; 73 | outputFileListPaths = ( 74 | ); 75 | outputPaths = ( 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | shellPath = /bin/sh; 79 | shellScript = "\"${SRCROOT}/scripts/swiftlint_run_script.sh\"\n"; 80 | }; 81 | /* End PBXShellScriptBuildPhase section */ 82 | 83 | /* Begin XCBuildConfiguration section */ 84 | 34B63AAD234BAF7500D4F0A9 /* Debug */ = { 85 | isa = XCBuildConfiguration; 86 | buildSettings = { 87 | ALWAYS_SEARCH_USER_PATHS = NO; 88 | CLANG_ANALYZER_NONNULL = YES; 89 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 90 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 91 | CLANG_CXX_LIBRARY = "libc++"; 92 | CLANG_ENABLE_MODULES = YES; 93 | CLANG_ENABLE_OBJC_ARC = YES; 94 | CLANG_ENABLE_OBJC_WEAK = YES; 95 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 96 | CLANG_WARN_BOOL_CONVERSION = YES; 97 | CLANG_WARN_COMMA = YES; 98 | CLANG_WARN_CONSTANT_CONVERSION = YES; 99 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 100 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 101 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 102 | CLANG_WARN_EMPTY_BODY = YES; 103 | CLANG_WARN_ENUM_CONVERSION = YES; 104 | CLANG_WARN_INFINITE_RECURSION = YES; 105 | CLANG_WARN_INT_CONVERSION = YES; 106 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 107 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 108 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 109 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 110 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 111 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 112 | CLANG_WARN_STRICT_PROTOTYPES = YES; 113 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 114 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 115 | CLANG_WARN_UNREACHABLE_CODE = YES; 116 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 117 | COPY_PHASE_STRIP = NO; 118 | CURRENT_PROJECT_VERSION = 1; 119 | DEBUG_INFORMATION_FORMAT = dwarf; 120 | ENABLE_STRICT_OBJC_MSGSEND = YES; 121 | ENABLE_TESTABILITY = YES; 122 | GCC_C_LANGUAGE_STANDARD = gnu11; 123 | GCC_DYNAMIC_NO_PIC = NO; 124 | GCC_NO_COMMON_BLOCKS = YES; 125 | GCC_OPTIMIZATION_LEVEL = 0; 126 | GCC_PREPROCESSOR_DEFINITIONS = ( 127 | "DEBUG=1", 128 | "$(inherited)", 129 | ); 130 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 131 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 132 | GCC_WARN_UNDECLARED_SELECTOR = YES; 133 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 134 | GCC_WARN_UNUSED_FUNCTION = YES; 135 | GCC_WARN_UNUSED_VARIABLE = YES; 136 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 137 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 138 | MTL_FAST_MATH = YES; 139 | ONLY_ACTIVE_ARCH = YES; 140 | SDKROOT = iphoneos; 141 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 142 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 143 | VERSIONING_SYSTEM = "apple-generic"; 144 | VERSION_INFO_PREFIX = ""; 145 | }; 146 | name = Debug; 147 | }; 148 | 34B63AAE234BAF7500D4F0A9 /* Release */ = { 149 | isa = XCBuildConfiguration; 150 | buildSettings = { 151 | ALWAYS_SEARCH_USER_PATHS = NO; 152 | CLANG_ANALYZER_NONNULL = YES; 153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 155 | CLANG_CXX_LIBRARY = "libc++"; 156 | CLANG_ENABLE_MODULES = YES; 157 | CLANG_ENABLE_OBJC_ARC = YES; 158 | CLANG_ENABLE_OBJC_WEAK = YES; 159 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 160 | CLANG_WARN_BOOL_CONVERSION = YES; 161 | CLANG_WARN_COMMA = YES; 162 | CLANG_WARN_CONSTANT_CONVERSION = YES; 163 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 164 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 165 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 166 | CLANG_WARN_EMPTY_BODY = YES; 167 | CLANG_WARN_ENUM_CONVERSION = YES; 168 | CLANG_WARN_INFINITE_RECURSION = YES; 169 | CLANG_WARN_INT_CONVERSION = YES; 170 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 171 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 172 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 173 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 174 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 175 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 176 | CLANG_WARN_STRICT_PROTOTYPES = YES; 177 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 178 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 179 | CLANG_WARN_UNREACHABLE_CODE = YES; 180 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 181 | COPY_PHASE_STRIP = NO; 182 | CURRENT_PROJECT_VERSION = 1; 183 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 184 | ENABLE_NS_ASSERTIONS = NO; 185 | ENABLE_STRICT_OBJC_MSGSEND = YES; 186 | GCC_C_LANGUAGE_STANDARD = gnu11; 187 | GCC_NO_COMMON_BLOCKS = YES; 188 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 189 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 190 | GCC_WARN_UNDECLARED_SELECTOR = YES; 191 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 192 | GCC_WARN_UNUSED_FUNCTION = YES; 193 | GCC_WARN_UNUSED_VARIABLE = YES; 194 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 195 | MTL_ENABLE_DEBUG_INFO = NO; 196 | MTL_FAST_MATH = YES; 197 | SDKROOT = iphoneos; 198 | SWIFT_COMPILATION_MODE = wholemodule; 199 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 200 | VALIDATE_PRODUCT = YES; 201 | VERSIONING_SYSTEM = "apple-generic"; 202 | VERSION_INFO_PREFIX = ""; 203 | }; 204 | name = Release; 205 | }; 206 | 34E5ABD324620FF50070A80B /* Debug */ = { 207 | isa = XCBuildConfiguration; 208 | buildSettings = { 209 | CODE_SIGN_STYLE = Automatic; 210 | DEVELOPMENT_TEAM = ZW7QWR628D; 211 | PRODUCT_NAME = "$(TARGET_NAME)"; 212 | }; 213 | name = Debug; 214 | }; 215 | 34E5ABD424620FF50070A80B /* Release */ = { 216 | isa = XCBuildConfiguration; 217 | buildSettings = { 218 | CODE_SIGN_STYLE = Automatic; 219 | DEVELOPMENT_TEAM = ZW7QWR628D; 220 | PRODUCT_NAME = "$(TARGET_NAME)"; 221 | }; 222 | name = Release; 223 | }; 224 | /* End XCBuildConfiguration section */ 225 | 226 | /* Begin XCConfigurationList section */ 227 | 34B63AA1234BAF7500D4F0A9 /* Build configuration list for PBXProject "RunScriptHelper" */ = { 228 | isa = XCConfigurationList; 229 | buildConfigurations = ( 230 | 34B63AAD234BAF7500D4F0A9 /* Debug */, 231 | 34B63AAE234BAF7500D4F0A9 /* Release */, 232 | ); 233 | defaultConfigurationIsVisible = 0; 234 | defaultConfigurationName = Release; 235 | }; 236 | 34E5ABD224620FF50070A80B /* Build configuration list for PBXAggregateTarget "RunScriptHelper" */ = { 237 | isa = XCConfigurationList; 238 | buildConfigurations = ( 239 | 34E5ABD324620FF50070A80B /* Debug */, 240 | 34E5ABD424620FF50070A80B /* Release */, 241 | ); 242 | defaultConfigurationIsVisible = 0; 243 | defaultConfigurationName = Release; 244 | }; 245 | /* End XCConfigurationList section */ 246 | }; 247 | rootObject = 34B63A9E234BAF7500D4F0A9 /* Project object */; 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/SHAutocorrectSuggestionView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SpotHero, Inc. All rights reserved. 2 | 3 | #if canImport(UIKit) 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | /// Completion block for when the autocorrect suggestion view is shown. 9 | public typealias SetupBlock = (SHAutocorrectSuggestionView?) -> Void 10 | 11 | // Obj-C code is documented above every call or signature for ease of maintenance and identifying any escaped defects. 12 | // As we refactor the behavior and logic and feel more confident in the translation to Swift, we'll remove the commented code blocks. 13 | 14 | public class SHAutocorrectSuggestionView: UIView { 15 | private static let cornerRadius: CGFloat = 6 16 | private static let arrowHeight: CGFloat = 12 17 | private static let arrowWidth: CGFloat = 8 18 | private static let maxWidth: CGFloat = 240 19 | private static let dismissButtonWidth: CGFloat = 30 20 | 21 | weak var delegate: AutocorrectSuggestionViewDelegate? 22 | 23 | public var suggestedText: String? 24 | public var fillColor: UIColor? 25 | public var titleColor: UIColor? 26 | public var suggestionColor: UIColor? 27 | 28 | private var target: UIView? 29 | private var titleRect: CGRect? 30 | private var suggestionRect: CGRect? 31 | 32 | private let titleFont: UIFont 33 | private let suggestionFont: UIFont 34 | private let title: String? 35 | 36 | public static func show(from target: UIView, 37 | inContainerView container: UIView?, 38 | title: String?, 39 | autocorrectSuggestion suggestion: String?, 40 | withSetupBlock block: SetupBlock?) -> SHAutocorrectSuggestionView { 41 | let suggestionView = SHAutocorrectSuggestionView(target: target, 42 | title: title, 43 | autocorrectSuggestion: suggestion, 44 | withSetupBlock: block) 45 | 46 | suggestionView.show(from: target, inContainerView: container) 47 | 48 | return suggestionView 49 | } 50 | 51 | public static func show(from target: UIView, 52 | title: String?, 53 | autocorrectSuggestion suggestion: String?, 54 | withSetupBlock block: SetupBlock?) -> SHAutocorrectSuggestionView { 55 | return SHAutocorrectSuggestionView.show(from: target, 56 | inContainerView: target.superview, 57 | title: title, 58 | autocorrectSuggestion: suggestion, 59 | withSetupBlock: block) 60 | } 61 | 62 | public static func defaultFillColor() -> UIColor { 63 | return .black 64 | } 65 | 66 | public static func defaultTitleColor() -> UIColor { 67 | return .white 68 | } 69 | 70 | public static func defaultSuggestionColor() -> UIColor { 71 | return UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 1.0) 72 | } 73 | 74 | public init(target: UIView, title: String?, autocorrectSuggestion suggestion: String?, withSetupBlock block: SetupBlock?) { 75 | self.title = title 76 | self.suggestedText = suggestion 77 | self.titleFont = UIFont.boldSystemFont(ofSize: 13) 78 | self.suggestionFont = UIFont.boldSystemFont(ofSize: 13) 79 | 80 | super.init(frame: .zero) 81 | 82 | let paragraphTitleStyle = NSMutableParagraphStyle() 83 | paragraphTitleStyle.lineBreakMode = .byWordWrapping 84 | paragraphTitleStyle.alignment = .left 85 | 86 | let paragraphSuggestedStyle = NSMutableParagraphStyle() 87 | paragraphSuggestedStyle.lineBreakMode = .byCharWrapping 88 | paragraphSuggestedStyle.alignment = .left 89 | 90 | let titleSizeRect = title?.boundingRect(with: CGSize(width: Self.maxWidth - Self.dismissButtonWidth, 91 | height: CGFloat.greatestFiniteMagnitude), 92 | options: .usesLineFragmentOrigin, 93 | attributes: [ 94 | .font: self.titleFont, 95 | .paragraphStyle: paragraphTitleStyle, 96 | .foregroundColor: UIColor.white, 97 | ], 98 | context: nil) 99 | 100 | let suggestionSizeRect = suggestion?.boundingRect(with: CGSize(width: Self.maxWidth - Self.dismissButtonWidth, 101 | height: CGFloat.greatestFiniteMagnitude), 102 | options: .usesLineFragmentOrigin, 103 | attributes: [ 104 | .font: self.suggestionFont, 105 | .paragraphStyle: paragraphSuggestedStyle, 106 | .foregroundColor: Self.defaultSuggestionColor(), 107 | ], 108 | context: nil) 109 | 110 | guard 111 | let titleSize = titleSizeRect?.size, 112 | let suggestionSize = suggestionSizeRect?.size else { 113 | return 114 | } 115 | 116 | let width = max(titleSize.width, suggestionSize.width) + Self.dismissButtonWidth + (Self.cornerRadius * 2) 117 | let height = titleSize.height + suggestionSize.height + Self.arrowHeight + (Self.cornerRadius * 2) 118 | let left = max(10, target.center.x - (width / 2)) 119 | let top = target.frame.origin.y - height + 4 120 | 121 | self.frame = CGRect(x: left, y: top, width: width, height: height).integral 122 | self.isOpaque = false 123 | 124 | self.titleRect = CGRect(x: (width - Self.dismissButtonWidth - titleSize.width) / 2, 125 | y: Self.cornerRadius, 126 | width: titleSize.width, 127 | height: titleSize.height) 128 | 129 | self.suggestionRect = CGRect(x: Self.cornerRadius, 130 | y: Self.cornerRadius + titleSize.height, 131 | width: suggestionSize.width, 132 | height: suggestionSize.height) 133 | 134 | block?(self) 135 | 136 | self.fillColor = self.fillColor ?? Self.defaultFillColor() 137 | 138 | self.titleColor = self.titleColor ?? Self.defaultTitleColor() 139 | 140 | self.suggestionColor = self.suggestionColor ?? Self.defaultSuggestionColor() 141 | } 142 | 143 | @available(*, unavailable) 144 | required init?(coder: NSCoder) { 145 | fatalError("init(coder:) has not been implemented") 146 | } 147 | 148 | override public func draw(_ rect: CGRect) { 149 | let contentSize = CGSize(width: self.bounds.size.width, height: self.bounds.size.height - Self.arrowHeight) 150 | let arrowBottom = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height) 151 | 152 | let path = CGMutablePath() 153 | 154 | path.move(to: CGPoint(x: arrowBottom.x, y: arrowBottom.y)) 155 | path.addLine(to: CGPoint(x: arrowBottom.x - Self.arrowWidth, y: arrowBottom.y - Self.arrowHeight)) 156 | 157 | path.addArc(tangent1End: CGPoint(x: 0, y: contentSize.height), 158 | tangent2End: CGPoint(x: 0, y: contentSize.height - Self.cornerRadius), 159 | radius: Self.cornerRadius) 160 | path.addArc(tangent1End: CGPoint(x: 0, y: 0), 161 | tangent2End: CGPoint(x: Self.cornerRadius, y: 0), 162 | radius: Self.cornerRadius) 163 | path.addArc(tangent1End: CGPoint(x: contentSize.width, y: 0), 164 | tangent2End: CGPoint(x: contentSize.width, y: Self.cornerRadius), 165 | radius: Self.cornerRadius) 166 | path.addArc(tangent1End: CGPoint(x: contentSize.width, y: contentSize.height), 167 | tangent2End: CGPoint(x: contentSize.width - Self.cornerRadius, y: contentSize.height), 168 | radius: Self.cornerRadius) 169 | 170 | path.addLine(to: CGPoint(x: arrowBottom.x + Self.arrowWidth, y: arrowBottom.y - Self.arrowHeight)) 171 | 172 | path.closeSubpath() 173 | 174 | guard let context = UIGraphicsGetCurrentContext() else { 175 | return 176 | } 177 | 178 | context.saveGState() 179 | 180 | context.addPath(path) 181 | context.clip() 182 | 183 | let fillColor = self.fillColor ?? Self.defaultFillColor() 184 | 185 | context.setFillColor(fillColor.cgColor) 186 | context.fill(bounds) 187 | 188 | context.restoreGState() 189 | 190 | let separatorX = contentSize.width - Self.dismissButtonWidth 191 | context.setStrokeColor(UIColor.gray.cgColor) 192 | context.setLineWidth(1) 193 | context.move(to: CGPoint(x: separatorX, y: 0)) 194 | context.addLine(to: CGPoint(x: separatorX, y: contentSize.height)) 195 | context.strokePath() 196 | 197 | let xSize: CGFloat = 12 198 | 199 | context.setLineWidth(4) 200 | 201 | context.move(to: CGPoint(x: separatorX + (Self.dismissButtonWidth - xSize) / 2, y: (contentSize.height - xSize) / 2)) 202 | context.addLine(to: CGPoint(x: separatorX + (Self.dismissButtonWidth + xSize) / 2, y: (contentSize.height + xSize) / 2)) 203 | context.strokePath() 204 | 205 | context.move(to: CGPoint(x: separatorX + (Self.dismissButtonWidth - xSize) / 2, y: (contentSize.height + xSize) / 2)) 206 | context.addLine(to: CGPoint(x: separatorX + (Self.dismissButtonWidth + xSize) / 2, y: (contentSize.height - xSize) / 2)) 207 | context.strokePath() 208 | 209 | let paragraphTitleStyle = NSMutableParagraphStyle() 210 | paragraphTitleStyle.lineBreakMode = .byWordWrapping 211 | paragraphTitleStyle.alignment = .center 212 | 213 | let paragraphSuggestedStyle = NSMutableParagraphStyle() 214 | paragraphSuggestedStyle.lineBreakMode = .byCharWrapping 215 | paragraphSuggestedStyle.alignment = .left 216 | 217 | if let title = self.title, let titleRect = self.titleRect { 218 | self.titleColor?.set() 219 | 220 | title.draw(in: titleRect, withAttributes: [ 221 | .font: self.titleFont, 222 | .paragraphStyle: paragraphTitleStyle, 223 | .foregroundColor: UIColor.white, 224 | ]) 225 | } 226 | 227 | if let suggestedText = self.suggestedText, let suggestionRect = self.suggestionRect { 228 | self.suggestionColor?.set() 229 | 230 | suggestedText.draw(in: suggestionRect, withAttributes: [ 231 | .font: self.suggestionFont, 232 | .paragraphStyle: paragraphSuggestedStyle, 233 | .foregroundColor: Self.defaultSuggestionColor(), 234 | ]) 235 | } 236 | } 237 | 238 | override public func touchesEnded(_ touches: Set, with event: UIEvent?) { 239 | guard touches.count == 1 else { 240 | return 241 | } 242 | 243 | guard let touchPoint = touches.first?.location(in: self) else { 244 | return 245 | } 246 | 247 | let viewSize = self.bounds.size 248 | 249 | guard touchPoint.x >= 0, touchPoint.x < viewSize.width, touchPoint.y >= 0, touchPoint.y < viewSize.height - Self.arrowHeight else { 250 | return 251 | } 252 | 253 | let wasDismissedWithAccepted = touchPoint.x <= viewSize.width - Self.dismissButtonWidth && self.suggestedText != nil 254 | 255 | self.delegate?.suggestionView(self, wasDismissedWithAccepted: wasDismissedWithAccepted) 256 | self.dismiss() 257 | } 258 | 259 | public func show(from target: UIView, inContainerView container: UIView?) { 260 | self.target = target 261 | 262 | self.alpha = 0.2 263 | self.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) 264 | 265 | self.frame = target.superview?.convert(self.frame, to: container) ?? .zero 266 | container?.addSubview(self) 267 | 268 | UIView.animate( 269 | withDuration: 0.2, 270 | animations: { 271 | self.alpha = 1 272 | self.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) 273 | }, 274 | completion: { _ in 275 | UIView.animate(withDuration: 0.1, animations: { 276 | self.transform = .identity 277 | }) 278 | } 279 | ) 280 | } 281 | 282 | public func updatePosition() { 283 | guard 284 | let target = self.target, 285 | let targetSuperview = target.superview else { 286 | return 287 | } 288 | 289 | let width = self.bounds.size.width 290 | let height = self.bounds.size.height 291 | let left = max(10, target.center.x - (width / 2)) 292 | let top = target.frame.origin.y - height 293 | 294 | self.frame = targetSuperview.convert(CGRect(x: left, y: top, width: width, height: height), to: self.superview).integral 295 | } 296 | 297 | public func dismiss() { 298 | UIView.animate( 299 | withDuration: 0.1, 300 | animations: { 301 | self.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) 302 | }, 303 | completion: { _ in 304 | UIView.animate( 305 | withDuration: 0.2, 306 | animations: { 307 | self.alpha = 0.2 308 | self.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) 309 | }, 310 | completion: { _ in 311 | self.removeFromSuperview() 312 | self.target = nil 313 | } 314 | ) 315 | } 316 | ) 317 | } 318 | } 319 | 320 | #endif 321 | -------------------------------------------------------------------------------- /SpotHeroEmailValidatorDemo/SpotHeroEmailValidatorDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 34954FC5235431A800C8580B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34954FC4235431A800C8580B /* AppDelegate.swift */; }; 11 | 34954FC7235431A800C8580B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34954FC6235431A800C8580B /* SceneDelegate.swift */; }; 12 | 34954FC9235431A800C8580B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34954FC8235431A800C8580B /* ViewController.swift */; }; 13 | 34954FCC235431A800C8580B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 34954FCA235431A800C8580B /* Main.storyboard */; }; 14 | 34954FCE235431A800C8580B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 34954FCD235431A800C8580B /* Assets.xcassets */; }; 15 | 34954FD1235431A800C8580B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 34954FCF235431A800C8580B /* LaunchScreen.storyboard */; }; 16 | 34C967FD258169BD009DDF4B /* SpotHeroEmailValidator in Frameworks */ = {isa = PBXBuildFile; productRef = 34C967FC258169BD009DDF4B /* SpotHeroEmailValidator */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 344977B5234C344E00D71628 /* SpotHeroEmailValidator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SpotHeroEmailValidator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 34954FC2235431A800C8580B /* SpotHeroEmailValidatorDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpotHeroEmailValidatorDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 34954FC4235431A800C8580B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 34954FC6235431A800C8580B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 24 | 34954FC8235431A800C8580B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 34954FCB235431A800C8580B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 34954FCD235431A800C8580B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 34954FD0235431A800C8580B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 34954FD2235431A800C8580B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 34954FD82354366600C8580B /* SpotHeroEmailValidator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SpotHeroEmailValidator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 34954FBF235431A800C8580B /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 34C967FD258169BD009DDF4B /* SpotHeroEmailValidator in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 3449778C234C337B00D71628 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 34954FC3235431A800C8580B /* SpotHeroEmailValidatorDemo */, 48 | 34497796234C337B00D71628 /* Products */, 49 | 344977B4234C344E00D71628 /* Frameworks */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | 34497796234C337B00D71628 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 34954FC2235431A800C8580B /* SpotHeroEmailValidatorDemo.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | 344977B4234C344E00D71628 /* Frameworks */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 34954FD82354366600C8580B /* SpotHeroEmailValidator.framework */, 65 | 344977B5234C344E00D71628 /* SpotHeroEmailValidator.framework */, 66 | ); 67 | name = Frameworks; 68 | sourceTree = ""; 69 | }; 70 | 34954FC3235431A800C8580B /* SpotHeroEmailValidatorDemo */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 34954FD2235431A800C8580B /* Info.plist */, 74 | 34954FC4235431A800C8580B /* AppDelegate.swift */, 75 | 34954FC6235431A800C8580B /* SceneDelegate.swift */, 76 | 34954FC8235431A800C8580B /* ViewController.swift */, 77 | 34954FCA235431A800C8580B /* Main.storyboard */, 78 | 34954FD62354320200C8580B /* Resources */, 79 | ); 80 | path = SpotHeroEmailValidatorDemo; 81 | sourceTree = ""; 82 | }; 83 | 34954FD62354320200C8580B /* Resources */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 34954FCD235431A800C8580B /* Assets.xcassets */, 87 | 34954FCF235431A800C8580B /* LaunchScreen.storyboard */, 88 | ); 89 | path = Resources; 90 | sourceTree = ""; 91 | }; 92 | /* End PBXGroup section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | 34954FC1235431A800C8580B /* SpotHeroEmailValidatorDemo */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = 34954FD5235431A800C8580B /* Build configuration list for PBXNativeTarget "SpotHeroEmailValidatorDemo" */; 98 | buildPhases = ( 99 | 34954FBE235431A800C8580B /* Sources */, 100 | 34954FBF235431A800C8580B /* Frameworks */, 101 | 34954FC0235431A800C8580B /* Resources */, 102 | ); 103 | buildRules = ( 104 | ); 105 | dependencies = ( 106 | ); 107 | name = SpotHeroEmailValidatorDemo; 108 | packageProductDependencies = ( 109 | 34C967FC258169BD009DDF4B /* SpotHeroEmailValidator */, 110 | ); 111 | productName = SpotHeroEmailValidatorSwiftDemo; 112 | productReference = 34954FC2235431A800C8580B /* SpotHeroEmailValidatorDemo.app */; 113 | productType = "com.apple.product-type.application"; 114 | }; 115 | /* End PBXNativeTarget section */ 116 | 117 | /* Begin PBXProject section */ 118 | 3449778D234C337B00D71628 /* Project object */ = { 119 | isa = PBXProject; 120 | attributes = { 121 | LastSwiftUpdateCheck = 1110; 122 | LastUpgradeCheck = 1220; 123 | ORGANIZATIONNAME = "SpotHero, Inc."; 124 | TargetAttributes = { 125 | 34954FC1235431A800C8580B = { 126 | CreatedOnToolsVersion = 11.1; 127 | }; 128 | }; 129 | }; 130 | buildConfigurationList = 34497790234C337B00D71628 /* Build configuration list for PBXProject "SpotHeroEmailValidatorDemo" */; 131 | compatibilityVersion = "Xcode 10.0"; 132 | developmentRegion = en; 133 | hasScannedForEncodings = 0; 134 | knownRegions = ( 135 | en, 136 | Base, 137 | ); 138 | mainGroup = 3449778C234C337B00D71628; 139 | productRefGroup = 34497796234C337B00D71628 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | 34954FC1235431A800C8580B /* SpotHeroEmailValidatorDemo */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | 34954FC0235431A800C8580B /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 34954FD1235431A800C8580B /* LaunchScreen.storyboard in Resources */, 154 | 34954FCE235431A800C8580B /* Assets.xcassets in Resources */, 155 | 34954FCC235431A800C8580B /* Main.storyboard in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXSourcesBuildPhase section */ 162 | 34954FBE235431A800C8580B /* Sources */ = { 163 | isa = PBXSourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | 34954FC9235431A800C8580B /* ViewController.swift in Sources */, 167 | 34954FC5235431A800C8580B /* AppDelegate.swift in Sources */, 168 | 34954FC7235431A800C8580B /* SceneDelegate.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin PBXVariantGroup section */ 175 | 34954FCA235431A800C8580B /* Main.storyboard */ = { 176 | isa = PBXVariantGroup; 177 | children = ( 178 | 34954FCB235431A800C8580B /* Base */, 179 | ); 180 | name = Main.storyboard; 181 | sourceTree = ""; 182 | }; 183 | 34954FCF235431A800C8580B /* LaunchScreen.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | 34954FD0235431A800C8580B /* Base */, 187 | ); 188 | name = LaunchScreen.storyboard; 189 | sourceTree = ""; 190 | }; 191 | /* End PBXVariantGroup section */ 192 | 193 | /* Begin XCBuildConfiguration section */ 194 | 344977AC234C337B00D71628 /* Debug */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 201 | CLANG_CXX_LIBRARY = "libc++"; 202 | CLANG_ENABLE_MODULES = YES; 203 | CLANG_ENABLE_OBJC_ARC = YES; 204 | CLANG_ENABLE_OBJC_WEAK = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INFINITE_RECURSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = dwarf; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_TESTABILITY = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu11; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 247 | MTL_FAST_MATH = YES; 248 | ONLY_ACTIVE_ARCH = YES; 249 | SDKROOT = iphoneos; 250 | }; 251 | name = Debug; 252 | }; 253 | 344977AD234C337B00D71628 /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 281 | CLANG_WARN_STRICT_PROTOTYPES = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 284 | CLANG_WARN_UNREACHABLE_CODE = YES; 285 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 288 | ENABLE_NS_ASSERTIONS = NO; 289 | ENABLE_STRICT_OBJC_MSGSEND = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu11; 291 | GCC_NO_COMMON_BLOCKS = YES; 292 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 293 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 294 | GCC_WARN_UNDECLARED_SELECTOR = YES; 295 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 296 | GCC_WARN_UNUSED_FUNCTION = YES; 297 | GCC_WARN_UNUSED_VARIABLE = YES; 298 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 299 | MTL_ENABLE_DEBUG_INFO = NO; 300 | MTL_FAST_MATH = YES; 301 | SDKROOT = iphoneos; 302 | VALIDATE_PRODUCT = YES; 303 | }; 304 | name = Release; 305 | }; 306 | 34954FD3235431A800C8580B /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CODE_SIGN_STYLE = Automatic; 311 | DEVELOPMENT_TEAM = ZW7QWR628D; 312 | INFOPLIST_FILE = SpotHeroEmailValidatorDemo/Info.plist; 313 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 314 | LD_RUNPATH_SEARCH_PATHS = ( 315 | "$(inherited)", 316 | "@executable_path/Frameworks", 317 | ); 318 | PRODUCT_BUNDLE_IDENTIFIER = com.spothero.SpotHeroEmailValidatorDemo; 319 | PRODUCT_NAME = SpotHeroEmailValidatorDemo; 320 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | SWIFT_VERSION = 5.0; 323 | TARGETED_DEVICE_FAMILY = "1,2"; 324 | }; 325 | name = Debug; 326 | }; 327 | 34954FD4235431A800C8580B /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | CODE_SIGN_STYLE = Automatic; 332 | DEVELOPMENT_TEAM = ZW7QWR628D; 333 | INFOPLIST_FILE = SpotHeroEmailValidatorDemo/Info.plist; 334 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 335 | LD_RUNPATH_SEARCH_PATHS = ( 336 | "$(inherited)", 337 | "@executable_path/Frameworks", 338 | ); 339 | PRODUCT_BUNDLE_IDENTIFIER = com.spothero.SpotHeroEmailValidatorDemo; 340 | PRODUCT_NAME = SpotHeroEmailValidatorDemo; 341 | SWIFT_COMPILATION_MODE = wholemodule; 342 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 343 | SWIFT_VERSION = 5.0; 344 | TARGETED_DEVICE_FAMILY = "1,2"; 345 | }; 346 | name = Release; 347 | }; 348 | /* End XCBuildConfiguration section */ 349 | 350 | /* Begin XCConfigurationList section */ 351 | 34497790234C337B00D71628 /* Build configuration list for PBXProject "SpotHeroEmailValidatorDemo" */ = { 352 | isa = XCConfigurationList; 353 | buildConfigurations = ( 354 | 344977AC234C337B00D71628 /* Debug */, 355 | 344977AD234C337B00D71628 /* Release */, 356 | ); 357 | defaultConfigurationIsVisible = 0; 358 | defaultConfigurationName = Release; 359 | }; 360 | 34954FD5235431A800C8580B /* Build configuration list for PBXNativeTarget "SpotHeroEmailValidatorDemo" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 34954FD3235431A800C8580B /* Debug */, 364 | 34954FD4235431A800C8580B /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | /* End XCConfigurationList section */ 370 | 371 | /* Begin XCSwiftPackageProductDependency section */ 372 | 34C967FC258169BD009DDF4B /* SpotHeroEmailValidator */ = { 373 | isa = XCSwiftPackageProductDependency; 374 | productName = SpotHeroEmailValidator; 375 | }; 376 | /* End XCSwiftPackageProductDependency section */ 377 | }; 378 | rootObject = 3449778D234C337B00D71628 /* Project object */; 379 | } 380 | -------------------------------------------------------------------------------- /Sources/SpotHeroEmailValidator/Data/DomainData.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommonDomains 6 | 7 | gmail.com 8 | googlemail.com 9 | yahoo.com 10 | yahoo.co.uk 11 | yahoo.co.in 12 | yahoo.ca 13 | ymail.com 14 | hotmail.com 15 | hotmail.co.uk 16 | msn.com 17 | live.com 18 | outlook.com 19 | comcast.net 20 | sbcglobal.net 21 | bellsouth.net 22 | verizon.net 23 | earthlink.net 24 | cox.net 25 | rediffmail.com 26 | charter.net 27 | facebook.com 28 | mail.com 29 | gmx.com 30 | aol.com 31 | att.net 32 | mac.com 33 | rocketmail.com 34 | 35 | CommonTLDs 36 | 37 | com 38 | net 39 | ru 40 | org 41 | de 42 | uk 43 | jp 44 | ca 45 | fr 46 | au 47 | br 48 | us 49 | info 50 | cn 51 | dk 52 | edu 53 | gov 54 | mil 55 | ch 56 | it 57 | nl 58 | se 59 | no 60 | es 61 | me 62 | 63 | IANARegisteredTLDs 64 | 65 | aaa 66 | aarp 67 | abarth 68 | abb 69 | abbott 70 | abbvie 71 | abc 72 | able 73 | abogado 74 | abudhabi 75 | ac 76 | academy 77 | accenture 78 | accountant 79 | accountants 80 | aco 81 | actor 82 | ad 83 | adac 84 | ads 85 | adult 86 | ae 87 | aeg 88 | aero 89 | aetna 90 | af 91 | afamilycompany 92 | afl 93 | africa 94 | ag 95 | agakhan 96 | agency 97 | ai 98 | aig 99 | airbus 100 | airforce 101 | airtel 102 | akdn 103 | al 104 | alfaromeo 105 | alibaba 106 | alipay 107 | allfinanz 108 | allstate 109 | ally 110 | alsace 111 | alstom 112 | am 113 | amazon 114 | americanexpress 115 | americanfamily 116 | amex 117 | amfam 118 | amica 119 | amsterdam 120 | analytics 121 | android 122 | anquan 123 | anz 124 | ao 125 | aol 126 | apartments 127 | app 128 | apple 129 | aq 130 | aquarelle 131 | ar 132 | arab 133 | aramco 134 | archi 135 | army 136 | arpa 137 | art 138 | arte 139 | as 140 | asda 141 | asia 142 | associates 143 | at 144 | athleta 145 | attorney 146 | au 147 | auction 148 | audi 149 | audible 150 | audio 151 | auspost 152 | author 153 | auto 154 | autos 155 | avianca 156 | aw 157 | aws 158 | ax 159 | axa 160 | az 161 | azure 162 | ba 163 | baby 164 | baidu 165 | banamex 166 | bananarepublic 167 | band 168 | bank 169 | bar 170 | barcelona 171 | barclaycard 172 | barclays 173 | barefoot 174 | bargains 175 | baseball 176 | basketball 177 | bauhaus 178 | bayern 179 | bb 180 | bbc 181 | bbt 182 | bbva 183 | bcg 184 | bcn 185 | bd 186 | be 187 | beats 188 | beauty 189 | beer 190 | bentley 191 | berlin 192 | best 193 | bestbuy 194 | bet 195 | bf 196 | bg 197 | bh 198 | bharti 199 | bi 200 | bible 201 | bid 202 | bike 203 | bing 204 | bingo 205 | bio 206 | biz 207 | bj 208 | black 209 | blackfriday 210 | blockbuster 211 | blog 212 | bloomberg 213 | blue 214 | bm 215 | bms 216 | bmw 217 | bn 218 | bnpparibas 219 | bo 220 | boats 221 | boehringer 222 | bofa 223 | bom 224 | bond 225 | boo 226 | book 227 | booking 228 | bosch 229 | bostik 230 | boston 231 | bot 232 | boutique 233 | box 234 | br 235 | bradesco 236 | bridgestone 237 | broadway 238 | broker 239 | brother 240 | brussels 241 | bs 242 | bt 243 | budapest 244 | bugatti 245 | build 246 | builders 247 | business 248 | buy 249 | buzz 250 | bv 251 | bw 252 | by 253 | bz 254 | bzh 255 | ca 256 | cab 257 | cafe 258 | cal 259 | call 260 | calvinklein 261 | cam 262 | camera 263 | camp 264 | cancerresearch 265 | canon 266 | capetown 267 | capital 268 | capitalone 269 | car 270 | caravan 271 | cards 272 | care 273 | career 274 | careers 275 | cars 276 | casa 277 | case 278 | cash 279 | casino 280 | cat 281 | catering 282 | catholic 283 | cba 284 | cbn 285 | cbre 286 | cbs 287 | cc 288 | cd 289 | center 290 | ceo 291 | cern 292 | cf 293 | cfa 294 | cfd 295 | cg 296 | ch 297 | chanel 298 | channel 299 | charity 300 | chase 301 | chat 302 | cheap 303 | chintai 304 | christmas 305 | chrome 306 | church 307 | ci 308 | cipriani 309 | circle 310 | cisco 311 | citadel 312 | citi 313 | citic 314 | city 315 | cityeats 316 | ck 317 | cl 318 | claims 319 | cleaning 320 | click 321 | clinic 322 | clinique 323 | clothing 324 | cloud 325 | club 326 | clubmed 327 | cm 328 | cn 329 | co 330 | coach 331 | codes 332 | coffee 333 | college 334 | cologne 335 | com 336 | comcast 337 | commbank 338 | community 339 | company 340 | compare 341 | computer 342 | comsec 343 | condos 344 | construction 345 | consulting 346 | contact 347 | contractors 348 | cooking 349 | cookingchannel 350 | cool 351 | coop 352 | corsica 353 | country 354 | coupon 355 | coupons 356 | courses 357 | cpa 358 | cr 359 | credit 360 | creditcard 361 | creditunion 362 | cricket 363 | crown 364 | crs 365 | cruise 366 | cruises 367 | csc 368 | cu 369 | cuisinella 370 | cv 371 | cw 372 | cx 373 | cy 374 | cymru 375 | cyou 376 | cz 377 | dabur 378 | dad 379 | dance 380 | data 381 | date 382 | dating 383 | datsun 384 | day 385 | dclk 386 | dds 387 | de 388 | deal 389 | dealer 390 | deals 391 | degree 392 | delivery 393 | dell 394 | deloitte 395 | delta 396 | democrat 397 | dental 398 | dentist 399 | desi 400 | design 401 | dev 402 | dhl 403 | diamonds 404 | diet 405 | digital 406 | direct 407 | directory 408 | discount 409 | discover 410 | dish 411 | diy 412 | dj 413 | dk 414 | dm 415 | dnp 416 | do 417 | docs 418 | doctor 419 | dog 420 | domains 421 | dot 422 | download 423 | drive 424 | dtv 425 | dubai 426 | duck 427 | dunlop 428 | dupont 429 | durban 430 | dvag 431 | dvr 432 | dz 433 | earth 434 | eat 435 | ec 436 | eco 437 | edeka 438 | edu 439 | education 440 | ee 441 | eg 442 | email 443 | emerck 444 | energy 445 | engineer 446 | engineering 447 | enterprises 448 | epson 449 | equipment 450 | er 451 | ericsson 452 | erni 453 | es 454 | esq 455 | estate 456 | et 457 | etisalat 458 | eu 459 | eurovision 460 | eus 461 | events 462 | exchange 463 | expert 464 | exposed 465 | express 466 | extraspace 467 | fage 468 | fail 469 | fairwinds 470 | faith 471 | family 472 | fan 473 | fans 474 | farm 475 | farmers 476 | fashion 477 | fast 478 | fedex 479 | feedback 480 | ferrari 481 | ferrero 482 | fi 483 | fiat 484 | fidelity 485 | fido 486 | film 487 | final 488 | finance 489 | financial 490 | fire 491 | firestone 492 | firmdale 493 | fish 494 | fishing 495 | fit 496 | fitness 497 | fj 498 | fk 499 | flickr 500 | flights 501 | flir 502 | florist 503 | flowers 504 | fly 505 | fm 506 | fo 507 | foo 508 | food 509 | foodnetwork 510 | football 511 | ford 512 | forex 513 | forsale 514 | forum 515 | foundation 516 | fox 517 | fr 518 | free 519 | fresenius 520 | frl 521 | frogans 522 | frontdoor 523 | frontier 524 | ftr 525 | fujitsu 526 | fun 527 | fund 528 | furniture 529 | futbol 530 | fyi 531 | ga 532 | gal 533 | gallery 534 | gallo 535 | gallup 536 | game 537 | games 538 | gap 539 | garden 540 | gay 541 | gb 542 | gbiz 543 | gd 544 | gdn 545 | ge 546 | gea 547 | gent 548 | genting 549 | george 550 | gf 551 | gg 552 | ggee 553 | gh 554 | gi 555 | gift 556 | gifts 557 | gives 558 | giving 559 | gl 560 | glade 561 | glass 562 | gle 563 | global 564 | globo 565 | gm 566 | gmail 567 | gmbh 568 | gmo 569 | gmx 570 | gn 571 | godaddy 572 | gold 573 | goldpoint 574 | golf 575 | goo 576 | goodyear 577 | goog 578 | google 579 | gop 580 | got 581 | gov 582 | gp 583 | gq 584 | gr 585 | grainger 586 | graphics 587 | gratis 588 | green 589 | gripe 590 | grocery 591 | group 592 | gs 593 | gt 594 | gu 595 | guardian 596 | gucci 597 | guge 598 | guide 599 | guitars 600 | guru 601 | gw 602 | gy 603 | hair 604 | hamburg 605 | hangout 606 | haus 607 | hbo 608 | hdfc 609 | hdfcbank 610 | health 611 | healthcare 612 | help 613 | helsinki 614 | here 615 | hermes 616 | hgtv 617 | hiphop 618 | hisamitsu 619 | hitachi 620 | hiv 621 | hk 622 | hkt 623 | hm 624 | hn 625 | hockey 626 | holdings 627 | holiday 628 | homedepot 629 | homegoods 630 | homes 631 | homesense 632 | honda 633 | horse 634 | hospital 635 | host 636 | hosting 637 | hot 638 | hoteles 639 | hotels 640 | hotmail 641 | house 642 | how 643 | hr 644 | hsbc 645 | ht 646 | hu 647 | hughes 648 | hyatt 649 | hyundai 650 | ibm 651 | icbc 652 | ice 653 | icu 654 | id 655 | ie 656 | ieee 657 | ifm 658 | ikano 659 | il 660 | im 661 | imamat 662 | imdb 663 | immo 664 | immobilien 665 | in 666 | inc 667 | industries 668 | infiniti 669 | info 670 | ing 671 | ink 672 | institute 673 | insurance 674 | insure 675 | int 676 | international 677 | intuit 678 | investments 679 | io 680 | ipiranga 681 | iq 682 | ir 683 | irish 684 | is 685 | ismaili 686 | ist 687 | istanbul 688 | it 689 | itau 690 | itv 691 | jaguar 692 | java 693 | jcb 694 | je 695 | jeep 696 | jetzt 697 | jewelry 698 | jio 699 | jll 700 | jm 701 | jmp 702 | jnj 703 | jo 704 | jobs 705 | joburg 706 | jot 707 | joy 708 | jp 709 | jpmorgan 710 | jprs 711 | juegos 712 | juniper 713 | kaufen 714 | kddi 715 | ke 716 | kerryhotels 717 | kerrylogistics 718 | kerryproperties 719 | kfh 720 | kg 721 | kh 722 | ki 723 | kia 724 | kim 725 | kinder 726 | kindle 727 | kitchen 728 | kiwi 729 | km 730 | kn 731 | koeln 732 | komatsu 733 | kosher 734 | kp 735 | kpmg 736 | kpn 737 | kr 738 | krd 739 | kred 740 | kuokgroup 741 | kw 742 | ky 743 | kyoto 744 | kz 745 | la 746 | lacaixa 747 | lamborghini 748 | lamer 749 | lancaster 750 | lancia 751 | land 752 | landrover 753 | lanxess 754 | lasalle 755 | lat 756 | latino 757 | latrobe 758 | law 759 | lawyer 760 | lb 761 | lc 762 | lds 763 | lease 764 | leclerc 765 | lefrak 766 | legal 767 | lego 768 | lexus 769 | lgbt 770 | li 771 | lidl 772 | life 773 | lifeinsurance 774 | lifestyle 775 | lighting 776 | like 777 | lilly 778 | limited 779 | limo 780 | lincoln 781 | linde 782 | link 783 | lipsy 784 | live 785 | living 786 | lixil 787 | lk 788 | llc 789 | llp 790 | loan 791 | loans 792 | locker 793 | locus 794 | loft 795 | lol 796 | london 797 | lotte 798 | lotto 799 | love 800 | lpl 801 | lplfinancial 802 | lr 803 | ls 804 | lt 805 | ltd 806 | ltda 807 | lu 808 | lundbeck 809 | luxe 810 | luxury 811 | lv 812 | ly 813 | ma 814 | macys 815 | madrid 816 | maif 817 | maison 818 | makeup 819 | man 820 | management 821 | mango 822 | map 823 | market 824 | marketing 825 | markets 826 | marriott 827 | marshalls 828 | maserati 829 | mattel 830 | mba 831 | mc 832 | mckinsey 833 | md 834 | me 835 | med 836 | media 837 | meet 838 | melbourne 839 | meme 840 | memorial 841 | men 842 | menu 843 | merckmsd 844 | mg 845 | mh 846 | miami 847 | microsoft 848 | mil 849 | mini 850 | mint 851 | mit 852 | mitsubishi 853 | mk 854 | ml 855 | mlb 856 | mls 857 | mm 858 | mma 859 | mn 860 | mo 861 | mobi 862 | mobile 863 | moda 864 | moe 865 | moi 866 | mom 867 | monash 868 | money 869 | monster 870 | mormon 871 | mortgage 872 | moscow 873 | moto 874 | motorcycles 875 | mov 876 | movie 877 | mp 878 | mq 879 | mr 880 | ms 881 | msd 882 | mt 883 | mtn 884 | mtr 885 | mu 886 | museum 887 | mutual 888 | mv 889 | mw 890 | mx 891 | my 892 | mz 893 | na 894 | nab 895 | nagoya 896 | name 897 | natura 898 | navy 899 | nba 900 | nc 901 | ne 902 | nec 903 | net 904 | netbank 905 | netflix 906 | network 907 | neustar 908 | new 909 | news 910 | next 911 | nextdirect 912 | nexus 913 | nf 914 | nfl 915 | ng 916 | ngo 917 | nhk 918 | ni 919 | nico 920 | nike 921 | nikon 922 | ninja 923 | nissan 924 | nissay 925 | nl 926 | no 927 | nokia 928 | northwesternmutual 929 | norton 930 | now 931 | nowruz 932 | nowtv 933 | np 934 | nr 935 | nra 936 | nrw 937 | ntt 938 | nu 939 | nyc 940 | nz 941 | obi 942 | observer 943 | off 944 | office 945 | okinawa 946 | olayan 947 | olayangroup 948 | oldnavy 949 | ollo 950 | om 951 | omega 952 | one 953 | ong 954 | onl 955 | online 956 | ooo 957 | open 958 | oracle 959 | orange 960 | org 961 | organic 962 | origins 963 | osaka 964 | otsuka 965 | ott 966 | ovh 967 | pa 968 | page 969 | panasonic 970 | paris 971 | pars 972 | partners 973 | parts 974 | party 975 | passagens 976 | pay 977 | pccw 978 | pe 979 | pet 980 | pf 981 | pfizer 982 | pg 983 | ph 984 | pharmacy 985 | phd 986 | philips 987 | phone 988 | photo 989 | photography 990 | photos 991 | physio 992 | pics 993 | pictet 994 | pictures 995 | pid 996 | pin 997 | ping 998 | pink 999 | pioneer 1000 | pizza 1001 | pk 1002 | pl 1003 | place 1004 | play 1005 | playstation 1006 | plumbing 1007 | plus 1008 | pm 1009 | pn 1010 | pnc 1011 | pohl 1012 | poker 1013 | politie 1014 | porn 1015 | post 1016 | pr 1017 | pramerica 1018 | praxi 1019 | press 1020 | prime 1021 | pro 1022 | prod 1023 | productions 1024 | prof 1025 | progressive 1026 | promo 1027 | properties 1028 | property 1029 | protection 1030 | pru 1031 | prudential 1032 | ps 1033 | pt 1034 | pub 1035 | pw 1036 | pwc 1037 | py 1038 | qa 1039 | qpon 1040 | quebec 1041 | quest 1042 | qvc 1043 | racing 1044 | radio 1045 | raid 1046 | re 1047 | read 1048 | realestate 1049 | realtor 1050 | realty 1051 | recipes 1052 | red 1053 | redstone 1054 | redumbrella 1055 | rehab 1056 | reise 1057 | reisen 1058 | reit 1059 | reliance 1060 | ren 1061 | rent 1062 | rentals 1063 | repair 1064 | report 1065 | republican 1066 | rest 1067 | restaurant 1068 | review 1069 | reviews 1070 | rexroth 1071 | rich 1072 | richardli 1073 | ricoh 1074 | ril 1075 | rio 1076 | rip 1077 | rmit 1078 | ro 1079 | rocher 1080 | rocks 1081 | rodeo 1082 | rogers 1083 | room 1084 | rs 1085 | rsvp 1086 | ru 1087 | rugby 1088 | ruhr 1089 | run 1090 | rw 1091 | rwe 1092 | ryukyu 1093 | sa 1094 | saarland 1095 | safe 1096 | safety 1097 | sakura 1098 | sale 1099 | salon 1100 | samsclub 1101 | samsung 1102 | sandvik 1103 | sandvikcoromant 1104 | sanofi 1105 | sap 1106 | sarl 1107 | sas 1108 | save 1109 | saxo 1110 | sb 1111 | sbi 1112 | sbs 1113 | sc 1114 | sca 1115 | scb 1116 | schaeffler 1117 | schmidt 1118 | scholarships 1119 | school 1120 | schule 1121 | schwarz 1122 | science 1123 | scjohnson 1124 | scot 1125 | sd 1126 | se 1127 | search 1128 | seat 1129 | secure 1130 | security 1131 | seek 1132 | select 1133 | sener 1134 | services 1135 | ses 1136 | seven 1137 | sew 1138 | sex 1139 | sexy 1140 | sfr 1141 | sg 1142 | sh 1143 | shangrila 1144 | sharp 1145 | shaw 1146 | shell 1147 | shia 1148 | shiksha 1149 | shoes 1150 | shop 1151 | shopping 1152 | shouji 1153 | show 1154 | showtime 1155 | si 1156 | silk 1157 | sina 1158 | singles 1159 | site 1160 | sj 1161 | sk 1162 | ski 1163 | skin 1164 | sky 1165 | skype 1166 | sl 1167 | sling 1168 | sm 1169 | smart 1170 | smile 1171 | sn 1172 | sncf 1173 | so 1174 | soccer 1175 | social 1176 | softbank 1177 | software 1178 | sohu 1179 | solar 1180 | solutions 1181 | song 1182 | sony 1183 | soy 1184 | spa 1185 | space 1186 | sport 1187 | spot 1188 | sr 1189 | srl 1190 | ss 1191 | st 1192 | stada 1193 | staples 1194 | star 1195 | statebank 1196 | statefarm 1197 | stc 1198 | stcgroup 1199 | stockholm 1200 | storage 1201 | store 1202 | stream 1203 | studio 1204 | study 1205 | style 1206 | su 1207 | sucks 1208 | supplies 1209 | supply 1210 | support 1211 | surf 1212 | surgery 1213 | suzuki 1214 | sv 1215 | swatch 1216 | swiftcover 1217 | swiss 1218 | sx 1219 | sy 1220 | sydney 1221 | systems 1222 | sz 1223 | tab 1224 | taipei 1225 | talk 1226 | taobao 1227 | target 1228 | tatamotors 1229 | tatar 1230 | tattoo 1231 | tax 1232 | taxi 1233 | tc 1234 | tci 1235 | td 1236 | tdk 1237 | team 1238 | tech 1239 | technology 1240 | tel 1241 | temasek 1242 | tennis 1243 | teva 1244 | tf 1245 | tg 1246 | th 1247 | thd 1248 | theater 1249 | theatre 1250 | tiaa 1251 | tickets 1252 | tienda 1253 | tiffany 1254 | tips 1255 | tires 1256 | tirol 1257 | tj 1258 | tjmaxx 1259 | tjx 1260 | tk 1261 | tkmaxx 1262 | tl 1263 | tm 1264 | tmall 1265 | tn 1266 | to 1267 | today 1268 | tokyo 1269 | tools 1270 | top 1271 | toray 1272 | toshiba 1273 | total 1274 | tours 1275 | town 1276 | toyota 1277 | toys 1278 | tr 1279 | trade 1280 | trading 1281 | training 1282 | travel 1283 | travelchannel 1284 | travelers 1285 | travelersinsurance 1286 | trust 1287 | trv 1288 | tt 1289 | tube 1290 | tui 1291 | tunes 1292 | tushu 1293 | tv 1294 | tvs 1295 | tw 1296 | tz 1297 | ua 1298 | ubank 1299 | ubs 1300 | ug 1301 | uk 1302 | unicom 1303 | university 1304 | uno 1305 | uol 1306 | ups 1307 | us 1308 | uy 1309 | uz 1310 | va 1311 | vacations 1312 | vana 1313 | vanguard 1314 | vc 1315 | ve 1316 | vegas 1317 | ventures 1318 | verisign 1319 | versicherung 1320 | vet 1321 | vg 1322 | vi 1323 | viajes 1324 | video 1325 | vig 1326 | viking 1327 | villas 1328 | vin 1329 | vip 1330 | virgin 1331 | visa 1332 | vision 1333 | viva 1334 | vivo 1335 | vlaanderen 1336 | vn 1337 | vodka 1338 | volkswagen 1339 | volvo 1340 | vote 1341 | voting 1342 | voto 1343 | voyage 1344 | vu 1345 | vuelos 1346 | wales 1347 | walmart 1348 | walter 1349 | wang 1350 | wanggou 1351 | watch 1352 | watches 1353 | weather 1354 | weatherchannel 1355 | webcam 1356 | weber 1357 | website 1358 | wed 1359 | wedding 1360 | weibo 1361 | weir 1362 | wf 1363 | whoswho 1364 | wien 1365 | wiki 1366 | williamhill 1367 | win 1368 | windows 1369 | wine 1370 | winners 1371 | wme 1372 | wolterskluwer 1373 | woodside 1374 | work 1375 | works 1376 | world 1377 | wow 1378 | ws 1379 | wtc 1380 | wtf 1381 | xbox 1382 | xerox 1383 | xfinity 1384 | xihuan 1385 | xin 1386 | xn--11b4c3d 1387 | xn--1ck2e1b 1388 | xn--1qqw23a 1389 | xn--2scrj9c 1390 | xn--30rr7y 1391 | xn--3bst00m 1392 | xn--3ds443g 1393 | xn--3e0b707e 1394 | xn--3hcrj9c 1395 | xn--3oq18vl8pn36a 1396 | xn--3pxu8k 1397 | xn--42c2d9a 1398 | xn--45br5cyl 1399 | xn--45brj9c 1400 | xn--45q11c 1401 | xn--4dbrk0ce 1402 | xn--4gbrim 1403 | xn--54b7fta0cc 1404 | xn--55qw42g 1405 | xn--55qx5d 1406 | xn--5su34j936bgsg 1407 | xn--5tzm5g 1408 | xn--6frz82g 1409 | xn--6qq986b3xl 1410 | xn--80adxhks 1411 | xn--80ao21a 1412 | xn--80aqecdr1a 1413 | xn--80asehdb 1414 | xn--80aswg 1415 | xn--8y0a063a 1416 | xn--90a3ac 1417 | xn--90ae 1418 | xn--90ais 1419 | xn--9dbq2a 1420 | xn--9et52u 1421 | xn--9krt00a 1422 | xn--b4w605ferd 1423 | xn--bck1b9a5dre4c 1424 | xn--c1avg 1425 | xn--c2br7g 1426 | xn--cck2b3b 1427 | xn--cckwcxetd 1428 | xn--cg4bki 1429 | xn--clchc0ea0b2g2a9gcd 1430 | xn--czr694b 1431 | xn--czrs0t 1432 | xn--czru2d 1433 | xn--d1acj3b 1434 | xn--d1alf 1435 | xn--e1a4c 1436 | xn--eckvdtc9d 1437 | xn--efvy88h 1438 | xn--fct429k 1439 | xn--fhbei 1440 | xn--fiq228c5hs 1441 | xn--fiq64b 1442 | xn--fiqs8s 1443 | xn--fiqz9s 1444 | xn--fjq720a 1445 | xn--flw351e 1446 | xn--fpcrj9c3d 1447 | xn--fzc2c9e2c 1448 | xn--fzys8d69uvgm 1449 | xn--g2xx48c 1450 | xn--gckr3f0f 1451 | xn--gecrj9c 1452 | xn--gk3at1e 1453 | xn--h2breg3eve 1454 | xn--h2brj9c 1455 | xn--h2brj9c8c 1456 | xn--hxt814e 1457 | xn--i1b6b1a6a2e 1458 | xn--imr513n 1459 | xn--io0a7i 1460 | xn--j1aef 1461 | xn--j1amh 1462 | xn--j6w193g 1463 | xn--jlq480n2rg 1464 | xn--jlq61u9w7b 1465 | xn--jvr189m 1466 | xn--kcrx77d1x4a 1467 | xn--kprw13d 1468 | xn--kpry57d 1469 | xn--kput3i 1470 | xn--l1acc 1471 | xn--lgbbat1ad8j 1472 | xn--mgb9awbf 1473 | xn--mgba3a3ejt 1474 | xn--mgba3a4f16a 1475 | xn--mgba7c0bbn0a 1476 | xn--mgbaakc7dvf 1477 | xn--mgbaam7a8h 1478 | xn--mgbab2bd 1479 | xn--mgbah1a3hjkrd 1480 | xn--mgbai9azgqp6j 1481 | xn--mgbayh7gpa 1482 | xn--mgbbh1a 1483 | xn--mgbbh1a71e 1484 | xn--mgbc0a9azcg 1485 | xn--mgbca7dzdo 1486 | xn--mgbcpq6gpa1a 1487 | xn--mgberp4a5d4ar 1488 | xn--mgbgu82a 1489 | xn--mgbi4ecexp 1490 | xn--mgbpl2fh 1491 | xn--mgbt3dhd 1492 | xn--mgbtx2b 1493 | xn--mgbx4cd0ab 1494 | xn--mix891f 1495 | xn--mk1bu44c 1496 | xn--mxtq1m 1497 | xn--ngbc5azd 1498 | xn--ngbe9e0a 1499 | xn--ngbrx 1500 | xn--node 1501 | xn--nqv7f 1502 | xn--nqv7fs00ema 1503 | xn--nyqy26a 1504 | xn--o3cw4h 1505 | xn--ogbpf8fl 1506 | xn--otu796d 1507 | xn--p1acf 1508 | xn--p1ai 1509 | xn--pgbs0dh 1510 | xn--pssy2u 1511 | xn--q7ce6a 1512 | xn--q9jyb4c 1513 | xn--qcka1pmc 1514 | xn--qxa6a 1515 | xn--qxam 1516 | xn--rhqv96g 1517 | xn--rovu88b 1518 | xn--rvc1e0am3e 1519 | xn--s9brj9c 1520 | xn--ses554g 1521 | xn--t60b56a 1522 | xn--tckwe 1523 | xn--tiq49xqyj 1524 | xn--unup4y 1525 | xn--vermgensberater-ctb 1526 | xn--vermgensberatung-pwb 1527 | xn--vhquv 1528 | xn--vuq861b 1529 | xn--w4r85el8fhu5dnra 1530 | xn--w4rs40l 1531 | xn--wgbh1c 1532 | xn--wgbl6a 1533 | xn--xhq521b 1534 | xn--xkc2al3hye2a 1535 | xn--xkc2dl3a5ee0h 1536 | xn--y9a3aq 1537 | xn--yfro4i67o 1538 | xn--ygbi2ammx 1539 | xn--zfr164b 1540 | xxx 1541 | xyz 1542 | yachts 1543 | yahoo 1544 | yamaxun 1545 | yandex 1546 | ye 1547 | yodobashi 1548 | yoga 1549 | yokohama 1550 | you 1551 | youtube 1552 | yt 1553 | yun 1554 | za 1555 | zappos 1556 | zara 1557 | zero 1558 | zip 1559 | zm 1560 | zone 1561 | zuerich 1562 | zw 1563 | 1564 | 1565 | 1566 | --------------------------------------------------------------------------------