// MessageIdentifierSet.swift // Implementation of type-safe sets for IMAP message identifiers import Foundation import NIOIMAPCore // MARK: - MessageIdentifier Protocol /// Protocol for message identifiers (UID and SequenceNumber) public protocol MessageIdentifier: Hashable, Comparable { var value: UInt32 { get } init(_ value: UInt32) static var latest: Self { get } } extension MessageIdentifier { public init(_ value: Int) { self.init(UInt32(value)) } public static func < (lhs: Self, rhs: Self) -> Bool { return lhs.value < rhs.value } } // MARK: - UID Implementation /// Represents a Unique Identifier (UID) in IMAP public struct UID: MessageIdentifier, Sendable { public let value: UInt32 public init(_ value: UInt32) { self.value = value } public static let latest = UID(UInt32.max) // Convert to NIO UID internal func toNIO() -> NIOIMAPCore.UID { return NIOIMAPCore.UID(rawValue: self.value) } } // MARK: - SequenceNumber Implementation /// Represents a Sequence Number in IMAP public struct SequenceNumber: MessageIdentifier { public let value: UInt32 public init(_ value: UInt32) { self.value = value } public static let latest = SequenceNumber(UInt32.max) // Convert to NIO SequenceNumber internal func toNIO() -> NIOIMAPCore.SequenceNumber { return NIOIMAPCore.SequenceNumber(rawValue: self.value) } } // MARK: - MessageIdentifierSet Implementation /// A type-safe set for message identifiers that provides an efficient /// representation of non-contiguous ranges public struct MessageIdentifierSet { /// The underlying Foundation IndexSet that stores the actual data private var indexSet: Foundation.IndexSet /// Creates an empty set public init() { self.indexSet = Foundation.IndexSet() } /// Creates a set containing the given integer public init(_ value: Int) { self.indexSet = Foundation.IndexSet(integer: value) } /// Creates a set containing integers in the given range public init(_ range: ClosedRange) { self.indexSet = Foundation.IndexSet(integersIn: range) } /// Creates a set containing integers from a lower bound to the maximum value public init(_ range: PartialRangeFrom) { // Use Int.max as the upper bound for partial ranges self.indexSet = Foundation.IndexSet(integersIn: range.lowerBound...Int(UInt32.max)) } /// Creates a set containing integers in the given ranges public init(ranges: ClosedRange...) { self.indexSet = Foundation.IndexSet() for range in ranges { self.indexSet.insert(integersIn: range) } } /// Creates a set from a comma-separated string like "1-3,5-10" public init?(string: String) { self.indexSet = Foundation.IndexSet() let components = string.components(separatedBy: ",") for component in components { let rangeParts = component.components(separatedBy: "-") if rangeParts.count == 1, let value = Int(rangeParts[0].trimmingCharacters(in: .whitespaces)) { // Single value self.indexSet.insert(value) } else if rangeParts.count == 2, let lower = Int(rangeParts[0].trimmingCharacters(in: .whitespaces)), let upper = Int(rangeParts[1].trimmingCharacters(in: .whitespaces)) { // Range self.indexSet.insert(integersIn: lower...upper) } else { return nil // Invalid format } } } /// Creates a set containing the given identifier public init(_ identifier: Identifier) { self.indexSet = Foundation.IndexSet(integer: Int(identifier.value)) } /// Creates a set from an array of identifiers public init(_ identifiers: [Identifier]) { self.indexSet = Foundation.IndexSet() for identifier in identifiers { self.indexSet.insert(Int(identifier.value)) } } /// Creates a set containing identifiers in the given range public init(_ range: ClosedRange) { self.indexSet = Foundation.IndexSet(integersIn: Int(range.lowerBound.value)...Int(range.upperBound.value)) } /// Creates a set containing identifiers from a lower bound to the maximum value public init(_ range: PartialRangeFrom) { // Use the type's 'latest' value as the upper bound self.indexSet = Foundation.IndexSet(integersIn: Int(range.lowerBound.value)...Int(UInt32.max)) } /// Inserts the given integer into the set public mutating func insert(_ value: Int) { indexSet.insert(value) } /// Inserts the given identifier into the set public mutating func insert(_ identifier: Identifier) { indexSet.insert(Int(identifier.value)) } /// Inserts integers in the given range into the set public mutating func insert(range: ClosedRange) { indexSet.insert(integersIn: range) } /// Inserts integers from a lower bound to the maximum value public mutating func insert(range: PartialRangeFrom) { indexSet.insert(integersIn: range.lowerBound...Int.max) } /// Inserts identifiers in the given range into the set public mutating func insert(range: ClosedRange) { indexSet.insert(integersIn: Int(range.lowerBound.value)...Int(range.upperBound.value)) } /// Inserts identifiers from a lower bound to the maximum value public mutating func insert(range: PartialRangeFrom) { indexSet.insert(integersIn: Int(range.lowerBound.value)...Int(UInt32.max)) } /// Inserts integers in the given ranges into the set public mutating func insert(ranges: ClosedRange...) { for range in ranges { indexSet.insert(integersIn: range) } } /// Returns true if the set contains the given integer public func contains(_ value: Int) -> Bool { return indexSet.contains(value) } /// Returns true if the set contains the given identifier public func contains(_ identifier: Identifier) -> Bool { return indexSet.contains(Int(identifier.value)) } /// Returns true if the set is empty public var isEmpty: Bool { return indexSet.isEmpty } /// Returns the number of integers in the set public var count: Int { return indexSet.count } /// Returns a view of the ranges in the set public var ranges: [ClosedRange] { return indexSet.rangeView.map { $0.lowerBound...$0.upperBound - 1 } } /// Converts the set to an array of identifiers public func toArray() -> [Identifier] { return indexSet.map { Identifier(UInt32($0)) } } } // MARK: - Type Aliases /// A type-safe set of UIDs public typealias UIDSet = MessageIdentifierSet /// A type-safe set of sequence numbers public typealias SequenceNumberSet = MessageIdentifierSet // MARK: - NIO Conversion Extensions extension MessageIdentifierSet { /// Converts to NIO MessageIdentifierSetNonEmpty /// This method uses type constraints to determine the correct NIO type internal func toNIOSet() -> NIOIMAPCore.MessageIdentifierSetNonEmpty { precondition(!self.isEmpty, "Cannot convert an empty set to NIO") // Create an empty NIO set var nioSet = NIOIMAPCore.MessageIdentifierSet() // Convert each range to a NIO range and add it to the set for range in self.ranges { if Identifier.self == UID.self && NIOType.self == NIOIMAPCore.UID.self { let startUID = NIOIMAPCore.UID(rawValue: UInt32(range.lowerBound)) let endUID = NIOIMAPCore.UID(rawValue: UInt32(range.upperBound)) let nioRange = NIOIMAPCore.MessageIdentifierRange(startUID...endUID) nioSet.formUnion(NIOIMAPCore.MessageIdentifierSet(nioRange as! NIOIMAPCore.MessageIdentifierRange)) } else if Identifier.self == SequenceNumber.self && NIOType.self == NIOIMAPCore.SequenceNumber.self { let startSeq = NIOIMAPCore.SequenceNumber(rawValue: UInt32(range.lowerBound)) let endSeq = NIOIMAPCore.SequenceNumber(rawValue: UInt32(range.upperBound)) let nioRange = NIOIMAPCore.MessageIdentifierRange(startSeq...endSeq) nioSet.formUnion(NIOIMAPCore.MessageIdentifierSet(nioRange as! NIOIMAPCore.MessageIdentifierRange)) } else { preconditionFailure("Unsupported type combination") } } return NIOIMAPCore.MessageIdentifierSetNonEmpty(set: nioSet)! } } extension MessageIdentifierSet where Identifier == UID { /// Converts to NIO MessageIdentifierSetNonEmpty for UID internal func toNIOSet() -> NIOIMAPCore.MessageIdentifierSetNonEmpty? { if self.isEmpty { return nil } var nioSet = NIOIMAPCore.MessageIdentifierSet() for range in self.ranges { let startUID = NIOIMAPCore.UID(rawValue: UInt32(range.lowerBound)) let endUID = NIOIMAPCore.UID(rawValue: UInt32(range.upperBound)) let nioRange = NIOIMAPCore.MessageIdentifierRange(startUID...endUID) nioSet.formUnion(NIOIMAPCore.MessageIdentifierSet(nioRange)) } return NIOIMAPCore.MessageIdentifierSetNonEmpty(set: nioSet) } } extension MessageIdentifierSet where Identifier == SequenceNumber { /// Converts to NIO MessageIdentifierSetNonEmpty for SequenceNumber internal func toNIOSet() -> NIOIMAPCore.MessageIdentifierSetNonEmpty? { if self.isEmpty { return nil } var nioSet = NIOIMAPCore.MessageIdentifierSet() for range in self.ranges { let startSeq = NIOIMAPCore.SequenceNumber(rawValue: UInt32(range.lowerBound)) let endSeq = NIOIMAPCore.SequenceNumber(rawValue: UInt32(range.upperBound)) let nioRange = NIOIMAPCore.MessageIdentifierRange(startSeq...endSeq) nioSet.formUnion(NIOIMAPCore.MessageIdentifierSet(nioRange)) } return NIOIMAPCore.MessageIdentifierSetNonEmpty(set: nioSet) } } // MARK: - Conversion from NIO types extension UID { /// Create a UID from a NIO UID public init(nio: NIOIMAPCore.UID) { self.init(nio.rawValue) } } extension SequenceNumber { /// Create a SequenceNumber from a NIO SequenceNumber public init(nio: NIOIMAPCore.SequenceNumber) { self.init(nio.rawValue) } } // MARK: - Example Usage /* // Create a set with a single UID var uidSet = UIDSet(UID(1)) // Create a set with a range of UIDs let rangeSet = UIDSet(UID(1)...UID(10)) // Create a set with multiple ranges let multiRangeSet = UIDSet(ranges: 1...3, 5...10) // Create a set from a string if let setFromString = UIDSet(string: "1-3,5-10") { // Use the set } // Add a UID to the set uidSet.insert(UID(4)) // Add a range of UIDs uidSet.insert(range: UID(5)...UID(10)) // Convert to NIO MessageIdentifierSetNonEmpty for use with the IMAP library if let nioSet = uidSet.toNIOSet() { // Use with NIO IMAP library } */