Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIDBATCHES Extension #777

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Sources/NIOIMAPCore/Grammar/Command/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public enum Command: Hashable, Sendable {
/// Retrieves the namespaces available to the user.
case namespace

/// UIDBATCHES to get partition UIDs into batches.
///
/// TODO: Add link to RFC
case uidBatches(batchSize: Int, batchRange: ClosedRange<Int>?)

/// Similar to `.copy`, but uses unique identifier instead of sequence numbers to identify messages.
case uidCopy(LastCommandSet<UID>, MailboxName)

Expand Down Expand Up @@ -307,6 +312,8 @@ extension CommandEncodeBuffer {
return self.writeCommandKind_urlFetch(urls: urls)
case .compress(let kind):
return self.writeCommandKind_compress(kind: kind)
case .uidBatches(batchSize: let size, batchRange: let range):
return self.writeCommandKind_uidBatches(batchSize: size, batchRange: range)
case .custom(name: let name, payloads: let payloads):
return self.writeCommandKind_custom(name: name, payloads: payloads)
}
Expand Down Expand Up @@ -465,6 +472,20 @@ extension CommandEncodeBuffer {
self.buffer.writeString("COMPRESS \(kind.rawValue)")
}

private mutating func writeCommandKind_uidBatches(batchSize: Int, batchRange: ClosedRange<Int>?) -> Int {
self.buffer.writeString("UIDBATCHES \(batchSize)")
+ self.buffer.writeIfExists(batchRange) {
let range =
UnknownMessageIdentifier(exactly: $0.lowerBound)!...UnknownMessageIdentifier(
exactly: $0.upperBound
)!
return self.buffer
.writeString(" ")
+ self.buffer
.writeMessageIdentifierRange(range)
}
}

private mutating func writeCommandKind_custom(name: String, payloads: [Command.CustomCommandPayload]) -> Int {
self.buffer.writeString("\(name)")
+ self.buffer.writeArray(payloads, prefix: " ", separator: "", parenthesis: false) { (payload, self) in
Expand Down Expand Up @@ -651,6 +672,13 @@ extension CommandEncodeBuffer {
// MARK: - Conveniences

extension Command {
/// Convenience for creating a `UIDBATCHES` command.
///
/// TODO: Add link to RFC
public static func uidBatches(batchSize: Int) -> Command {
return .uidBatches(batchSize: batchSize, batchRange: nil)
}

/// Convenience for creating a *UID MOVE* command.
/// Pass in a `UIDSet`, and if that set is valid (i.e. non-empty) then a command is returned.
/// - parameter messages: The set of message UIDs to use.
Expand Down
5 changes: 5 additions & 0 deletions Sources/NIOIMAPCore/Grammar/Mailbox/MailboxData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public enum MailboxData: Hashable, Sendable {

/// Response to a search-sort command, containing an array of identifiers and sequence information.
case searchSort(SearchSort)

/// TODO: Add link to RFC
case uidBatches(UIDBatchesResponse)
}

extension MailboxData {
Expand Down Expand Up @@ -101,6 +104,8 @@ extension EncodeBuffer {
return self.writeNamespaceResponse(namespaceResponse)
case .searchSort(let data):
return self.writeMailboxDataSearchSort(data)
case .uidBatches(let response):
return self.writeUIDBatchesResponse(response)
}
}

Expand Down
46 changes: 46 additions & 0 deletions Sources/NIOIMAPCore/Grammar/Response/UIDBatchesResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import struct NIO.ByteBuffer

/// Sent from a server in response to an extended search.
public struct UIDBatchesResponse: Hashable, Sendable {
/// The tag of the command that this search result is a response to.
public var correlator: String

/// Data returned from the search.
public var batches: [UIDRange]

/// Creates a new `UIDBatchesResponse`.
/// - parameter correlator: Identifies the command that resulted in this response.
/// - parameter batches: The UID batches returned by the command.
public init(correlator: String, batches: [UIDRange]) {
self.correlator = correlator
self.batches = batches
}
}

// MARK: - Encoding

extension EncodeBuffer {
@discardableResult mutating func writeUIDBatchesResponse(_ response: UIDBatchesResponse) -> Int {
self.writeString(#"UIDBATCHES (TAG "\#(response.correlator)")"#)
+ self.write(if: !response.batches.isEmpty) {
self.writeString(" ")
+ self.writeArray(response.batches, separator: ",", parenthesis: false) { range, buffer -> Int in
buffer.writeMessageIdentifierRange(range, descending: true)
}
}
}
}
11 changes: 8 additions & 3 deletions Sources/NIOIMAPCore/Grammar/UID/MessageIdentifierRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,15 @@ extension MessageIdentifierRange {
// MARK: - Encoding

extension EncodeBuffer {
@discardableResult mutating func writeMessageIdentifierRange<T>(_ range: MessageIdentifierRange<T>) -> Int {
self.writeMessageIdentifier(range.range.lowerBound)
@discardableResult mutating func writeMessageIdentifierRange<T>(
_ range: MessageIdentifierRange<T>,
descending: Bool = false
) -> Int {
let a = descending ? range.range.upperBound : range.range.lowerBound
let b = descending ? range.range.lowerBound : range.range.upperBound
return self.writeMessageIdentifier(a)
+ self.write(if: range.range.lowerBound < range.range.upperBound) {
self.writeString(":") + self.writeMessageIdentifier(range.range.upperBound)
self.writeString(":") + self.writeMessageIdentifier(b)
}
}
}
23 changes: 21 additions & 2 deletions Sources/NIOIMAPCore/Parser/Grammar/GrammarParser+Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ extension GrammarParser {
"SETQUOTA": self.parseCommandSuffix_setQuota,
"GETQUOTAROOT": self.parseCommandSuffix_getQuotaRoot,
"COMPRESS": self.parseCommandSuffix_compress,
"UIDBATCHES": self.parseCommandSuffix_uidBatched,
]
return try parseFromLookupTable(buffer: &buffer, tracker: tracker, parsers: commandParsers)
}
Expand Down Expand Up @@ -537,8 +538,6 @@ extension GrammarParser {
}
}

// compress = "COMPRESS" SP algorithm
// algorithm = "DEFLATE" (or any atom)
func parseCommandSuffix_compress(buffer: inout ParseBuffer, tracker: StackTracker) throws -> Command {
try PL.composite(buffer: &buffer, tracker: tracker) { buffer, tracker -> Command in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
Expand All @@ -548,6 +547,26 @@ extension GrammarParser {
}
}

// message-batches = "UIDBATCHES" SP nz-number
// [SP nz-number ":" nz-number]
func parseCommandSuffix_uidBatched(buffer: inout ParseBuffer, tracker: StackTracker) throws -> Command {
try PL.composite(buffer: &buffer, tracker: tracker) { buffer, tracker -> Command in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
let size = try self.parseNZNumber(buffer: &buffer, tracker: tracker)
let range: MessageIdentifierRange<UnknownMessageIdentifier>? = try PL.parseOptional(
buffer: &buffer,
tracker: tracker
) { buffer, tracker in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return try self.parseMessageIdentifierRange(buffer: &buffer, tracker: tracker)
}
let batchRange = range.map {
Int($0.lowerBound)...Int($0.upperBound)
}
return .uidBatches(batchSize: Int(size), batchRange: batchRange)
}
}

func parseSelectParameters(buffer: inout ParseBuffer, tracker: StackTracker) throws -> [SelectParameter] {
try PL.parseOptional(buffer: &buffer, tracker: tracker) { (buffer, tracker) -> [SelectParameter] in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
Expand Down
153 changes: 90 additions & 63 deletions Sources/NIOIMAPCore/Parser/Grammar/GrammarParser+Mailbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,66 +35,104 @@ extension GrammarParser {
// "STATUS" SP mailbox SP "(" [status-att-list] ")" /
// number SP "EXISTS" / Namespace-Response
func parseMailboxData(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
func parseMailboxData_flags(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("FLAGS ", buffer: &buffer, tracker: tracker)
return .flags(try self.parseFlagList(buffer: &buffer, tracker: tracker))
}
/// Parse those sub-parses that have a fixed text prefix.
func parseMailboxData_withFixedPrefix(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
func parseMailboxData_flags(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return .flags(try self.parseFlagList(buffer: &buffer, tracker: tracker))
}

func parseMailboxData_list(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("LIST ", buffer: &buffer, tracker: tracker)
return .list(try self.parseMailboxList(buffer: &buffer, tracker: tracker))
}
func parseMailboxData_list(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return .list(try self.parseMailboxList(buffer: &buffer, tracker: tracker))
}

func parseMailboxData_lsub(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("LSUB ", buffer: &buffer, tracker: tracker)
return .lsub(try self.parseMailboxList(buffer: &buffer, tracker: tracker))
}
func parseMailboxData_lsub(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return .lsub(try self.parseMailboxList(buffer: &buffer, tracker: tracker))
}

func parseMailboxData_extendedSearch(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
let response = try self.parseExtendedSearchResponse(buffer: &buffer, tracker: tracker)
return .extendedSearch(response)
}
func parseMailboxData_extendedSearch(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData
{
let response = try self.parseExtendedSearchResponse(buffer: &buffer, tracker: tracker)
return .extendedSearch(response)
}

func parseMailboxData_search(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("SEARCH", buffer: &buffer, tracker: tracker)
let nums = try PL.parseZeroOrMore(buffer: &buffer, tracker: tracker) {
(buffer, tracker) -> UnknownMessageIdentifier in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
let num = try self.parseNZNumber(buffer: &buffer, tracker: tracker)
guard let id = UnknownMessageIdentifier(exactly: num) else {
throw ParserError(hint: "Can't make unknown message identfiier from \(num)")
func parseMailboxData_search_combined(
buffer: inout ParseBuffer,
tracker: StackTracker
) throws -> MailboxData {
func parseMailboxData_search(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
let nums = try PL.parseZeroOrMore(buffer: &buffer, tracker: tracker) {
(buffer, tracker) -> UnknownMessageIdentifier in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
let num = try self.parseNZNumber(buffer: &buffer, tracker: tracker)
guard let id = UnknownMessageIdentifier(exactly: num) else {
throw ParserError(hint: "Can't make unknown message identfiier from \(num)")
}
return id
}
return .search(nums)
}
return id
}
return .search(nums)
}

func parseMailboxData_searchSort(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("SEARCH", buffer: &buffer, tracker: tracker)
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
var array = [try self.parseNZNumber(buffer: &buffer, tracker: tracker)]
try PL.parseZeroOrMore(
buffer: &buffer,
into: &array,
tracker: tracker,
parser: { (buffer, tracker) in
func parseMailboxData_searchSort(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData
{
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
var array = [try self.parseNZNumber(buffer: &buffer, tracker: tracker)]
try PL.parseZeroOrMore(
buffer: &buffer,
into: &array,
tracker: tracker,
parser: { (buffer, tracker) in
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return try self.parseNZNumber(buffer: &buffer, tracker: tracker)
}
)
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
return try self.parseNZNumber(buffer: &buffer, tracker: tracker)
let seq = try self.parseSearchSortModificationSequence(buffer: &buffer, tracker: tracker)
return .searchSort(.init(identifiers: array, modificationSequence: seq))
}
)
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
let seq = try self.parseSearchSortModificationSequence(buffer: &buffer, tracker: tracker)
return .searchSort(.init(identifiers: array, modificationSequence: seq))
}

func parseMailboxData_status(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseFixedString("STATUS ", buffer: &buffer, tracker: tracker)
let mailbox = try self.parseMailbox(buffer: &buffer, tracker: tracker)
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
try PL.parseFixedString("(", buffer: &buffer, tracker: tracker)
let status = try PL.parseOptional(buffer: &buffer, tracker: tracker, parser: self.parseMailboxStatus)
try PL.parseFixedString(")", buffer: &buffer, tracker: tracker)
return .status(mailbox, status ?? .init())
return try PL.parseOneOf(
parseMailboxData_searchSort,
parseMailboxData_search,
buffer: &buffer,
tracker: tracker
)
}

func parseMailboxData_status(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
let mailbox = try self.parseMailbox(buffer: &buffer, tracker: tracker)
try PL.parseSpaces(buffer: &buffer, tracker: tracker)
try PL.parseFixedString("(", buffer: &buffer, tracker: tracker)
let status = try PL.parseOptional(buffer: &buffer, tracker: tracker, parser: self.parseMailboxStatus)
try PL.parseFixedString(")", buffer: &buffer, tracker: tracker)
return .status(mailbox, status ?? .init())
}

func parseMailboxData_namespace(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
.namespace(try self.parseNamespaceResponse(buffer: &buffer, tracker: tracker))
}

func parseMailboxData_uidBatchesResponse(
buffer: inout ParseBuffer,
tracker: StackTracker
) throws -> MailboxData {
.uidBatches(try self.parseUIDBatchesResponse(buffer: &buffer, tracker: tracker))
}

let commandParsers: [String: (inout ParseBuffer, StackTracker) throws -> MailboxData] = [
"FLAGS": parseMailboxData_flags,
"LIST": parseMailboxData_list,
"LSUB": parseMailboxData_lsub,
"ESEARCH": parseMailboxData_extendedSearch,
"SEARCH": parseMailboxData_search_combined,
"STATUS": parseMailboxData_status,
"NAMESPACE": parseMailboxData_namespace,
"UIDBATCHES": parseMailboxData_uidBatchesResponse,
]
return try parseFromLookupTable(buffer: &buffer, tracker: tracker, parsers: commandParsers)
}

func parseMailboxData_exists(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
Expand All @@ -109,22 +147,11 @@ extension GrammarParser {
return .recent(number)
}

func parseMailboxData_namespace(buffer: inout ParseBuffer, tracker: StackTracker) throws -> MailboxData {
.namespace(try self.parseNamespaceResponse(buffer: &buffer, tracker: tracker))
}

return try PL.parseOneOf(
[
parseMailboxData_flags,
parseMailboxData_list,
parseMailboxData_lsub,
parseMailboxData_extendedSearch,
parseMailboxData_status,
parseMailboxData_withFixedPrefix,
parseMailboxData_exists,
parseMailboxData_recent,
parseMailboxData_searchSort,
parseMailboxData_search,
parseMailboxData_namespace,
],
buffer: &buffer,
tracker: tracker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ extension GrammarParser {
}

func parseSuffix_namespace(buffer: inout ParseBuffer, tracker: StackTracker) throws -> ResponseTextCode {
.namespace(try self.parseNamespaceSuffix(buffer: &buffer, tracker: tracker))
.namespace(try self.parseNamespaceResponse(buffer: &buffer, tracker: tracker))
}

func parseResponseTextCode_atom(buffer: inout ParseBuffer, tracker: StackTracker) throws -> ResponseTextCode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ extension GrammarParser {
guard let id2 = id2 else {
return MessageIdentifierRange(id1)
}
guard id1 <= id2 else {
throw ParserError(hint: "Invalid range, \(id1):\(id2)")
}
return MessageIdentifierRange(id1...id2)
return (id1 <= id2) ? MessageIdentifierRange(id1...id2) : MessageIdentifierRange(id2...id1)
}
}

Expand Down
Loading
Loading