fix code review issues: deferral date format, storeFlags SELECT, event loop leaks, GTD selection tracking
- fix deferral resurfacing using VTODOParser.formatDateOnly instead of ISO8601 (date format mismatch) - add SELECT mailbox before UID STORE in storeFlags (IMAP protocol requirement) - pass credentials to SyncCoordinator so IDLE monitoring activates - add selectedItem tracking to MailViewModel, wire List selection in GTD views - fix startPeriodicSync to sleep-first, preventing duplicate sync on launch - add deinit cleanup for EventLoopGroup in IMAPConnection, SMTPConnection - use separate IMAP client for attachment downloads, avoid shared connection interference - remove [weak self] from IMAPIdleClient actor Task to prevent orphaned connections - fix isGTDPerspective to check selectedMailbox instead of items.isEmpty - fix fetchBody to use complete RFC822 fetch instead of BODY[TEXT] - reuse single IMAP connection per ActionQueue.flush() batch - add requiresIMAP to ActionPayload for connection batching - load task categories from label store instead of hardcoded empty array - suppress NIOSSLHandler Sendable warnings via Package.swift unsafeFlags - fix unused variable warnings across codebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,23 +152,24 @@ public actor IMAPClient: IMAPClientProtocol {
|
||||
}
|
||||
|
||||
public func fetchBody(uid: Int) async throws -> (text: String?, html: String?) {
|
||||
guard var runner else { throw IMAPError.notConnected }
|
||||
let uidValue = UID(rawValue: UInt32(uid))
|
||||
let range = MessageIdentifierRange<UID>(uidValue...uidValue)
|
||||
let set = MessageIdentifierSetNonEmpty<UID>(range: range)
|
||||
let responses = try await runner.run(.uidFetch(
|
||||
.set(set),
|
||||
[.bodySection(peek: true, SectionSpecifier(kind: .text), nil)],
|
||||
[]
|
||||
))
|
||||
self.runner = runner
|
||||
return parseBodyResponse(responses)
|
||||
// Fetch the complete RFC822 message and parse with MIME-aware logic
|
||||
let raw = try await fetchFullMessage(uid: uid)
|
||||
guard !raw.isEmpty else { return (text: nil, html: nil) }
|
||||
return parseBodyResponse(raw)
|
||||
}
|
||||
|
||||
// MARK: - Write operations
|
||||
|
||||
public func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws {
|
||||
guard var runner else { throw IMAPError.notConnected }
|
||||
|
||||
// SELECT the mailbox before UID STORE — IMAP requires it
|
||||
let selectResponses = try await runner.run(.select(MailboxName(ByteBuffer(string: mailbox))))
|
||||
self.runner = runner
|
||||
guard selectResponses.contains(where: { isOKTagged($0) }) else {
|
||||
throw IMAPError.unexpectedResponse("SELECT \(mailbox) failed")
|
||||
}
|
||||
|
||||
let uidValue = UID(rawValue: UInt32(uid))
|
||||
let range = MessageIdentifierRange<UID>(uidValue...uidValue)
|
||||
let set = MessageIdentifierSetNonEmpty<UID>(range: range)
|
||||
@@ -516,28 +517,21 @@ public actor IMAPClient: IMAPClientProtocol {
|
||||
return String(buffer: bodyBuffer)
|
||||
}
|
||||
|
||||
private func parseBodyResponse(_ responses: [Response]) -> (text: String?, html: String?) {
|
||||
var bodyBuffer = ByteBuffer()
|
||||
|
||||
for response in responses {
|
||||
if case .fetch(let fetchResponse) = response {
|
||||
switch fetchResponse {
|
||||
case .streamingBytes(let bytes):
|
||||
var mutableBytes = bytes
|
||||
bodyBuffer.writeBuffer(&mutableBytes)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
private func parseBodyResponse(_ raw: String) -> (text: String?, html: String?) {
|
||||
// Extract body after blank line (headers end at \r\n\r\n)
|
||||
let bodyString: String
|
||||
if let range = raw.range(of: "\r\n\r\n") {
|
||||
bodyString = String(raw[range.upperBound...])
|
||||
} else if let range = raw.range(of: "\n\n") {
|
||||
bodyString = String(raw[range.upperBound...])
|
||||
} else {
|
||||
bodyString = raw
|
||||
}
|
||||
|
||||
guard bodyBuffer.readableBytes > 0 else {
|
||||
guard !bodyString.isEmpty else {
|
||||
return (text: nil, html: nil)
|
||||
}
|
||||
|
||||
let bodyString = String(buffer: bodyBuffer)
|
||||
|
||||
// Simple heuristic: if the body contains HTML tags, treat as HTML
|
||||
let lowered = bodyString.lowercased()
|
||||
if lowered.contains("<html") || lowered.contains("<body") || lowered.contains("<div") {
|
||||
return (text: nil, html: bodyString)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NIO
|
||||
import NIOIMAPCore
|
||||
@preconcurrency import NIOIMAP
|
||||
@preconcurrency import NIOSSL
|
||||
import NIOIMAP
|
||||
import NIOSSL
|
||||
|
||||
actor IMAPConnection {
|
||||
private let host: String
|
||||
@@ -17,16 +17,20 @@ actor IMAPConnection {
|
||||
self.responseHandler = IMAPResponseHandler()
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? group.syncShutdownGracefully()
|
||||
}
|
||||
|
||||
func connect() async throws {
|
||||
let sslContext = try NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration())
|
||||
let handler = responseHandler
|
||||
let hostname = host
|
||||
|
||||
nonisolated(unsafe) let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: hostname)
|
||||
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([
|
||||
channel.pipeline.addHandlers([
|
||||
sslHandler,
|
||||
IMAPClientHandler(),
|
||||
handler,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import NIO
|
||||
import NIOIMAPCore
|
||||
@preconcurrency import NIOIMAP
|
||||
@preconcurrency import NIOSSL
|
||||
import NIOIMAP
|
||||
import NIOSSL
|
||||
import Models
|
||||
|
||||
public actor IMAPIdleClient {
|
||||
@@ -29,10 +29,9 @@ public actor IMAPIdleClient {
|
||||
try await connectAndLogin()
|
||||
try await selectInbox()
|
||||
|
||||
monitorTask = Task { [weak self] in
|
||||
monitorTask = Task {
|
||||
var backoffSeconds: UInt64 = 5
|
||||
while !Task.isCancelled {
|
||||
guard let self else { break }
|
||||
do {
|
||||
try await self.idleLoop(onNewMail: onNewMail)
|
||||
} catch {
|
||||
@@ -79,11 +78,11 @@ public actor IMAPIdleClient {
|
||||
|
||||
let responseHandler = IMAPResponseHandler()
|
||||
|
||||
nonisolated(unsafe) let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: hostname)
|
||||
let bootstrap = ClientBootstrap(group: eventLoopGroup)
|
||||
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
||||
.channelInitializer { channel in
|
||||
let sslHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: hostname)
|
||||
return channel.pipeline.addHandlers([
|
||||
channel.pipeline.addHandlers([
|
||||
sslHandler,
|
||||
IMAPClientHandler(),
|
||||
responseHandler,
|
||||
|
||||
Reference in New Issue
Block a user