add fetchFullMessage, fetchSection to IMAPClient for MIME attachment retrieval
This commit is contained in:
@@ -93,6 +93,65 @@ public actor IMAPClient: IMAPClientProtocol {
|
||||
return parseFlagResponses(responses)
|
||||
}
|
||||
|
||||
public func fetchFullMessage(uid: Int) async throws -> 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: .complete), nil)],
|
||||
[]
|
||||
))
|
||||
self.runner = runner
|
||||
return parseFullMessageResponse(responses)
|
||||
}
|
||||
|
||||
public func fetchSection(uid: Int, mailbox: String, section: String) async throws -> Data {
|
||||
guard var runner else { throw IMAPError.notConnected }
|
||||
|
||||
// Select the mailbox first
|
||||
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)
|
||||
|
||||
// Parse section path into SectionSpecifier.Part
|
||||
let sectionParts = section.split(separator: ".").compactMap { Int($0) }
|
||||
let part = SectionSpecifier.Part(sectionParts)
|
||||
let spec = SectionSpecifier(part: part)
|
||||
|
||||
let responses = try await runner.run(.uidFetch(
|
||||
.set(set),
|
||||
[.bodySection(peek: true, spec, nil)],
|
||||
[]
|
||||
))
|
||||
self.runner = runner
|
||||
|
||||
var bodyBuffer = ByteBuffer()
|
||||
for response in responses {
|
||||
if case .fetch(let fetchResponse) = response {
|
||||
if case .streamingBytes(let bytes) = fetchResponse {
|
||||
var mutableBytes = bytes
|
||||
bodyBuffer.writeBuffer(&mutableBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The section content may be base64 encoded — decode it
|
||||
let raw = String(buffer: bodyBuffer)
|
||||
let cleaned = raw.filter { !$0.isWhitespace }
|
||||
if let decoded = Data(base64Encoded: cleaned) {
|
||||
return decoded
|
||||
}
|
||||
return Data(bodyBuffer.readableBytesView)
|
||||
}
|
||||
|
||||
public func fetchBody(uid: Int) async throws -> (text: String?, html: String?) {
|
||||
guard var runner else { throw IMAPError.notConnected }
|
||||
let uidValue = UID(rawValue: UInt32(uid))
|
||||
@@ -456,6 +515,22 @@ public actor IMAPClient: IMAPClientProtocol {
|
||||
return results
|
||||
}
|
||||
|
||||
private func parseFullMessageResponse(_ responses: [Response]) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
return String(buffer: bodyBuffer)
|
||||
}
|
||||
|
||||
private func parseBodyResponse(_ responses: [Response]) -> (text: String?, html: String?) {
|
||||
var bodyBuffer = ByteBuffer()
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ public protocol IMAPClientProtocol: Sendable {
|
||||
func fetchFlags(uids: ClosedRange<Int>) async throws -> [UIDFlagsPair]
|
||||
func fetchBody(uid: Int) async throws -> (text: String?, html: String?)
|
||||
|
||||
// v0.5 attachment/MIME operations
|
||||
func fetchFullMessage(uid: Int) async throws -> String
|
||||
func fetchSection(uid: Int, mailbox: String, section: String) async throws -> Data
|
||||
|
||||
// v0.3 write operations
|
||||
func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws
|
||||
func moveMessage(uid: Int, from: String, to: String) async throws
|
||||
|
||||
@@ -285,6 +285,8 @@ private final class FailingIMAPClient: IMAPClientProtocol, @unchecked Sendable {
|
||||
func fetchEnvelopes(uidsGreaterThan uid: Int) async throws -> [FetchedEnvelope] { [] }
|
||||
func fetchFlags(uids: ClosedRange<Int>) async throws -> [UIDFlagsPair] { [] }
|
||||
func fetchBody(uid: Int) async throws -> (text: String?, html: String?) { (nil, nil) }
|
||||
func fetchFullMessage(uid: Int) async throws -> String { "" }
|
||||
func fetchSection(uid: Int, mailbox: String, section: String) async throws -> Data { Data() }
|
||||
func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws {
|
||||
throw FailingIMAPError.alwaysFails
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ final class MockIMAPClient: IMAPClientProtocol, @unchecked Sendable {
|
||||
var expungedMailboxes: [String] = []
|
||||
var appendedMessages: [(mailbox: String, message: Data, flags: [String])] = []
|
||||
var serverCapabilities: Set<String> = ["IMAP4rev1", "MOVE"]
|
||||
var fullMessages: [Int: String] = [:]
|
||||
var sections: [String: Data] = [:] // key: "uid-section"
|
||||
var fetchFullMessageCalls: [Int] = []
|
||||
var fetchSectionCalls: [(uid: Int, mailbox: String, section: String)] = []
|
||||
|
||||
func connect() async throws {
|
||||
connectCalled = true
|
||||
@@ -61,6 +65,18 @@ final class MockIMAPClient: IMAPClientProtocol, @unchecked Sendable {
|
||||
bodies[uid] ?? (nil, nil)
|
||||
}
|
||||
|
||||
// MARK: - v0.5 attachment/MIME operations
|
||||
|
||||
func fetchFullMessage(uid: Int) async throws -> String {
|
||||
fetchFullMessageCalls.append(uid)
|
||||
return fullMessages[uid] ?? ""
|
||||
}
|
||||
|
||||
func fetchSection(uid: Int, mailbox: String, section: String) async throws -> Data {
|
||||
fetchSectionCalls.append((uid: uid, mailbox: mailbox, section: section))
|
||||
return sections["\(uid)-\(section)"] ?? Data()
|
||||
}
|
||||
|
||||
// MARK: - v0.3 write operations
|
||||
|
||||
func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws {
|
||||
|
||||
Reference in New Issue
Block a user