1- import Foundation
21import RegexBuilder
2+ import Standards
3+ import INCITS_4_1986
34
45@_exported import struct RFC_1123. Domain
56
@@ -16,7 +17,7 @@ public struct EmailAddress: Hashable, Sendable {
1617
1718 /// Initialize with components
1819 public init ( displayName: String ? = nil , localPart: LocalPart , domain: RFC_1123 . Domain ) {
19- self . displayName = displayName? . trimmingCharacters ( in : . whitespaces)
20+ self . displayName = displayName? . trimming ( . whitespaces)
2021 self . localPart = localPart
2122 self . domain = domain
2223 }
@@ -41,11 +42,11 @@ public struct EmailAddress: Hashable, Sendable {
4142
4243 // Extract display name if present and normalize spaces
4344 let displayName = captures. 1 . map { name in
44- let trimmedName = name. trimmingCharacters ( in : . whitespaces)
45+ let trimmedName = name. trimming ( . whitespaces)
4546 if trimmedName. hasPrefix ( " \" " ) && trimmedName. hasSuffix ( " \" " ) {
4647 let withoutQuotes = String ( trimmedName. dropFirst ( ) . dropLast ( ) )
47- return withoutQuotes. replacingOccurrences ( of : " \\ \" " , with: " \" " )
48- . replacingOccurrences ( of : " \\ \\ " , with: " \\ " )
48+ return withoutQuotes. replacing ( " \\ \" " , with: " \" " )
49+ . replacing ( " \\ \\ " , with: " \\ " )
4950 }
5051 return trimmedName
5152 }
@@ -90,6 +91,11 @@ extension RFC_5321.EmailAddress {
9091
9192 /// Initialize with a string
9293 public in it ( _ string: String) throws {
94+ // RFC 5321 is ASCII-only - validate before processing
95+ guard string. asciiBytes != nil else {
96+ throw ValidationError . nonASCIICharacters
97+ }
98+
9399 // Check overall length first
94100 guard string. count <= Limits . maxLength else {
95101 throw ValidationError . localPartTooLong ( string. count)
@@ -145,34 +151,44 @@ extension RFC_5321.EmailAddress {
145151 nonisolated( unsafe) private static let quotedRegex = /(?:[^"\\]|\\["\\])+/
146152}
147153
148- extension RFC_5321. EmailAddress {
149- /// The complete email address string, including display name if present
150- public var stringValue: String {
151- if let name = displayName {
154+ extension String {
155+ public init(
156+ _ email: RFC_5321 . EmailAddress
157+ ) {
158+ if let name = email. displayName {
152159 let needsQuoting = name. contains ( where: {
153- !$0. isLetter && !$0. isNumber && !$0. isWhitespace
160+ !$0. isASCIILetter && !$0. isASCIIDigit && !$0. isASCIIWhitespace
154161 } )
155162 let quotedName =
156- needsQuoting ? " \" \( name. replacingOccurrences ( of: " \" " , with: " \\ \" " ) ) \" " : name
157- return " \( quotedName) < \( localPart) @ \( domain. name) > " // Exactly one space before angle bracket
163+ needsQuoting ? " \" \( name. replacing ( " \" " , with: " \\ \" " ) ) \" " : name
164+ self = " \( quotedName) < \( email. localPart) @ \( email. domain. name) > " // Exactly one space before angle bracket
165+ } else {
166+ self = " \( email. localPart) @ \( email. domain. name) "
158167 }
159- return " \( localPart) @ \( domain. name) "
168+ }
169+ }
170+
171+ extension RFC_5321. EmailAddress {
172+ /// The complete email address string, including display name if present
173+ public var value: String {
174+ String ( self )
160175 }
161176
162177 /// Just the email address part without display name
163- public var addressValue : String {
178+ public var address : String {
164179 " \( localPart) @ \( domain. name) "
165180 }
166181}
167182
168183// MARK: - Errors
169184extension RFC_5321. EmailAddress {
170- public enum ValidationError: Error , LocalizedError , Equatable {
185+ public enum ValidationError: Error , Equatable {
171186 case missingAtSign
172187 case invalidDotAtom
173188 case invalidQuotedString
174189 case totalLengthExceeded( _ length: Int)
175190 case localPartTooLong( _ length: Int)
191+ case nonASCIICharacters
176192
177193 public var errorDescription : String ? {
178194 switch self {
@@ -186,14 +202,16 @@ extension RFC_5321.EmailAddress {
186202 return " Local-part length \( length) exceeds maximum of \( Limits . maxLength) "
187203 case . totalLengthExceeded( let length) :
188204 return " Total length \( length) exceeds maximum of \( Limits . maxTotalLength) "
205+ case . nonASCIICharacters:
206+ return " RFC 5321 email addresses must contain only ASCII characters "
189207 }
190208 }
191209 }
192210}
193211
194212// MARK: - Protocol Conformances
195213extension RFC_5321. EmailAddress: CustomStringConvertible {
196- public var description: String { stringValue }
214+ public var description: String { String ( self ) }
197215}
198216
199217extension RFC_5321. EmailAddress : Codable {
@@ -210,6 +228,6 @@ extension RFC_5321.EmailAddress: Codable {
210228}
211229
212230extension RFC_5321. EmailAddress : RawRepresentable {
213- public var rawValue: String { stringValue }
231+ public var rawValue: String { String ( self ) }
214232 public init? ( rawValue: String) { try ? self . init ( rawValue) }
215233}
0 commit comments