Skip to content

Add Gmail X-GM-EXT-1 attribute fetch (X-GM-MSGID, X-GM-THRID, X-GM-LABELS)#192

Merged
odrobnik merged 3 commits into
Cocoanetics:mainfrom
thesako:feature/gmail-fetch-attributes
Jul 25, 2026
Merged

Add Gmail X-GM-EXT-1 attribute fetch (X-GM-MSGID, X-GM-THRID, X-GM-LABELS)#192
odrobnik merged 3 commits into
Cocoanetics:mainfrom
thesako:feature/gmail-fetch-attributes

Conversation

@thesako

@thesako thesako commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Adds support for Gmail's X-GM-EXT-1 message attributes: X-GM-MSGID, X-GM-THRID and X-GM-LABELS.

Motivation

Gmail exposes a message's real label set and its native thread ID over IMAP via the X-GM-EXT-1 extension. Without these, a client has to approximate Gmail's model — inferring threads from References/In-Reply-To and treating labels as folders — which loses fidelity, since a Gmail message can carry several labels at once and folder-shaped models flatten that.

NIOIMAPCore already models all three attributes natively (FetchAttribute.gmailMessageID/.gmailThreadID/.gmailLabels, and the matching MessageAttribute cases with parser support). This PR only surfaces that existing capability through SwiftMail's public API — it adds no parsing of its own.

What's added

  • FetchGmailAttributesCommand — issues UID FETCH <set> (UID X-GM-MSGID X-GM-THRID X-GM-LABELS)
  • FetchGmailAttributesHandler — accumulates the response, following the existing FetchMessageInfoHandler idiom
  • IMAPServer.fetchGmailAttributes(for:) — public API, in its own extension file:
public func fetchGmailAttributes(
    for identifierSet: UIDSet
) async throws -> [UID: (messageID: UInt64, threadID: UInt64, labels: [String])]

Labels are returned via GmailLabel.makeDisplayString(), so modified-UTF-7 names decode correctly.

Design note

UID is requested explicitly and results are keyed by the returned UID rather than by response position, because the server need not answer in the requested order. There is a test covering exactly this, since getting it wrong produces silent mis-association rather than an obvious failure.

Compatibility

Purely additive — no existing public API is changed. The command requires the X-GM-EXT-1 capability; other servers answer with a tagged BAD, so callers should gate on Capability.gmailExtensions. This is documented on the public method.

Tests

Four tests in Tests/SwiftIMAPTests/FetchGmailAttributesTests.swift:

  • wire format is UID FETCH ... (UID X-GM-MSGID X-GM-THRID X-GM-LABELS)
  • decoding of message ID, thread ID and a mixed system/user label set (\Inbox + Work)
  • results keyed by returned UID when the server answers out of order (round-trip through the public API on an embedded channel)
  • validate() throws on an empty identifier set

Suite goes 352 → 356, all passing, nothing regressed.

Not covered: end-to-end through IMAPTestServer, whose FETCH stub doesn't support X-GM-*; extending it would have widened the diff well beyond this feature, so the embedded-channel harness is used instead — it exercises the same command/response/keying path.

Verification

Built and tested against a real Gmail account: capabilities reported X-GM-EXT-1: true, and live messages returned correct thread IDs, message IDs and label sets. Replies correctly showed msgid != thrid while thread-starting messages showed them equal.

thesako and others added 3 commits July 25, 2026 02:37
Adds wire-format, decoding, UID-keying and validation coverage for
FetchGmailAttributesCommand/Handler and IMAPServer.fetchGmailAttributes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SwiftLint flags large_tuple on the two `(messageID:threadID:labels:)`
tuple occurrences in fetchGmailAttributes(for:), and trailing_comma on
a test array literal. Fix both without suppression:

- Rename the internal parsing accumulator from GmailMessageAttributes
  to GmailAttributeRecord (its optional fields make it unsuitable as
  public API).
- Add a new public GmailMessageAttributes struct (Sendable, Hashable)
  under Models/ with non-optional fields, doc-commented as carrying
  Gmail's X-GM-EXT-1 attributes.
- fetchGmailAttributes(for:) now returns [UID: GmailMessageAttributes].
- Drop the trailing comma from the array literal in
  FetchGmailAttributesTests.swift.

@odrobnik odrobnik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the UID FETCH wire format, returned-UID association, pinned NIOIMAPCore Gmail attribute decoding, public API/model semantics, command execution and failure paths, concurrency behavior, and the added tests. No correctness or contract issues found. Review was read-only; builds and tests were not run as requested.

👍 No findings.

@odrobnik
odrobnik merged commit 7ba2211 into Cocoanetics:main Jul 25, 2026
6 checks passed
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.

2 participants