diff --git a/Sources/NIOIMAPCore/Grammar/Command/Command.swift b/Sources/NIOIMAPCore/Grammar/Command/Command.swift index 5df9ffb7e..b6272f17a 100644 --- a/Sources/NIOIMAPCore/Grammar/Command/Command.swift +++ b/Sources/NIOIMAPCore/Grammar/Command/Command.swift @@ -184,6 +184,12 @@ public enum Command: Hashable { /// Instructs the server to use the named compression mechanism. case compress(Capability.CompressionKind) + + /// A custom command that’s not defined in any RFC. + /// + /// If `payload` contains multiple elements, no spaces or other separators will be output + /// between them. A `.verbatim` element must be used to output spaces if desired. + case custom(name: String, payloads: [CustomCommandPayload]) } extension Command: CustomDebugStringConvertible { @@ -297,6 +303,8 @@ extension CommandEncodeBuffer { return self.writeCommandKind_urlFetch(urls: urls) case .compress(let kind): return self.writeCommandKind_compress(kind: kind) + case .custom(name: let name, payloads: let payloads): + return self.writeCommandKind_custom(name: name, payloads: payloads) } } @@ -456,6 +464,13 @@ extension CommandEncodeBuffer { self.buffer.writeString("COMPRESS \(kind.rawValue)") } + 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 + self.writeCustomCommandPayload(payload) + } + } + private mutating func writeCommandKind_authenticate(mechanism: AuthenticationMechanism, initialResponse: InitialResponse?) -> Int { self.buffer.writeString("AUTHENTICATE ") + self.buffer.writeAuthenticationMechanism(mechanism) + diff --git a/Sources/NIOIMAPCore/Grammar/Command/CustomCommandPayload.swift b/Sources/NIOIMAPCore/Grammar/Command/CustomCommandPayload.swift new file mode 100644 index 000000000..ae0394dd0 --- /dev/null +++ b/Sources/NIOIMAPCore/Grammar/Command/CustomCommandPayload.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +extension Command { + public enum CustomCommandPayload: Hashable { + /// This will be encoded using `quoted` or `literal`. + case literal(ByteBuffer) + /// This will be encoded _verbatim_, i.e. directly copied to the output buffer without change. + case verbatim(ByteBuffer) + } +} + +// MARK: - + +extension EncodeBuffer { + /// Writes a `CustomCommandPayload` to the buffer ready to be sent to the network. + /// - parameter stream: The `CustomCommandPayload` to write. + /// - returns: The number of bytes written. + @discardableResult public mutating func writeCustomCommandPayload(_ payload: Command.CustomCommandPayload) -> Int { + switch payload { + case .literal(let literal): + return self.writeIMAPString(literal) + case .verbatim(let verbatim): + return self.writeBytes(verbatim.readableBytesView) + } + } +} diff --git a/Sources/NIOIMAPCore/Pipelining.swift b/Sources/NIOIMAPCore/Pipelining.swift index c4f229b0c..6392ead42 100644 --- a/Sources/NIOIMAPCore/Pipelining.swift +++ b/Sources/NIOIMAPCore/Pipelining.swift @@ -225,6 +225,14 @@ extension Command { .generateAuthorizedURL, .urlFetch: return [] + case .custom: + return [ + .noMailboxCommandsRunning, + .noUntaggedExpungeResponse, + .noUIDBasedCommandRunning, + .noFlagChanges(.all), + .noFlagReads(.all), + ] } } } @@ -379,6 +387,8 @@ extension Command { .setMetadata: // TODO: Metadata dependencies? return [.mayTriggerUntaggedExpunge] + case .custom: + return [.barrier] } } } diff --git a/Tests/NIOIMAPCoreTests/Grammar/CommandType/CommandType+Tests.swift b/Tests/NIOIMAPCoreTests/Grammar/CommandType/CommandType+Tests.swift index d1a0e7f44..c5824045c 100644 --- a/Tests/NIOIMAPCoreTests/Grammar/CommandType/CommandType+Tests.swift +++ b/Tests/NIOIMAPCoreTests/Grammar/CommandType/CommandType+Tests.swift @@ -64,6 +64,17 @@ extension CommandType_Tests { (.create(.inbox, []), CommandEncodingOptions(), ["CREATE \"INBOX\""], #line), (.create(.inbox, [.attributes([.archive, .drafts, .flagged])]), CommandEncodingOptions(), ["CREATE \"INBOX\" (USE (\\Archive \\Drafts \\Flagged))"], #line), (.compress(.deflate), CommandEncodingOptions(), ["COMPRESS DEFLATE"], #line), + + // Custom + + (.custom(name: "FOOBAR", payloads: []), CommandEncodingOptions(), ["FOOBAR"], #line), + (.custom(name: "FOOBAR", payloads: [.verbatim(.init(string: "A B C"))]), CommandEncodingOptions(), ["FOOBAR A B C"], #line), + (.custom(name: "FOOBAR", payloads: [.verbatim(.init(string: "A")), .verbatim(.init(string: "B"))]), CommandEncodingOptions(), ["FOOBAR AB"], #line), + (.custom(name: "FOOBAR", payloads: [.literal(.init(string: "A"))]), CommandEncodingOptions(), [#"FOOBAR "A""#], #line), + (.custom(name: "FOOBAR", payloads: [.literal(.init(string: "A B C"))]), CommandEncodingOptions(), [#"FOOBAR "A B C""#], #line), + (.custom(name: "FOOBAR", payloads: [.literal(.init(string: "A")), .literal(.init(string: "B"))]), CommandEncodingOptions(), [#"FOOBAR "A""B""#], #line), + (.custom(name: "FOOBAR", payloads: [.literal(.init(string: "A")), .verbatim(.init(string: " ")), .literal(.init(string: "B"))]), CommandEncodingOptions(), [#"FOOBAR "A" "B""#], #line), + (.custom(name: "FOOBAR", payloads: [.literal(.init(string: "¶"))]), CommandEncodingOptions(), ["FOOBAR {2}\r\n", "¶"], #line), ] for (test, options, expectedStrings, line) in inputs { diff --git a/Tests/NIOIMAPCoreTests/Parser/Grammar/GrammarParser+Commands+Tests.swift b/Tests/NIOIMAPCoreTests/Parser/Grammar/GrammarParser+Commands+Tests.swift index ade0c62aa..5508945be 100644 --- a/Tests/NIOIMAPCoreTests/Parser/Grammar/GrammarParser+Commands+Tests.swift +++ b/Tests/NIOIMAPCoreTests/Parser/Grammar/GrammarParser+Commands+Tests.swift @@ -81,7 +81,7 @@ extension GrammarParser_Commands_Tests { } // Minimum 1 valid test for each command to ensure all commands are supported - // dedicated unit tests areprovided for each sub-parser + // dedicated unit tests are provided for each sub-parser func testParseCommand() { self.iterateTests( testFunction: GrammarParser().parseCommand,