diff --git a/Sources/ValidationKit/Resources/en.lproj/Localizable.strings b/Sources/ValidationKit/Resources/en.lproj/Localizable.strings index 1e44abe..1902404 100644 --- a/Sources/ValidationKit/Resources/en.lproj/Localizable.strings +++ b/Sources/ValidationKit/Resources/en.lproj/Localizable.strings @@ -16,3 +16,4 @@ "invalid_date" = "is not a valid date"; "not_accepted" = "is not accepted"; "invalid_option" = "invalid option %@"; +"invalid_email" = "invalid e-mail address"; diff --git a/Sources/ValidationKit/Resources/nl.lproj/Localizable.strings b/Sources/ValidationKit/Resources/nl.lproj/Localizable.strings index f9214f0..4217fa5 100644 --- a/Sources/ValidationKit/Resources/nl.lproj/Localizable.strings +++ b/Sources/ValidationKit/Resources/nl.lproj/Localizable.strings @@ -16,3 +16,4 @@ "invalid_date" = "is geen geldige datum"; "not_accepted" = "is niet geaccepteerd"; "invalid_option" = "ongeldige optie %@"; +"invalid_email" = "ongeldig e-mailadres"; diff --git a/Sources/ValidationKit/Validators/Validator+Email.swift b/Sources/ValidationKit/Validators/Validator+Email.swift new file mode 100644 index 0000000..797a7f8 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Email.swift @@ -0,0 +1,47 @@ +// +// Validator+Email.swift +// ValidationKit +// +// Created by Mathijs Bernson on 25/05/2023. +// + +import Foundation + +private let emailPattern = "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + +public extension Validator { + /// Validates whether the value is a valid e-mail address, according to the WHATWG HTML living standard. + /// + /// A valid email address is a string that matches the email production of the following ABNF, the character set for which is Unicode. This ABNF implements the extensions described in RFC 1123. + /// + /// ``` + /// email = 1*( atext / "." ) "@" label *( "." label ) + /// label = let-dig [ [ ldh-str ] let-dig ] ; limited to a length of 63 characters by RFC 1034 section 3.5 + /// atext = < as defined in RFC 5322 section 3.2.3 > + /// let-dig = < as defined in RFC 1034 section 3.5 > + /// ldh-str = < as defined in RFC 1034 section 3.5 > + /// ``` + /// + /// Note: This requirement is a willful violation of RFC 5322, which defines a syntax for email addresses that is simultaneously too strict (before the "@" character), too vague (after the "@" character), and too lax (allowing comments, whitespace characters, and quoted strings in manners unfamiliar to most users) to be of practical use here. + /// + /// Reference: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + static var email: Validator { + Validator { input in + if let range = input.range(of: emailPattern, options: .regularExpression) { + let output = String(input[range]) + return .valid(output) + } else { + return .invalid(.invalidEmail) + } + } + } +} +public extension ValidationError { + static let invalidEmail = ValidationError(localizedDescription: NSLocalizedString("invalid_email", comment: "Invalid e-mail address error")) +} diff --git a/Tests/ValidationKitTests/EmailTests.swift b/Tests/ValidationKitTests/EmailTests.swift new file mode 100644 index 0000000..ef23420 --- /dev/null +++ b/Tests/ValidationKitTests/EmailTests.swift @@ -0,0 +1,84 @@ +import XCTest +import ValidationKit + +class EmailTests: XCTestCase { + var validator: Validator! + + override func setUpWithError() throws { + validator = .email + } + + func testValidEmailAddresses() throws { + XCTAssertTrue(validator.validate(input: "mathijsb+test@q42.nl").isValid) + XCTAssertTrue(validator.validate(input: "mathijsb@subdomain.q42.nl").isValid) + + // Test cases from: https://www.softwaretestingo.com/test-cases-for-email-field/ + XCTAssertTrue(validator.validate(input: "email@domain.com").isValid, "Valid email") + XCTAssertTrue(validator.validate(input: "firstname.lastname@domain.com").isValid, "The email contains a dot in the address field") + XCTAssertTrue(validator.validate(input: "email@subdomain.domain.com").isValid, "The email contains a dot with a subdomain") + XCTAssertTrue(validator.validate(input: "firstname+lastname@domain.com").isValid, "Plus sign is considered a valid character") + XCTAssertTrue(validator.validate(input: "email@123.123.123.123").isValid, "The domain is a valid IP address") + XCTAssertTrue(validator.validate(input: "1234567890@domain.com").isValid, "Digits in the address are valid") + XCTAssertTrue(validator.validate(input: "email@domain-one.com").isValid, "Dash in the domain name is valid") + XCTAssertTrue(validator.validate(input: "_______@domain.com").isValid, "Underscore in the address field is valid") + XCTAssertTrue(validator.validate(input: "email@domain.name").isValid, ".name is a valid Top Level Domain name") + XCTAssertTrue(validator.validate(input: "email@domain.co.jp").isValid, "Dot in Top Level Domain name also considered valid (use co.jp as an example here)") + XCTAssertTrue(validator.validate(input: "firstname-lastname@domain.com").isValid, "Dash in the address field is valid") + + // Test cases from ChatGPT + XCTAssertTrue(validator.validate(input: "example@example.com").isValid, "Standard email format") + XCTAssertTrue(validator.validate(input: "john.doe@example.co.uk").isValid, "Email with a subdomain") + XCTAssertTrue(validator.validate(input: "user123@example123.com").isValid, "Email with numbers in the domain name") + XCTAssertTrue(validator.validate(input: "john_doe+test@example.com").isValid, "Email with special characters in the local part") + XCTAssertTrue(validator.validate(input: "user@example.io").isValid, "Email with a two-letter top-level domain (TLD)") + XCTAssertTrue(validator.validate(input: "test-email@example-domain.com").isValid, "Email with a hyphen in the domain name") + XCTAssertTrue(validator.validate(input: "a@example.com").isValid, "Email with a single-letter local part") + XCTAssertTrue(validator.validate(input: "thisisaverylongemailaddresswithlotsofcharacters@example.com").isValid, "Email with a long local part and domain name") + XCTAssertTrue(validator.validate(input: ".test@example.com").isValid, "Email with a dot at the beginning of the local part") + XCTAssertTrue(validator.validate(input: "test.@example.com").isValid, "Email with a dot at the end of the local part") + + // These cases are currently not considered valid, but they should be + // XCTAssertTrue(validator.validate(input: "email@[123.123.123.123]").isValid, "A square bracket around the IP address is considered valid") + // XCTAssertTrue(validator.validate(input: "“email”@domain.com").isValid, "Quotes around email are considered valid") + } + + func testInvalidEmailAddresses() throws { + XCTAssertFalse(validator.validate(input: "").isValid) + XCTAssertFalse(validator.validate(input: "foo").isValid) + XCTAssertFalse(validator.validate(input: "foobarbazquuxwhopper").isValid) + + // Test cases from: https://www.softwaretestingo.com/test-cases-for-email-field/ + XCTAssertFalse(validator.validate(input: "plain address").isValid, "Missing @ sign and domain") + XCTAssertFalse(validator.validate(input: "#@%^%#$@#$@#.com").isValid, "Garbage") + XCTAssertFalse(validator.validate(input: "@domain.com").isValid, "Missing username") + XCTAssertFalse(validator.validate(input: "email.domain.com").isValid, "Missing @") + XCTAssertFalse(validator.validate(input: "email@domain").isValid, "Missing top-level domain (.com/.net/.org/etc.)") + XCTAssertFalse(validator.validate(input: "email@-domain.com").isValid, "The leading dash in front of the domain is invalid") + XCTAssertFalse(validator.validate(input: "email@domain..com").isValid, "Multiple dots in the domain portion is invalid") + + // Test cases from ChatGPT + XCTAssertFalse(validator.validate(input: "example.com").isValid, "Missing @ symbol") + XCTAssertFalse(validator.validate(input: "john@.com").isValid, "Email without a domain name") + XCTAssertFalse(validator.validate(input: "user@example").isValid, "Email without a top-level domain (TLD)") + XCTAssertFalse(validator.validate(input: "john@example#.com").isValid, "Email with invalid characters in the domain name") + XCTAssertFalse(validator.validate(input: "@example.com").isValid, "Email without a local part") + XCTAssertFalse(validator.validate(input: "john_doe@_example.com").isValid, "Email with an underscore at the beginning of the domain name") + XCTAssertFalse(validator.validate(input: "user@example.").isValid, "Email with a missing domain extension") + + // These cases are currently considered valid, but they should not be + // XCTAssertFalse(validator.validate(input: "email.@domain.com").isValid, "Trailing dot in address is not allowed") + // XCTAssertFalse(validator.validate(input: "Joe Smith ").isValid, "Encoded HTML within an email is invalid") + // XCTAssertFalse(validator.validate(input: "email@domain@domain.com").isValid, "Two @ sign") + // XCTAssertFalse(validator.validate(input: ".email@domain.com").isValid, "The leading dot in the address is not allowed") + // XCTAssertFalse(validator.validate(input: "email..email@domain.com").isValid, "Multiple dots") + // XCTAssertFalse(validator.validate(input: "あいうえお@domain.com").isValid, "Unicode char as address") + // XCTAssertFalse(validator.validate(input: "email@domain.com (Joe Smith)").isValid, "Text followed email is not allowed") + // XCTAssertFalse(validator.validate(input: "email@domain.web").isValid, ".web is not a valid top-level domain") + // XCTAssertFalse(validator.validate(input: "email@111.222.333.44444").isValid, "Invalid IP format") + + // These cases are currently considered valid, but they should not be (test cases from ChatGPT) + // XCTAssertFalse(validator.validate(input: "john@doe@example.com").isValid, "Email with multiple @ symbols") + // XCTAssertFalse(validator.validate(input: "john doe@example.com").isValid, "Email with a space character") + // XCTAssertFalse(validator.validate(input: "john..doe@example.com").isValid, "Email with consecutive dots in the local part") + } +}