From f8b60fb85e26b25a4b06aed496cfb20d7922a882 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Tue, 22 Jul 2025 16:25:22 +0200 Subject: [PATCH] Replace ID command dictionary with Identification struct --- .../IMAP/IMAP/Commands/IDCommand.swift | 10 +- .../IMAP/IMAP/Handler/IDHandler.swift | 4 +- Sources/SwiftMail/IMAP/IMAPServer.swift | 10 +- .../SwiftMail/IMAP/Models/IDResponse.swift | 27 ------ .../IMAP/Models/Identification.swift | 92 +++++++++++++++++++ 5 files changed, 102 insertions(+), 41 deletions(-) delete mode 100644 Sources/SwiftMail/IMAP/Models/IDResponse.swift create mode 100644 Sources/SwiftMail/IMAP/Models/Identification.swift diff --git a/Sources/SwiftMail/IMAP/IMAP/Commands/IDCommand.swift b/Sources/SwiftMail/IMAP/IMAP/Commands/IDCommand.swift index e3b11ba..8e0e463 100644 --- a/Sources/SwiftMail/IMAP/IMAP/Commands/IDCommand.swift +++ b/Sources/SwiftMail/IMAP/IMAP/Commands/IDCommand.swift @@ -4,17 +4,17 @@ import OrderedCollections /// Command for IMAP ID. struct IDCommand: IMAPCommand { - typealias ResultType = IDResponse + typealias ResultType = Identification typealias HandlerType = IDHandler /// Client identification parameters. - let parameters: OrderedDictionary + let identification: Identification - init(parameters: OrderedDictionary = [:]) { - self.parameters = parameters + init(identification: Identification = Identification()) { + self.identification = identification } func toTaggedCommand(tag: String) -> TaggedCommand { - TaggedCommand(tag: tag, command: .id(parameters)) + TaggedCommand(tag: tag, command: .id(identification.nioParameters)) } } diff --git a/Sources/SwiftMail/IMAP/IMAP/Handler/IDHandler.swift b/Sources/SwiftMail/IMAP/IMAP/Handler/IDHandler.swift index 573ad04..2ac5640 100644 --- a/Sources/SwiftMail/IMAP/IMAP/Handler/IDHandler.swift +++ b/Sources/SwiftMail/IMAP/IMAP/Handler/IDHandler.swift @@ -6,7 +6,7 @@ import NIOConcurrencyHelpers import OrderedCollections /// Handler for the IMAP ID command. -final class IDHandler: BaseIMAPCommandHandler, IMAPCommandHandler, @unchecked Sendable { +final class IDHandler: BaseIMAPCommandHandler, IMAPCommandHandler, @unchecked Sendable { private var responseParams: OrderedDictionary = [:] override func handleUntaggedResponse(_ response: Response) -> Bool { @@ -19,7 +19,7 @@ final class IDHandler: BaseIMAPCommandHandler, IMAPCommandHandler, @ override func handleTaggedOKResponse(_ response: TaggedResponse) { let params = lock.withLock { responseParams } - succeedWithResult(IDResponse(parameters: params)) + succeedWithResult(Identification(parameters: params)) } override func handleTaggedErrorResponse(_ response: TaggedResponse) { diff --git a/Sources/SwiftMail/IMAP/IMAPServer.swift b/Sources/SwiftMail/IMAP/IMAPServer.swift index 53c5280..0eef56a 100644 --- a/Sources/SwiftMail/IMAP/IMAPServer.swift +++ b/Sources/SwiftMail/IMAP/IMAPServer.swift @@ -212,19 +212,15 @@ public actor IMAPServer { } /// Identify the client to the server using the `ID` command. - /// - Parameter parameters: Key/value pairs describing the client. Pass an empty dictionary to send no information. + /// - Parameter identification: Information describing the client. Pass the default value to send no information. /// - Returns: Information returned by the server. /// - Throws: ``IMAPError.commandNotSupported`` if the server does not support the command or ``IMAPError.commandFailed`` on failure. - public func id(parameters: [String: String?] = [:]) async throws -> IDResponse { + public func id(_ identification: Identification = Identification()) async throws -> Identification { guard capabilities.contains(.id) else { throw IMAPError.commandNotSupported("ID command not supported by server") } - var ordered = OrderedDictionary() - for (key, value) in parameters { - ordered[key] = value - } - let command = IDCommand(parameters: ordered) + let command = IDCommand(identification: identification) return try await executeCommand(command) } diff --git a/Sources/SwiftMail/IMAP/Models/IDResponse.swift b/Sources/SwiftMail/IMAP/Models/IDResponse.swift deleted file mode 100644 index c31c383..0000000 --- a/Sources/SwiftMail/IMAP/Models/IDResponse.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import OrderedCollections - -/// Information returned by the IMAP `ID` command. -/// -/// The server may return any key/value pairs describing its -/// implementation. Values may be `nil` if the server omits them. -public struct IDResponse: Sendable { - /// Raw key/value pairs returned by the server. - public var parameters: OrderedDictionary - - /// Create a new response with the provided parameters. - public init(parameters: OrderedDictionary = [:]) { - self.parameters = parameters - } - - /// Access a value by key. - public subscript(key: String) -> String? { - parameters[key] ?? nil - } - - /// Commonly used name field of the server. - public var name: String? { self["name"] } - - /// Server version string if provided. - public var version: String? { self["version"] } -} diff --git a/Sources/SwiftMail/IMAP/Models/Identification.swift b/Sources/SwiftMail/IMAP/Models/Identification.swift new file mode 100644 index 0000000..a4853f7 --- /dev/null +++ b/Sources/SwiftMail/IMAP/Models/Identification.swift @@ -0,0 +1,92 @@ +import Foundation +import OrderedCollections + +/// Client or server identification information used with the IMAP `ID` command. +/// +/// The most common fields defined by RFC 2971 are provided as stored +/// properties. Additional parameters that do not match these fields can +/// be accessed via the subscript operator. +public struct Identification: Sendable { + /// Product name of the client or server. + public var name: String? + /// Product version string. + public var version: String? + /// Operating system name. + public var os: String? + /// Operating system version string. + public var osVersion: String? + /// Name of the vendor. + public var vendor: String? + /// Support contact URL. + public var supportURL: String? + + /// Any additional parameters returned by the server. + public var additional: [String: String?] + + /// Create a new identification value. + public init( + name: String? = nil, + version: String? = nil, + os: String? = nil, + osVersion: String? = nil, + vendor: String? = nil, + supportURL: String? = nil, + additional: [String: String?] = [:] + ) { + self.name = name + self.version = version + self.os = os + self.osVersion = osVersion + self.vendor = vendor + self.supportURL = supportURL + self.additional = additional + } + + /// Create an Identification from raw parameters received from NIOIMAP. + internal init(parameters: OrderedDictionary) { + self.name = parameters["name"] ?? nil + self.version = parameters["version"] ?? nil + self.os = parameters["os"] ?? nil + self.osVersion = parameters["os-version"] ?? nil + self.vendor = parameters["vendor"] ?? nil + self.supportURL = parameters["support-url"] ?? nil + var other: [String: String?] = [:] + for (key, value) in parameters where !Self.knownKeys.contains(key) { + other[key] = value + } + self.additional = other + } + + /// Access a parameter by key. + public subscript(key: String) -> String? { + switch key { + case "name": return name + case "version": return version + case "os": return os + case "os-version": return osVersion + case "vendor": return vendor + case "support-url": return supportURL + default: return additional[key] ?? nil + } + } + + /// Convert this Identification into the ordered dictionary format expected by NIOIMAP. + internal var nioParameters: OrderedDictionary { + var params: OrderedDictionary = [:] + params["name"] = name + params["version"] = version + params["os"] = os + params["os-version"] = osVersion + params["vendor"] = vendor + params["support-url"] = supportURL + for (k, v) in additional { + params[k] = v + } + return params + } + + /// Keys that map directly to stored properties. + private static let knownKeys: Set = [ + "name", "version", "os", "os-version", "vendor", "support-url" + ] +}