Files
MagnumOpus/Packages/MagnumOpusCore/Sources/IMAPClient/IMAPConnection.swift
Felix Förtsch 427f197bb3 add IMAP write operations, special folder role detection
Extend IMAPClientProtocol with storeFlags, moveMessage, copyMessage,
expunge, appendMessage, capabilities methods. Implement all six in
IMAPClient actor using NIOIMAPCore typed commands. Add multi-part
command support to IMAPConnection/IMAPCommandRunner for APPEND.
MockIMAPClient tracks all write calls for testing. SyncCoordinator
detects mailbox roles from LIST attributes with name-based fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 05:13:39 +01:00

71 lines
2.2 KiB
Swift

import NIO
import NIOIMAPCore
@preconcurrency import NIOIMAP
@preconcurrency import NIOSSL
actor IMAPConnection {
private let host: String
private let port: Int
private let group: EventLoopGroup
private var channel: Channel?
private let responseHandler: IMAPResponseHandler
init(host: String, port: Int) {
self.host = host
self.port = port
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.responseHandler = IMAPResponseHandler()
}
func connect() async throws {
let sslContext = try NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration())
let handler = responseHandler
let hostname = host
let bootstrap = ClientBootstrap(group: group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelInitializer { channel in
let sslHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: hostname)
return channel.pipeline.addHandlers([
sslHandler,
IMAPClientHandler(),
handler,
])
}
channel = try await bootstrap.connect(host: host, port: port).get()
try await handler.waitForGreeting()
}
func sendCommand(_ tag: String, command: CommandStreamPart) async throws -> [Response] {
guard let channel else { throw IMAPError.notConnected }
let handler = responseHandler
return try await withCheckedThrowingContinuation { continuation in
handler.sendCommand(tag: tag, continuation: continuation)
channel.writeAndFlush(IMAPClientHandler.Message.part(command), promise: nil)
}
}
/// Send multiple command parts as a single multi-part command (e.g. APPEND).
/// The tag must match the tag embedded in the first part.
func sendMultiPartCommand(_ tag: String, parts: [CommandStreamPart]) async throws -> [Response] {
guard let channel else { throw IMAPError.notConnected }
let handler = responseHandler
return try await withCheckedThrowingContinuation { continuation in
handler.sendCommand(tag: tag, continuation: continuation)
for part in parts {
channel.writeAndFlush(IMAPClientHandler.Message.part(part), promise: nil)
}
}
}
func disconnect() async throws {
try await channel?.close()
channel = nil
}
func shutdown() async throws {
try await group.shutdownGracefully()
}
}