mirror of
https://github.com/Cocoanetics/SwiftMail.git
synced 2026-03-17 20:02:25 +00:00
337 lines
11 KiB
Swift
337 lines
11 KiB
Swift
// 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<Identifier: MessageIdentifier> {
|
|
/// 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<Int>) {
|
|
self.indexSet = Foundation.IndexSet(integersIn: range)
|
|
}
|
|
|
|
/// Creates a set containing integers from a lower bound to the maximum value
|
|
public init(_ range: PartialRangeFrom<Int>) {
|
|
// 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<Int>...) {
|
|
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<Identifier>) {
|
|
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<Identifier>) {
|
|
// 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<Int>) {
|
|
indexSet.insert(integersIn: range)
|
|
}
|
|
|
|
/// Inserts integers from a lower bound to the maximum value
|
|
public mutating func insert(range: PartialRangeFrom<Int>) {
|
|
indexSet.insert(integersIn: range.lowerBound...Int.max)
|
|
}
|
|
|
|
/// Inserts identifiers in the given range into the set
|
|
public mutating func insert(range: ClosedRange<Identifier>) {
|
|
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<Identifier>) {
|
|
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<Int>...) {
|
|
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<Int>] {
|
|
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<UID>
|
|
|
|
/// A type-safe set of sequence numbers
|
|
public typealias SequenceNumberSet = MessageIdentifierSet<SequenceNumber>
|
|
|
|
// MARK: - NIO Conversion Extensions
|
|
|
|
extension MessageIdentifierSet {
|
|
/// Converts to NIO MessageIdentifierSetNonEmpty
|
|
/// This method uses type constraints to determine the correct NIO type
|
|
internal func toNIOSet<NIOType>() -> NIOIMAPCore.MessageIdentifierSetNonEmpty<NIOType> {
|
|
|
|
precondition(!self.isEmpty, "Cannot convert an empty set to NIO")
|
|
|
|
// Create an empty NIO set
|
|
var nioSet = NIOIMAPCore.MessageIdentifierSet<NIOType>()
|
|
|
|
// 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<NIOType>(nioRange as! NIOIMAPCore.MessageIdentifierRange<NIOType>))
|
|
} 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<NIOType>(nioRange as! NIOIMAPCore.MessageIdentifierRange<NIOType>))
|
|
} 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<NIOIMAPCore.UID>? {
|
|
if self.isEmpty {
|
|
return nil
|
|
}
|
|
|
|
var nioSet = NIOIMAPCore.MessageIdentifierSet<NIOIMAPCore.UID>()
|
|
|
|
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<NIOIMAPCore.UID>(nioRange))
|
|
}
|
|
|
|
return NIOIMAPCore.MessageIdentifierSetNonEmpty(set: nioSet)
|
|
}
|
|
}
|
|
|
|
extension MessageIdentifierSet where Identifier == SequenceNumber {
|
|
/// Converts to NIO MessageIdentifierSetNonEmpty for SequenceNumber
|
|
internal func toNIOSet() -> NIOIMAPCore.MessageIdentifierSetNonEmpty<NIOIMAPCore.SequenceNumber>? {
|
|
if self.isEmpty {
|
|
return nil
|
|
}
|
|
|
|
var nioSet = NIOIMAPCore.MessageIdentifierSet<NIOIMAPCore.SequenceNumber>()
|
|
|
|
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<NIOIMAPCore.SequenceNumber>(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
|
|
}
|
|
*/
|