├── 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 | [](https://github.com/spothero/SpotHeroEmailValidator-iOS/actions?query=workflow%3A%22CI%22)
4 | [](https://github.com/spothero/SpotHeroEmailValidator-iOS/releases)
5 | [](https://developer.apple.com/swift)
6 | [](https://github.com/spothero/SpotHeroEmailValidator-iOS/blob/main/Package.swift)
7 | [](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 | 
13 | 
14 | 
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 |
--------------------------------------------------------------------------------