import Foundation /// A validator reducing a submitted email address to its canonical form. /// /// Trims surrounding whitespace, lowercases the address (so one mailbox cannot register once per spelling), caps it at the 254 bytes an address can /// be, and checks its shape: something before the `@`, and a domain with a dot. Control characters are rejected separately: the shape only excludes /// whitespace, which would let non-whitespace controls (such as `NUL`) into the stored address. public struct NormalizeEmail: Sendable { // MARK: Initializers /// Creates an email normalization method. public init() {} // MARK: Functions /// Validates and normalizes a submitted email address. /// - Parameter email: the submitted email address, if any. /// - Returns: the normalized address, or `nil` when the submission is missing or invalid. public func callAsFunction( _ email: String? ) -> String? { guard let email = email? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased(), email.utf8.count <= Constant.Length.maxEmail, !email.unicodeScalars.contains(where: { $0.properties.generalCategory == .control }), email.wholeMatch(of: /[^\s@]+@[^\s@]+\.[^\s@]+/) != nil else { return nil } return email } } // MARK: - Constants private enum Constant { /// A namespace for the length constants. enum Length { /// The longest an email address can be, in bytes, per RFC 5321's path limit. static let maxEmail = 254 } }