Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Feature/Sources/IMAP/EmailAddress.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import EmailAddress
import MIME
import NIOIMAPCore

extension [EmailAddressListElement] {
Expand All @@ -17,7 +18,10 @@ extension EmailAddressListElement {
extension NIOIMAPCore.EmailAddressGroup: @retroactive CustomStringConvertible {

// MARK: CustomStringConvertible
public var description: String { groupName.readableBytesView.description } // TODO: Decode quoted-printable/base64 (RFC 2047)
public var description: String {
let description: String = groupName.readableBytesView.description
return (try? description.headerDecoded()) ?? description
}
}

extension NIOIMAPCore.EmailAddress: @retroactive CustomStringConvertible {
Expand All @@ -32,7 +36,7 @@ extension NIOIMAPCore.EmailAddress: @retroactive CustomStringConvertible {
return ""
}
let description: String = "\(mailbox)@\(host)"
if let personName: String = personName?.readableBytesView.description, // TODO: Decode quoted-printable/base64 (RFC 2047)
if let personName: String = try? personName?.readableBytesView.description.headerDecoded(),
!personName.isEmpty
{
return "\(personName) <\(description)>"
Expand Down
2 changes: 1 addition & 1 deletion Feature/Sources/IMAP/Envelope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct Envelope: Sendable {
}

init(_ envelope: NIOIMAPCore.Envelope) {
subject = envelope.subject?.readableBytesView.description // TODO: Decode quoted-printable/base64 (RFC 2047)
subject = try? envelope.subject?.readableBytesView.description.headerDecoded()
from = envelope.from.addresses
sender = envelope.sender.addresses
reply = envelope.reply.addresses
Expand Down
53 changes: 24 additions & 29 deletions Feature/Sources/IMAP/FetchAttribute.swift
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetch attributes were scattered all over the floor; tidying into two canned presets:

  • [FetchAttribute].complete fetches the complete message, including body and attachments
  • .header fetches all message headers, excluding body and attachments

Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,32 @@ public typealias FetchAttribute = NIOIMAPCore.FetchAttribute

extension [FetchAttribute] {

// Add supported fetch attributes according to set of server capabilities
static func extended(_ capabilities: Set<Capability>) -> Self {
Array(Set(capabilities.flatMap { extended($0) }))
}

// Add supported fetch attributes according to server capability
static func extended(_ capability: Capability) -> Self {
switch capability {
case .gmailExtensions:
[
.gmailLabels,
.gmailMessageID,
.gmailThreadID
]
case .objectID:
[
.emailID,
.threadID
]
default: []
}
}
/// Fetch complete, raw message data in one blob.
public static let complete: Self =
[
.bodySection(peek: true, .complete, nil)
] + standard

// Standard set of fetch attributes supported by all IMAP4 servers
static var standard: Self {
/// Fetch all message headers and body structure map.
public static let header: Self =
[
.envelope,
.flags,
.uid
]
}
.bodySection(peek: true, .header, nil),
.bodyStructure(extensions: true)
] + standard

/// Fetch message envelope, IDs, flags and Gmail labels.
public static let standard: Self = [
.emailID,
.envelope,
.flags,
.gmailLabels,
.gmailMessageID,
.gmailThreadID,
.internalDate,
.preview(lazy: true),
.threadID,
.uid
]

// Filter unsupported attributes according to server capabilities
func filtered(_ capabilities: Set<Capability>) -> Self {
Expand Down
4 changes: 2 additions & 2 deletions Feature/Sources/IMAP/FetchCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class FetchHandler: IMAPCommandHandler, @unchecked Sendable {
var isStreaming: Bool { streaming != nil }

private var streaming: (kind: StreamingKind, data: Data, byteCount: Int)?
private var components: [Message.Component] = []
private var sequenceNumber: SequenceNumber?
private var components: [Message.Component] = []

// MARK: IMAPCommandHandler
typealias InboundIn = Response
Expand Down Expand Up @@ -79,7 +79,7 @@ class FetchHandler: IMAPCommandHandler, @unchecked Sendable {
}
streaming = nil
case .finish:
messages[sequenceNumber!] = Message(components: components)
messages[sequenceNumber!] = Message(components)
sequenceNumber = nil
components = []
default:
Expand Down
28 changes: 24 additions & 4 deletions Feature/Sources/IMAP/IMAPClient.swift
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new presets for [FetchAttribute] sets, we can streamline the IMAPClient public fetching interface

Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,38 @@ public class IMAPClient {
try await execute(command: VoidCommand(.close))
}

/// Fetch messages by mailbox sequence number.
public func fetch(_ set: SequenceSet = .all, attributes: [FetchAttribute]) async throws -> MessageSet {
/// Fetch a set of messages by mailbox ``SequenceNumber``; fetches all headers and omits message body by default.
public func fetch(_ set: SequenceSet = .all, attributes: [FetchAttribute] = .header) async throws -> MessageSet {
logger?.info("Fetching messages by mailbox sequence number…")
return try await execute(command: FetchCommand(set, attributes: attributes.filtered(capabilities)))
}

/// Fetch messages by UID.
public func fetch(_ set: UIDSet, attributes: [FetchAttribute]) async throws -> MessageSet {
/// Fetch a specific message by ``SequenceNumber``; fetches complete message by default.
public func fetch(_ number: SequenceNumber, attributes: [FetchAttribute] = .complete) async throws -> Message {
logger?.info("Fetching message \(number)…")
let messages: MessageSet = try await fetch(SequenceSet(number), attributes: attributes)
guard let message: Message = messages.first?.value else {
throw IMAPError.commandFailed("Message \(number) not found")
}
return message
}

/// Fetch a set of messages by ``UID``; fetches all headers and omits message body by default.
public func fetch(uid set: UIDSet, attributes: [FetchAttribute] = .header) async throws -> MessageSet {
logger?.info("Fetching messages by UID…")
return try await execute(command: UIDFetchCommand(set, attributes: attributes.filtered(capabilities)))
}

/// Fetch a specific message by ``UID``; fetches complete message by default.
public func fetch(uid: UID, attributes: [FetchAttribute] = .complete) async throws -> Message {
logger?.info("Fetching message UID \(uid)…")
let messages: MessageSet = try await fetch(uid: UIDSet(uid), attributes: attributes)
guard let message: Message = messages.first?.value else {
throw IMAPError.commandFailed("Message UID \(uid) not found")
}
return message
}

public init(
_ server: Server,
logger: Logger? = Logger(subsystem: "net.thunderbird", category: "IMAP")
Expand Down
2 changes: 1 addition & 1 deletion Feature/Sources/IMAP/IMAPCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension IMAPCommandHandler where InboundIn == Response, Result == Void {
}

extension Int64 {
static let timeout: Self = 30 // Practical default
static let timeout: Self = 60 // Practical default
}

// IMAP [CLIENTBUG] is an error code mail servers include in responses to
Expand Down
Loading
Loading