Skip to content

Commit

Permalink
Add support for encoding custom commands
Browse files Browse the repository at this point in the history
This _only_ handles _encoding_ of custom commands.

This simply adds a
```swift
case custom(name: String, payloads: [CustomCommandPayload])
```
such that clients of this library do their own encoding of such commands.

This is related to the discussion in #92
  • Loading branch information
danieleggert committed Feb 19, 2022
1 parent 82c191e commit dd093ef
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 1 deletion.
15 changes: 15 additions & 0 deletions Sources/NIOIMAPCore/Grammar/Command/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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) +
Expand Down
40 changes: 40 additions & 0 deletions Sources/NIOIMAPCore/Grammar/Command/CustomCommandPayload.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
10 changes: 10 additions & 0 deletions Sources/NIOIMAPCore/Pipelining.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ extension Command {
.generateAuthorizedURL,
.urlFetch:
return []
case .custom:
return [
.noMailboxCommandsRunning,
.noUntaggedExpungeResponse,
.noUIDBasedCommandRunning,
.noFlagChanges(.all),
.noFlagReads(.all),
]
}
}
}
Expand Down Expand Up @@ -379,6 +387,8 @@ extension Command {
.setMetadata:
// TODO: Metadata dependencies?
return [.mayTriggerUntaggedExpunge]
case .custom:
return [.barrier]
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions Tests/NIOIMAPCoreTests/Grammar/CommandType/CommandType+Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit dd093ef

Please sign in to comment.