Skip to content

Commit 77dd564

Browse files
committed
feat!: refactor HeaderName to Header with nested Name type
BREAKING CHANGES: - Renamed RFC_5322.HeaderName to RFC_5322.Header.Name - RFC_5322.Message.additionalHeaders changed from [String: String] to [Header] - Headers are now RFC-accurate ordered sequences instead of dictionaries New Features: - RFC_5322.Header type representing complete header field (name-value pair) - Array convenience extensions for [Header]: - Subscript access: headers[.contentType] - Dictionary literal syntax support - all(_:) and values(for:) helper methods - Headers preserve order and allow duplicates (RFC-accurate) Architecture: - Header is the complete header field type - Header.Name is nested type for header names (case-insensitive) - All existing HeaderName static properties moved to Header.Name - Added @retroactive to ExpressibleByDictionaryLiteral conformance This models RFC 5322 accurately while providing Swift conveniences.
1 parent 9e76003 commit 77dd564

File tree

3 files changed

+339
-208
lines changed

3 files changed

+339
-208
lines changed

Sources/RFC_5322/Header.swift

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import Foundation
2+
3+
extension RFC_5322 {
4+
/// Email header field (name-value pair)
5+
///
6+
/// Represents a complete header field in Internet Message Format as defined by RFC 5322.
7+
/// Headers are stored as an ordered sequence of name-value pairs.
8+
///
9+
/// ## Example
10+
///
11+
/// ```swift
12+
/// let header = RFC_5322.Header(name: .contentType, value: "text/html")
13+
///
14+
/// var headers: [RFC_5322.Header] = [
15+
/// .init(name: .from, value: "[email protected]"),
16+
/// .init(name: .to, value: "[email protected]")
17+
/// ]
18+
///
19+
/// // Convenient subscript access
20+
/// headers[.contentType] = "text/html"
21+
/// print(headers[.contentType]) // Optional("text/html")
22+
/// ```
23+
///
24+
/// ## RFC Reference
25+
///
26+
/// From RFC 5322 Section 2.2:
27+
///
28+
/// > Each header field is a line of characters with a name, a colon,
29+
/// > and a value. Field names are comprised of printable US-ASCII
30+
/// > characters except colon. Field names are case-insensitive.
31+
public struct Header: Hashable, Sendable, Codable {
32+
/// The header field name
33+
public let name: Name
34+
35+
/// The header field value
36+
public let value: String
37+
38+
/// Creates a header field
39+
///
40+
/// - Parameters:
41+
/// - name: The header field name
42+
/// - value: The header field value
43+
public init(name: Name, value: String) {
44+
self.name = name
45+
self.value = value
46+
}
47+
}
48+
}
49+
50+
// MARK: - Header.Name
51+
52+
extension RFC_5322.Header {
53+
/// Email header field name
54+
///
55+
/// Represents header field names in Internet Message Format as defined by RFC 5322.
56+
/// Header field names are case-insensitive per the specification.
57+
///
58+
/// ## Example
59+
///
60+
/// ```swift
61+
/// let from = RFC_5322.Header.Name.from
62+
/// let custom = RFC_5322.Header.Name("X-Custom-Header")
63+
///
64+
/// var headers: [RFC_5322.Header] = []
65+
/// headers.append(.init(name: .messageId, value: "<[email protected]>"))
66+
/// headers.append(.init(name: .contentType, value: "text/plain"))
67+
/// ```
68+
///
69+
/// ## RFC Reference
70+
///
71+
/// From RFC 5322 Section 2.2:
72+
///
73+
/// > Field names are comprised of printable US-ASCII characters except colon.
74+
/// > Field names are case-insensitive.
75+
///
76+
/// ## Common Header Names
77+
///
78+
/// Standard RFC 5322 headers are available as static properties:
79+
/// - `from`, `to`, `cc`, `bcc`: Address headers
80+
/// - `subject`: Subject line
81+
/// - `date`: Date and time
82+
/// - `messageId`: Unique message identifier
83+
///
84+
/// MIME headers (RFC 2045) are also included:
85+
/// - `contentType`: Media type of content
86+
/// - `contentTransferEncoding`: Encoding mechanism
87+
/// - `mimeVersion`: MIME version
88+
///
89+
/// Custom headers can be created using the string-based initializer.
90+
public struct Name: Hashable, Sendable, Codable {
91+
/// The header field name
92+
public let rawValue: String
93+
94+
/// Creates a header name
95+
///
96+
/// - Parameter rawValue: The header field name (case-insensitive)
97+
public init(_ rawValue: String) {
98+
// Header names are case-insensitive, but we preserve original case
99+
// for display purposes while using case-insensitive comparison
100+
self.rawValue = rawValue
101+
}
102+
103+
/// Hash value (case-insensitive)
104+
public func hash(into hasher: inout Hasher) {
105+
hasher.combine(rawValue.lowercased())
106+
}
107+
108+
/// Equality comparison (case-insensitive)
109+
public static func == (lhs: RFC_5322.Header.Name, rhs: RFC_5322.Header.Name) -> Bool {
110+
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
111+
}
112+
}
113+
}
114+
115+
// MARK: - RFC 5322 Standard Headers
116+
117+
extension RFC_5322.Header.Name {
118+
/// From: header (originator)
119+
public static let from = RFC_5322.Header.Name("From")
120+
121+
/// To: header (primary recipients)
122+
public static let to = RFC_5322.Header.Name("To")
123+
124+
/// Cc: header (carbon copy recipients)
125+
public static let cc = RFC_5322.Header.Name("Cc")
126+
127+
/// Bcc: header (blind carbon copy recipients)
128+
public static let bcc = RFC_5322.Header.Name("Bcc")
129+
130+
/// Subject: header
131+
public static let subject = RFC_5322.Header.Name("Subject")
132+
133+
/// Date: header
134+
public static let date = RFC_5322.Header.Name("Date")
135+
136+
/// Message-ID: header (unique message identifier)
137+
public static let messageId = RFC_5322.Header.Name("Message-ID")
138+
139+
/// Reply-To: header
140+
public static let replyTo = RFC_5322.Header.Name("Reply-To")
141+
142+
/// Sender: header (actual sender if different from From)
143+
public static let sender = RFC_5322.Header.Name("Sender")
144+
145+
/// In-Reply-To: header (message being replied to)
146+
public static let inReplyTo = RFC_5322.Header.Name("In-Reply-To")
147+
148+
/// References: header (related messages)
149+
public static let references = RFC_5322.Header.Name("References")
150+
151+
/// Resent-From: header
152+
public static let resentFrom = RFC_5322.Header.Name("Resent-From")
153+
154+
/// Resent-To: header
155+
public static let resentTo = RFC_5322.Header.Name("Resent-To")
156+
157+
/// Resent-Date: header
158+
public static let resentDate = RFC_5322.Header.Name("Resent-Date")
159+
160+
/// Resent-Message-ID: header
161+
public static let resentMessageId = RFC_5322.Header.Name("Resent-Message-ID")
162+
163+
/// Return-Path: header
164+
public static let returnPath = RFC_5322.Header.Name("Return-Path")
165+
166+
/// Received: header (mail transfer path)
167+
public static let received = RFC_5322.Header.Name("Received")
168+
}
169+
170+
// MARK: - MIME Headers (RFC 2045)
171+
172+
extension RFC_5322.Header.Name {
173+
/// Content-Type: header (media type)
174+
public static let contentType = RFC_5322.Header.Name("Content-Type")
175+
176+
/// Content-Transfer-Encoding: header
177+
public static let contentTransferEncoding = RFC_5322.Header.Name("Content-Transfer-Encoding")
178+
179+
/// MIME-Version: header
180+
public static let mimeVersion = RFC_5322.Header.Name("MIME-Version")
181+
182+
/// Content-Disposition: header
183+
public static let contentDisposition = RFC_5322.Header.Name("Content-Disposition")
184+
185+
/// Content-ID: header
186+
public static let contentId = RFC_5322.Header.Name("Content-ID")
187+
188+
/// Content-Description: header
189+
public static let contentDescription = RFC_5322.Header.Name("Content-Description")
190+
}
191+
192+
// MARK: - Common Extension Headers
193+
194+
extension RFC_5322.Header.Name {
195+
/// X-Mailer: header (mail client identification)
196+
public static let xMailer = RFC_5322.Header.Name("X-Mailer")
197+
198+
/// X-Priority: header (message priority)
199+
public static let xPriority = RFC_5322.Header.Name("X-Priority")
200+
201+
/// List-Unsubscribe: header (mailing list unsubscribe)
202+
public static let listUnsubscribe = RFC_5322.Header.Name("List-Unsubscribe")
203+
204+
/// List-ID: header (mailing list identifier)
205+
public static let listId = RFC_5322.Header.Name("List-ID")
206+
207+
/// Precedence: header
208+
public static let precedence = RFC_5322.Header.Name("Precedence")
209+
210+
/// Auto-Submitted: header
211+
public static let autoSubmitted = RFC_5322.Header.Name("Auto-Submitted")
212+
}
213+
214+
// MARK: - Apple Mail Headers
215+
216+
extension RFC_5322.Header.Name {
217+
/// X-Apple-Base-Url: header
218+
public static let xAppleBaseUrl = RFC_5322.Header.Name("X-Apple-Base-Url")
219+
220+
/// X-Universally-Unique-Identifier: header
221+
public static let xUniversallyUniqueIdentifier = RFC_5322.Header.Name("X-Universally-Unique-Identifier")
222+
223+
/// X-Apple-Mail-Remote-Attachments: header
224+
public static let xAppleMailRemoteAttachments = RFC_5322.Header.Name("X-Apple-Mail-Remote-Attachments")
225+
226+
/// X-Apple-Windows-Friendly: header
227+
public static let xAppleWindowsFriendly = RFC_5322.Header.Name("X-Apple-Windows-Friendly")
228+
229+
/// X-Apple-Mail-Signature: header
230+
public static let xAppleMailSignature = RFC_5322.Header.Name("X-Apple-Mail-Signature")
231+
232+
/// X-Uniform-Type-Identifier: header
233+
public static let xUniformTypeIdentifier = RFC_5322.Header.Name("X-Uniform-Type-Identifier")
234+
}
235+
236+
// MARK: - Name Protocol Conformances
237+
238+
extension RFC_5322.Header.Name: ExpressibleByStringLiteral {
239+
/// Creates a header name from a string literal
240+
///
241+
/// Allows convenient syntax: `let header: Header.Name = "X-Custom"`
242+
public init(stringLiteral value: String) {
243+
self.init(value)
244+
}
245+
}
246+
247+
extension RFC_5322.Header.Name: CustomStringConvertible {
248+
/// Returns the header field name
249+
public var description: String {
250+
rawValue
251+
}
252+
}
253+
254+
// MARK: - Header Protocol Conformances
255+
256+
extension RFC_5322.Header: CustomStringConvertible {
257+
/// Returns the header in RFC 5322 format (name: value)
258+
public var description: String {
259+
"\(name.rawValue): \(value)"
260+
}
261+
}
262+
263+
// MARK: - Array Convenience Extensions
264+
265+
extension Array where Element == RFC_5322.Header {
266+
/// Subscript for convenient header access by name
267+
///
268+
/// Returns the value of the first header with the given name.
269+
/// Setting a value removes all existing headers with that name and appends a new one.
270+
/// Setting nil removes all headers with that name.
271+
///
272+
/// ## Example
273+
///
274+
/// ```swift
275+
/// var headers: [RFC_5322.Header] = []
276+
/// headers[.contentType] = "text/html"
277+
/// print(headers[.contentType]) // Optional("text/html")
278+
/// headers[.contentType] = nil // Removes the header
279+
/// ```
280+
public subscript(name: RFC_5322.Header.Name) -> String? {
281+
get {
282+
first(where: { $0.name == name })?.value
283+
}
284+
set {
285+
removeAll(where: { $0.name == name })
286+
if let newValue = newValue {
287+
append(RFC_5322.Header(name: name, value: newValue))
288+
}
289+
}
290+
}
291+
292+
/// Returns all headers with the given name
293+
///
294+
/// Useful for headers that can appear multiple times (like Received).
295+
///
296+
/// ## Example
297+
///
298+
/// ```swift
299+
/// let received = headers.all(.received)
300+
/// ```
301+
public func all(_ name: RFC_5322.Header.Name) -> [RFC_5322.Header] {
302+
filter { $0.name == name }
303+
}
304+
305+
/// Returns all values for headers with the given name
306+
///
307+
/// ## Example
308+
///
309+
/// ```swift
310+
/// let receivedValues = headers.values(for: .received)
311+
/// ```
312+
public func values(for name: RFC_5322.Header.Name) -> [String] {
313+
filter { $0.name == name }.map(\.value)
314+
}
315+
}
316+
317+
extension Array: @retroactive ExpressibleByDictionaryLiteral where Element == RFC_5322.Header {
318+
/// Creates an array of headers from a dictionary literal
319+
///
320+
/// Enables convenient syntax for creating headers.
321+
///
322+
/// ## Example
323+
///
324+
/// ```swift
325+
/// let headers: [RFC_5322.Header] = [
326+
/// .from: "[email protected]",
327+
/// .to: "[email protected]",
328+
/// .subject: "Hello"
329+
/// ]
330+
/// ```
331+
public init(dictionaryLiteral elements: (RFC_5322.Header.Name, String)...) {
332+
self = elements.map { RFC_5322.Header(name: $0.0, value: $0.1) }
333+
}
334+
}

0 commit comments

Comments
 (0)