Skip to content

feat(imap): complete decoding for IMAP headers#280

Open
toddheasley wants to merge 10 commits intothunderbird:mainfrom
toddheasley:feature-imap-message-model
Open

feat(imap): complete decoding for IMAP headers#280
toddheasley wants to merge 10 commits intothunderbird:mainfrom
toddheasley:feature-imap-message-model

Conversation

@toddheasley
Copy link
Collaborator

@toddheasley toddheasley commented Mar 17, 2026

Implement all additional MIME decodings used by IMAP:

  • Add String.headerDecoded; handle any combination of quoted-printable and base64 encodings in raw IMAP headers
  • Decode additional MIME body encodings (text/html) and character sets (UTF-8, Western Latin)
  • Refactor quoted-printable decoding to use String.removingPercentEncoding
  • Benchmark MIME decoding, add tests, fix
  • Model IMAP Message and integrate decoded data

Close #171; close #239

@toddheasley toddheasley changed the title Feature imap message model feat(imap): complete decoding for IMAP headers Mar 17, 2026
Copy link
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

Copy link
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

.threadID,
.uid
])
let messages: MessageSet = try await client.fetch()
Copy link
Collaborator Author

@toddheasley toddheasley Mar 17, 2026

Choose a reason for hiding this comment

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

Switch test to use [FetchAttribute] defaults

return string
}

func decodingQuotedPrintable(to encoding: Encoding = .utf8) throws -> Self {
Copy link
Collaborator Author

@toddheasley toddheasley Mar 17, 2026

Choose a reason for hiding this comment

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

Completely changing quoted-printable decoding. Parser I wrote initially failed benchmarking; it didn't ignore/recover from bad encodings.

New strategy leverages the knowledge that quoted-printable is identical to (URL) percent-escaping, except for the control character. Swap control and escape characters, let Swift do a better job decoding...

@@ -22,9 +22,14 @@ public struct Body: CustomStringConvertible, RawRepresentable, Sendable {
guard !parts.isEmpty else {
throw MIMEError.dataNotFound
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Handle any body encoding, as long as header matches

case .gmailMessageID(let id), .gmailThreadID(let id): "\(self.id): \(id)"
case .internalDate(let date): "\(id): \(date)"
case .uid(let uid): "\(id): \(uid)"
func merging(_ components: [Component]) -> Self {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because IMAP messages are immutable, updates/changes are merged into a copy

@toddheasley toddheasley self-assigned this Mar 18, 2026
@toddheasley toddheasley marked this pull request as ready for review March 18, 2026 13:26
@toddheasley toddheasley requested a review from a team as a code owner March 18, 2026 13:26
@toddheasley toddheasley requested review from jbott-tbird and removed request for a team March 18, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IMAP-SMTP Foundations: Decode MIME-encoded email addresses, subjects IMAP-SMTP Extensions: Handle Incomplete MIME Parts

1 participant