Conversation
Introduces the lint gate infrastructure only; existing violations will be fixed in follow-up commits so CI surfaces them per change. - .swiftlint.yml: indented switch cases, identifier-name exemptions for RFC 5322/2971/3501 protocol names (to, cc, os, on, or) - .editorconfig: 4-space indent, LF endings, trim trailing whitespace - .github/workflows/swiftlint.yml: macOS runner, brew install swiftlint, swiftlint lint --strict - scripts/hooks/pre-commit: auto-fix + strict lint on staged Swift files, refuses to run when staged file also has unstaged edits - scripts/install-hooks.sh: one-time per-clone hook installer Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical pass: swiftlint lint --fix corrected ~1465 violations across 137 files. All auto-fixable rules — no behavior changes. Rules fixed by --fix: trailing_whitespace, syntactic_sugar, vertical_whitespace, trailing_comma, trailing_newline, implicit_optional_initialization, unneeded_synthesized_initializer. Remaining: 883 violations from rules that need structural changes (switch_case_alignment, line_length, identifier_name, function_body_length, cyclomatic_complexity, file_length, type_body_length, force_try, force_cast, etc.) — addressed in follow-up commits, file by file. swift build clean, 284 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical line wraps to bring each file under the 120-char limit. Each file had exactly one line_length violation; no logic changes. Files: - Core/Commands/BaseMailCommandHandler.swift - Extensions/Email+CustomDebugStringConvertible.swift - Extensions/EmailAddress+StringConversion.swift - IMAP/IMAP/Commands/SearchCommand.swift - IMAP/IMAP/Handler/LoginHandler.swift - IMAP/IMAPIdleConfiguration.swift - IMAP/Models/Message+CustomStringConvertible.swift - IMAP/UndefinedFolderError.swift - SMTP/Handlers/BaseSMTPHandler.swift - Tests/SwiftIMAPTests/EMLTests.swift - Tests/SwiftIMAPTests/ICSCalendarDetectionTests.swift - Tests/SwiftIMAPTests/IMAPConnectionTLSModeTests.swift - Tests/SwiftIMAPTests/SendDraftTests.swift Violations: 883 → 870 (-13). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each file had exactly one violation: - Data+Utilities.swift: replace `guard let _ = String(data:encoding:)` with `guard String(data:encoding:) != nil` (unused_optional_binding) - CommandHandler.swift: add swiftlint:disable for the Void-cast on succeedWithResult — the cast is already guarded by an explicit `ResultType.self == Void.self` check on the line above (force_cast) - QuotaHandler.swift: inline the `q` temporary into `self.quota = ...` (identifier_name 'q') - IMAPCommandQueue.swift: rename `op` parameter to `operation` (identifier_name 'op') - LoginAuthHandler.swift: reformat init parameters one-per-line (vertical_parameter_alignment) - SMTPLogger.swift: add swiftlint:disable for the InboundIn -> String cast — MailLogger declares InboundIn=Any but SMTPLogger only ever receives String responses (force_cast) Violations: 870 → 864 (-6). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reformats case/default labels inside switch statements one level
deeper to match the `switch_case_alignment: indented_cases: true`
config — Xcode's default "Indent switch statement case labels in"
setting. Applied via:
swiftformat --rules indent --indent-case true --swift-version 6.0 \
Sources Tests Demos
110 files reformatted. Pure indentation change — diff is 3182/3182,
balanced exactly because lines moved but no content changed. No
behavior changes. swift test 284/284.
Violations: 864 → 345 (-519). All switch_case_alignment violations
cleared; ~9 lines now exceed 120 chars due to deeper indent — addressed
in next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applied via:
swiftformat --rules wrapArguments \
--max-width 120 \
--wrap-arguments before-first \
--wrap-parameters before-first \
--swift-version 6.0 \
Sources Tests Demos
43 files reformatted. Mechanical line wraps — no behavior changes.
swift test 284/284.
Violations: 345 → 262 (-83). Cleared 84 line_length violations;
one additional function_body_length surfaced (a function grew by the
wrap; will address with the other structural rules).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual line wraps for files with exactly one remaining line_length violation. Mix of comment splits, class-conformance wraps, ternary splits, and argument extractions. Files: Email+StringInitializers, String+Charset, SearchHandler, XOAUTH2AuthenticationHandler, BodyStructure+CustomStringConvertible, Message, MessagePart+BodyStructure, MessagePart, SMTPError, ExtendedSearchHandlerTests, IMAPNamedConnectionTests, MessageBodyTests, MessagePartBodyStructureTests, ProblematicMessageTests, String+MIMETests. Violations: 262 → 248 (-14). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- MIME/EMLParser.swift: wrap String(data:encoding:) fallback and filename suffix extraction - IMAPTestServer.swift: split CAPABILITY and SELECT response strings across multiple concatenations - XOAUTH2AuthenticationHandlerTests.swift: reformat withTimeout and setUpChannel signatures one-parameter-per-line Violations: 248 → 242 (-6). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IMAPNamedConnection.swift: reformat three function signatures one-parameter-per-line (fetchMessageInfosBulk, executeCommand, expungeMoveFallback) - ExtendedSearchHandler.swift: wrap class conformance list and convertNIOSet signature - FetchMessageInfoHandlerTests.swift: split two fetchResponse helpers with multi-line signatures and concatenated response strings - QuotedPrintableTests.swift: split long MIME-encoded test data across string concatenations - String+HostnameTests.swift: split the IPv6 regex pattern and two hostname patterns across alternation boundaries - Demos/SwiftIMAPCLI/main.swift: split three error/info messages Violations: 242 → 225 (-17). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IMAPConnection.swift (5 fixes): log messages bound to String let before passing to logger (Logger.Message doesn't accept + concatenation directly); also wrap executeCommandBody signature - SMTPServer.swift (4 fixes): same Logger.Message pattern for two log statements; wrap sendRawMessage and executeCommand signatures - Email+Demo.swift (4 fixes): wrap textBody string; surround the CSS-heavy multi-line HTML template with // swiftlint:disable/enable line_length (CSS rules don't wrap cleanly and this is a demo) Violations: 225 → 212 (-13). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three patterns: - Doc comments: split long /// parameter and throws descriptions across multiple lines (lines 128, 289, 308, 539, 1135-1136, 2236, 2260, 2408) - Function signatures: reformat generic functions and append/fetch variants one-parameter-per-line (idle, fetchMessageInfosBulk, fetchMessageInfos, fetchMessages, expungeMoveFallback, recursivelyFetchParts, executeCommand, append × 2) - Log messages: bind to String let then interpolate (Logger.Message doesn't accept + concatenation directly); used for IDLE reliability task start, reconnect failure logs, DONE checkpoint, error retry - Gmail special-use detection: extract matchesGmailFolder helper to collapse 6 nearly-identical caseInsensitiveCompare chains Violations: 212 → 185 (-27). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IMAPLogger.swift: surround compile-time regex literals with swiftlint:disable/enable force_try (NSRegularExpression with a string-literal pattern cannot fail at runtime) - SMTPServer.swift (3 sites): replace try! channel.pipeline.syncOperations calls with plain try and route failures through makeFailedFuture; the SSL branch already has a do/catch, and the plaintext branch gets one added so add-handler failures surface as a failed future rather than crashing - String+HostnameTests.swift (4 sites): swiftlint:disable for the four NSRegularExpression initializers — all use constant string-literal patterns defined immediately above Violations: 185 → 176 (-9). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace .data(using: .utf8)! with Data(_.utf8) across test files. Data(stringLiteral.utf8) is non-optional and always succeeds, while the .data(using:)! pattern returns Data? and crashes on nil — for literal strings the conversion can't fail, so the explicit cast just adds noise. Files: DataUtilitiesTests, MessageBodyTests, MessagePartBodyStructureTests, PipelinedFetchTests, SMTPTests. Violations: 176 → 160 (-16). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add swiftlint:disable:next with one-line reasons for each String(decoding:as:) call where lossy UTF-8 decoding is intentional: - IMAPServer.swift: raw message data may contain non-UTF-8 sequences (8-bit, binary content), and we'd rather preserve the message with replacement characters than fail the Sent-folder append - Mailbox.swift: mailbox names may use modified-UTF-7 (RFC 3501) which is not valid UTF-8; replacement characters > failing - String+QuotedPrintable.swift: best-effort fallback when bytes don't match the declared charset - SendContentCommand.swift: messages may carry 8-bit or non-UTF-8 bytes that still need to wire to the server Violations: 160 → 156 (-4). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both signatures are dictated externally or read clearer with explicit
parameters; add swiftlint:disable:next with one-line reasons:
- Demos/Swift{IMAP,SMTP}CLI/OSLogHandler.swift: 7-parameter log()
signature is the LogHandler protocol contract from swift-log
- Tests/FetchMessageInfoHandlerTests.swift: 6-int makeDate helper
reads more clearly at test call sites than a wrapper struct
Violations: 156 → 153 (-3). swift test 284/284.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical renames to satisfy identifier_name (3-40 chars): - fd -> fileDescriptor (4 sites: handleClient param, sendLine param, stop loop var, listen-cancel handler) - n -> bytesRead / bytesWritten (read/write return values) - ct -> contentType / mediaType / rawContentType (depending on context) - s -> value (quote helper param) No behavior changes; swift test 284/284. Violations: 153 → 144 (-9). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical renames in two test functions: pendingCount and failIndividualHandlers. p1/p2 -> promise1/promise2, h1/h2 -> handler1/handler2. No behavior changes; swift test 284/284. Violations: 144 → 136 (-8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Test helpers used a/b for equality and hash comparisons; expand to lhs/rhs which match the typical Swift convention for comparison operands and satisfy identifier_name. No behavior changes. Violations: 136 → 130 (-6). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c/c1/c2 used in `.and` and `.or` switch cases - rename to child / left / right which match the structural role of each branch. No behavior changes; swift test 284/284. Violations: 130 → 125 (-5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical identifier_name renames to satisfy the 3-40 char rule: Single-letter loop / temp vars: - IMAPConnection.swift, Section.swift, Email.swift, Data+Decoding.swift: i -> index - Identification.swift: k/v -> key/value - Flag.swift: s -> raw Two-char abbreviations expanded: - IMAPServer.swift: shorten the long testing accessor name primaryConnectionCertificateVerificationPolicyForTesting (55 chars exceeded 40) -> primaryConnectionVerifyPolicyForTesting - ExtendedSearchHandler, FetchMessageInfoHandler, NamespaceHandler: c, dc, ns -> count, dateComponents, namespace - Message.swift, EMLSerializer.swift, EMLParser.swift: ct -> contentType / lowercasedContentType - Data+Decoding.swift: hi/lo -> highNibble/lowNibble - Int+Utilities.swift: kb/mb/gb -> kilobytes/megabytes/gigabytes - SendContentCommand.swift: cr/lf -> carriageReturn/lineFeed - MessageID.swift, FetchMessageInfoHandler.swift: s -> trimmed / input - EMLParser.swift: n1 -> firstHexIndex - String+HostnameTests.swift: ip -> address - main.swift (demo): ts -> timestamp, f -> flagList IMAPTransportSecurityTests.swift updated to call the renamed accessor. No behavior changes; swift test 284/284. Violations: 125 → 93 (-32). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mixed bag of tail-end fixes:
- Email+Demo.swift: trim trailing whitespace inside the HTML template
- ExtendedSearchHandler.swift: split a 124-char doc comment
- Mailbox.swift: swiftlint:disable nesting for Mailbox.Info.Attributes —
the path is public API and flattening would break consumers
- IMAPConnection.swift / XOAUTH2AuthenticationHandlerTests.swift:
swiftlint:disable large_tuple for two intentional 3-tuples (internal
pipelined-request bookkeeping; test helper return type)
- IMAPServer.swift / Section.swift / ProblematicMessageTests.swift:
convert `for ... { if pred { ... } }` to `for ... where pred`
- MessageIdentifierSet.swift / SMTPServer.swift: swiftlint:disable
force_cast at three sites where the cast is guarded by an explicit
type check on the preceding line (UID vs SequenceNumber generic
bridging; LoginAuthCommand result-type bridging)
Violations: 93 → 83 (-10). swift test 284/284.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move idle(), idle(on:configuration:), idle(on:cycleInterval:), and done() to a new IMAPServer+Idle.swift extension file. Widen access on storage and helpers that the extension now references: private -> internal: host, port, primaryConnection, idleConnections, authentication, IdleConnection struct, Authentication enum, makeIdleConnection, endIdleSession private extension -> internal extension: resolveMailboxPath helpers IMAPServer.swift: 2480 -> 2099 lines, actor body 1014 -> 729 lines. No behavior changes; swift test 284/284. Lint count unchanged because the big idle(on:configuration:) function (267 lines, cyclomatic 31) still trips function_body_length / cyclomatic_complexity in its new home — to be addressed by extracting IMAPResilientIdleRunner in a follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 267-line idle(on:configuration:) body (cyclomatic complexity 31) is replaced with delegation to a non-isolated IMAPResilientIdleRunner enum that holds the cycle/recovery logic. Split across three files mirroring the original PR design: - IMAPServer+IdleRunner.swift: state types (IdleCycleState, IdleCycleTrigger, IdleCycleResult, IdleCycleContext) and the top-level run() / makeCycleLogger / reconnectDelay helpers - IMAPServer+IdleRunner+Cycle.swift: runOneCycle, raceIdleStreamAgainstTimer, applyCycleResult, handleStreamEnded, handleTimer - IMAPServer+IdleRunner+Recovery.swift: PostIdleEvents, collectPostIdleEvents, advanceTimersAfterCheckpoint, attemptRoutineReconnect, handleCycleError Also adds the IdleSessionRequest bundle and noop()'s definition moves into IMAPServer+Idle.swift (removed from the main file to avoid duplicate declaration). IMAPServer.swift: 2099 -> 2093 lines, actor body 729 -> 726 lines. IMAPServer+Idle.swift idle(on:configuration:) drops to ~40 lines. swift test 284/284. Violations: 83 -> 81 (-2). The orchestration is now splittable into per-phase static helpers, so subsequent function-body-length / cyclomatic work in the IDLE area is contained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move methods from the IMAPServer actor into per-feature extension files, mirroring the original PR 161 design. Each extension is isolated to the actor since they live in the same module. New files: - IMAPServer+Append.swift: append(rawMessage:), helpers - IMAPServer+Connection.swift: connect, disconnect, login, authenticate*, id, connection(named:), close-all - IMAPServer+Fetch.swift: fetchStructure, fetchPart, fetchPartsPipelined, fetchRawMessage, fetchAllMessageParts, fetchMessage, fetchMessageInfo*, fetchAndDecodeMessagePartData, recursivelyFetchParts - IMAPServer+FolderOperations.swift: createMailbox, deleteMailbox, renameMailbox, closeMailbox, unselectMailbox - IMAPServer+Folders.swift: listMailboxes, ensureMailboxesLoaded - IMAPServer+Helpers.swift: resolveMailboxPath, normalizedMailboxName, canonicalizeCRLF, makeInternalDate - IMAPServer+Mailbox.swift: mailboxStatus, expunge variants - IMAPServer+Manipulation.swift: move, copy, store, executeMove - IMAPServer+Namespace.swift: fetchNamespaces - IMAPServer+Quota.swift: getQuota, getQuotaRoot - IMAPServer+Search.swift: search, extendedSearch - IMAPServer+Send.swift: append(email:), createDraft, sendDraft - IMAPServer+SpecialUse.swift: listSpecialUseMailboxes, moveToTrash, archive, markAsJunk, saveAsDraft IMAPServer.swift: 2093 -> 217 lines (just the actor declaration, state, init, and updateMailboxes/updateSpecialMailboxes helpers). Actor body 726 -> ~120 lines. Renamed testing accessor: primaryConnectionVerifyPolicyForTesting -> certificatePolicyForTesting (the original PR's name). Updated IMAPTransportSecurityTests.swift accordingly. Violations: 81 -> 76 (-5). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move methods from the IMAPConnection class into per-feature extension files, mirroring the original PR 161 design. New files: - IMAPConnection+Authentication.swift: login, authenticate*, IDLE-required authenticate paths - IMAPConnection+CommandExecution.swift: executeCommand, executeCommandBody, recycleConnectionIfBufferedTermination - IMAPConnection+Connection.swift: connectBody, disconnectBody, channel-setup helpers, doneBody - IMAPConnection+Events.swift: drainBufferedEvents and other event helpers - IMAPConnection+Idle.swift: idle, doneBody, IDLE handler glue - IMAPConnection+PipelinedFetch.swift: pipelined FETCH dispatch - IMAPConnection+TLS.swift: TLS upgrade logic IMAPConnection.swift: 1362 -> 230 lines, class body 1074 -> ~85 lines (just the typealiases, state, init, and three top-level entry points that wrap commandQueue.run). Violations: 76 -> 66 (-10). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move methods from the SMTPServer actor into per-feature extension files mirroring the original PR 161 design. New files: - SMTPServer+Authentication.swift: authenticate(username:password:), loginAuth, plainAuth, auth helpers - SMTPServer+Capabilities.swift: capability parsing, supports8BitMIME, maximumMessageSizeOctets, EHLO handling - SMTPServer+Connection.swift: connect, disconnect, STARTTLS upgrade, resolveLegacyTransportSecurity, makeCommandHandler - SMTPServer+Send.swift: sendEmail, sendRawMessage, prepareEmailForSend SMTPServer.swift: 1021 -> 370 lines, actor body 501 -> ~80 lines. Violations: 66 -> 63 (-3). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the 1700+ MIME-type dictionaries out of String+MIME.swift into 6 partial-dictionary files (alphabetical buckets A/B/C × the two directions), mirroring the original PR 161 design. New files: - String+MIME+ExtensionToMimeTypeA.swift / ...B.swift / ...C.swift - String+MIME+MimeTypeToExtensionA.swift / ...B.swift / ...C.swift Each dictionary table is the second half of a static computed property that joins three buckets — keeps each file well under the 1000-line file_length limit. String+MIME.swift: 1834 -> 62 lines (just the public extension methods that look up the dictionaries). Violations: 63 -> 62 (-1, file_length on this file cleared). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the original PR 161 splits for three more medium-sized files. EMLParser.swift: 608 -> 75 lines, struct body 385 -> ~30 lines. Split across 5 feature files: - EMLParser+AddressAndDate.swift: address-list parsing, date parsing - EMLParser+Headers.swift: header parsing, MessageInfo construction - EMLParser+Multipart.swift: multipart body splitting - EMLParser+Parameters.swift: content-type / disposition parameter extraction - EMLParser+RFC2047.swift: =?CHARSET?B?...?= encoded-word decoding IMAPNamedConnection.swift: 448 -> 94 lines, actor body 334 -> ~50 lines. Split across 5 feature files: - IMAPNamedConnection+Fetch.swift - IMAPNamedConnection+Idle.swift - IMAPNamedConnection+Mailbox.swift - IMAPNamedConnection+Manipulation.swift - IMAPNamedConnection+Search.swift String+QuotedPrintable.swift: 477 -> ~50 lines. Split across 3 files: - String+QuotedPrintable+Decoding.swift - String+QuotedPrintable+Encoding.swift - String+QuotedPrintable+MIMEHeader.swift FetchMessageInfoHandler.swift was not split — the 105fc09 version predates the named-time-zone normalization added by 717b981/d8c5bb8. That file still has type_body_length / file_length / cyclomatic_complexity violations; will address with a tailored split that preserves the named-zone parsing in a follow-up commit. Violations: 62 -> 53 (-9). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the 105fc09 splits for the test files that exceeded file_length / type_body_length: - IMAPTestServer.swift: 540 -> 122 lines. Split into +CommandHandling, +MaildirLoading, +ResponseBuilding extensions - ExtendedSearchHandlerTests.swift: 404 -> 159 lines, body 293 -> ~150. Split into +Fallback, +Helpers, +WireFormat extensions (separate helper struct ExtendedSearchHandlerTestHelpers) - XOAUTH2AuthenticationHandlerTests.swift: 332 -> 132 lines, body 274 -> ~120. Split into +Failure, +Helpers extensions (separate fixture struct XOAUTH2TestFixtures) - EmailMessageConversionTests.swift: 407 -> ~140 lines. Split into +Fixtures, +MessageFromEmail extensions (separate fixture struct EmailMessageConversionFixtures) - SMTPTests.swift: 601 -> 59 lines. Split into +DotStuffing, +ErrorHandling, +MessageContent, +MessageID, +SendValidation, +ServerCapabilities extensions Violations: 53 -> 39 (-14). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move ParsableCommand subcommands and environment helpers out of the 837-line main.swift into per-command files under Commands/ and a shared Environment.swift, mirroring the 105fc09 split: - Commands/DownloadAttachment.swift - Commands/Fetch.swift - Commands/Folders.swift - Commands/Idle.swift - Commands/List.swift - Commands/Move.swift - Commands/Search.swift - Commands/Search+Criteria.swift - Environment.swift main.swift: 837 -> 56 lines (logger, async helpers, root IMAPTool command, and entry point only). Violations: 39 -> 31 (-8). swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…foHandler split Add scoped `// swiftlint:disable ... // swiftlint:enable` blocks around the 19 function/initializer bodies whose complexity or length is inherent to their purpose (response-payload switches, recursive body-structure parsers, multi-format date parsers, MIME assembly, Codable Decoders, LogHandler protocol witnesses). Each disable block carries a one-line justification in the function's doc comment so the rationale is discoverable without grep. Also split FetchMessageInfoHandler.swift one more time: - FetchMessageInfoHandler.swift: 431 -> 297 lines, class body 293 -> ~250. The static parsing helpers (parseEnvelopeDate, the named-zone table, parseMessageIDs, shouldCollectThreadingHeaders) move to - FetchMessageInfoHandler+Parsing.swift (141 lines, new file) Final couple of fixups: - Demos/SwiftIMAPCLI/Environment.swift: wrap a 123-char throw line - Demos/SwiftSMTPCLI/Email+Demo.swift: trim trailing whitespace inside the HTML template - Mailbox.swift: remove a duplicate doc comment that was orphaning 🟢 Violations: 31 -> 0 in 277 files. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splitting IMAPTestServer.swift into +CommandHandling / +MaildirLoading / +ResponseBuilding extensions introduced a build-macos hang that exceeded the 10-minute job timeout on three consecutive runs. iOS and Linux builds completed normally on the same commits, and the SwiftLint job finished green in 14s — so this is specifically a macOS test-runner interaction with the split. This matches the macOS hang the original PR 161 bisect commits were investigating; reverting the split is the lower-risk path. Restore the single-file IMAPTestServer.swift and gate the structural SwiftLint rules off for this one file via a top-of-file swiftlint:disable / bottom-of-file swiftlint:enable pair, with a comment naming the macOS hang as the reason. Violations: still 0 across 274 files. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After reverting just IMAPTestServer.swift, build-macos still hit the 10-minute timeout while build-ios (3m41s) and build-linux (5m29s) passed. The hang isn't unique to IMAPTestServer's POSIX-socket code — it's somehow tied to having Swift Testing tests split across multiple files in this repo's macOS test runner. Revert the remaining 4 test-file splits: - ExtendedSearchHandlerTests.swift (and remove +Fallback, +Helpers, +WireFormat) - XOAUTH2AuthenticationHandlerTests.swift (and remove +Failure, +Helpers) - EmailMessageConversionTests.swift (and remove +Fixtures, +MessageFromEmail) - SMTPTests.swift (and remove +DotStuffing, +ErrorHandling, +MessageContent, +MessageID, +SendValidation, +ServerCapabilities) Gate the structural rules (file_length, type_body_length, large_tuple on a test helper) off at the top of each restored file with a swiftlint:disable / swiftlint:enable pair and a one-line note. Violations: still 0 across 261 files. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
force_cast (3 sites): - CommandHandler.swift: replace `() as! ResultType` (guarded by an outer `ResultType.self == Void.self`) with `() as Any as? ResultType`, which performs the same runtime type check without the bang - SMTPLogger.swift: replace `unwrapInboundIn(data) as! String` with a guarded `as? String` that drops non-String inbound (MailLogger's declared InboundIn is Any but the SMTP pipeline only emits Strings) - SMTPServer.swift: add a `makeHandler(commandTag:promise:)` factory method to the SMTPCommand protocol with a default implementation that forwards to the handler's required init; LoginAuthCommand overrides it to inject username/password. The downstream makeCommandHandler in SMTPServer no longer needs an `as! EventLoopPromise<AuthResult>` cyclomatic_complexity (5 sites): - Attachment.swift: replace the file-extension -> MIME-type if/else chain with a `[String: String]` lookup table - FetchPartHandler.swift: hoist the `!didFinishPart` guard above the FetchResponse switch so each case stays single-purpose - AuthHandler.swift / AuthHandlerStateMachine.swift: split the nested switch over auth method × LOGIN-state into a one-shot helper (PLAIN/XOAUTH2) and a state-machine helper (LOGIN), with the two challenge-then-credential transitions consolidated through advanceLogin - EMLSerializer.swift: extract writeHeaders/writeBody helpers plus appendHeaderIfPresent/appendListHeader so serialize() is a three-step orchestrator Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
function_body_length (2 sites): - Email+Message.swift: extract collectAttachments(from:) and nonStandardHeaders(from:) helpers; the init becomes a 20-line orchestrator that delegates the attachment/header pulls - MessageInfo.swift: collapse the 70-line Codable Decoder.init into a single self.init(...) call by extracting decodeMessageID and decodeReferences for the three legacy-vs-current decode forks force_try (2 sites): - IMAPLogger.swift: replace `try!` on compile-time-constant regex patterns with a static makeRegex(_:) helper that surfaces a preconditionFailure on the impossible programmer error - String+HostnameTests.swift: same makeRegex(_:) pattern for the 4 IPv4/IPv6/hostname regex compilations function_parameter_count (1 site): - FetchMessageInfoHandlerTests.swift: collapse the 6-int makeDate helper into a single DateComponents parameter; callers wrap the named ints in `DateComponents(year:month:day:hour:minute:second:)` Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cyclomatic_complexity / function_body_length (3 sites): - NoopHandler.swift: split handleUntagged(ResponsePayload) into per-arm helpers (handleMailboxData, handleMessageData, handleConditionalState), and collapse the 7 "ignore unsolicited X" arms in MailboxData into a default-case log so each helper stays under the complexity threshold - IdleHandler.swift: same split pattern; bye-detection moves to handleConditionalState which returns Bool to signal session termination - SelectHandler.swift: collapse the nested conditionalState/ok/code-switch + mailboxData-switch into two per-payload helpers (applyResponseCode, applyMailboxData); the outer override is now a 5-arm switch line_length (4 sites): - IMAPServer+IdleRunner.swift: bind the cycle-start info message to a String let then interpolate; same pattern as the IMAPServer log-message fixes earlier in this PR - IMAPServer+IdleRunner+Recovery.swift: same pattern for the three reconnect-failure log lines Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cyclomatic_complexity (2 sites): - Mailbox.Attributes(from:): extract per-attribute mapping into a `attribute(for:)` helper and replace the special-use if/else chain with a small `[(token, Attributes)]` lookup table - FetchMessageInfoHandler.updateHeader: split the MessageAttribute switch into `applyEnvelope(_:to:)` and a static `date(from:)` helper for ServerMessageDate -> Date cyclomatic_complexity + function_body_length (1 site): - Email.constructContent: 155-line inline MIME assembler split into a short writeHeaders/writeBody dispatcher plus four per-structure helpers (writeMultipartMixed, writeHTMLWithInlineAttachments, writeAlternativeTextAndHTML, encodedAttachmentBody). Encoding / text-body selection and the three boundary tokens move into a new private MIMEBuildContext struct that the helpers thread through. Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cyclomatic_complexity + function_body_length (2 sites): - ExtendedSearchHandler.processResponse: split the 60-line, nested- triple-pattern function into a small dispatcher plus three helpers: failOnNegativeStatus (BAD/NO -> failWithError), ingest(MailboxData) for ESEARCH / SEARCH / SORT fanout, and ingest(SearchReturnData) for the per-ESEARCH-attribute updates (MIN/MAX/ALL/COUNT/PARTIAL) - SearchCriteria.toNIO: the 83-line / cyclomatic-39 enum-case switch was already grouped by intent in the original code; lift that intent into the type. The public method now dispatches in order through flagSearchKey, textSearchKey, dateSearchKey, sizeSearchKey, and compositeSearchKey (AND/OR/NOT/UID/MODSEQ). flagSearchKey further fans out into positive/negative/keyword helpers to keep each under the per-function complexity bar. Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
function_body_length (2 sites): - String+Charset.stringEncoding(for:): a 110-line function whose bulk was the two big mapping tables. Lift the tables to file-private let constants (charsetAliases, knownCharsetEncodings), pull normalization into canonicalCharsetLabel(_:), and isolate the CoreFoundation path in coreFoundationEncoding(forIANAName:). The public entry point is now a 15-line dispatcher. - MessageBodyTests.testFetchMessagesSequentialOrder: the 56-line test body was driven by an inline FakeServer class declaration. Hoist FakeSequentialFetchServer to file scope (a single test references it) so the @test body becomes a 10-line assertion. Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cyclomatic_complexity + function_body_length (1 site, large):
- The public init recursively walked the BodyStructure but folded all
of singlepart extraction, content-type rendering, disposition/filename
resolution, the message/rfc822 envelope decode, and the embedded-
body recursion into one 188-line body. Lift each concern into its
own helper:
- appendSinglepart(_:sectionPath:) / appendMultipart(_:sectionPath:)
mutating helpers for the two outer cases
- appendEmbeddedRFC822 for the nested message/rfc822 body walk
- contentType(for:) renders type/subtype[; charset=...] from
the typed `kind`
- dispositionAndFilename(for:contentType:) resolves the
Content-Type / Content-Disposition / text-calendar default /
Content-ID fallback / MIME-decoded filename chain (with
filenameFromContentTypeParameters extracted)
- embeddedMessageInfo(from:) builds the envelope-derived MessageInfo
- filename(forEmbeddedSubject:) does the subject-sanitising
`subject.eml` fallback
The public init is now a 10-line dispatcher. swift test 284/284.
Violations stay at 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
optional_data_string_conversion (4 -> 1 site): - Introduce `Data.lossyUTF8String` — a single, documented entry point for the lossy `String(decoding: data, as: UTF8.self)` pattern, with the swiftlint:disable scoped to that one property. Use it from the four lossy-decoding sites that previously each carried their own disable comment: String+QuotedPrintable+Decoding (fallback after String(data:encoding:)), IMAPServer+Send (raw message append), Mailbox.init(nio:) (mailbox-name modified-UTF-7), and SendContentCommand.toCommandString (DATA payload). large_tuple (1 site): - XOAUTH2AuthenticationHandlerTests.setUpChannel: replace the 3-tuple return with a `ChannelSetup` struct (channel/promise/handler). Each test now reads `let setup = try await setUpChannel(...)` and pulls `setup.channel` / `setup.promise` explicitly. Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
force_cast (2 -> 0 sites): - MessageIdentifierSet.toNIOSet<NIOType>: the generic-NIOType bridge used to build a same-shape `MessageIdentifierRange<NIOType>` from a concrete `<UID>` or `<SequenceNumber>` via `as!`. Route it through the existing concrete-typed `toNIOSet()` overloads on `MessageIdentifierSet<UID>` and `MessageIdentifierSet<SequenceNumber>` using `as?` checks, then `as?` the typed NIO result back to the caller's generic. Each bridge is now a runtime-checked conditional cast (which doesn't trigger force_cast); the unreachable third branch surfaces as preconditionFailure. function_body_length (1 site): - Email.demo (SwiftSMTPCLI): the 89-line demo helper folded download, validation, HTML template, and attachment construction inline. Lift downloadSwiftLogo() (network + 200-OK check) and htmlTemplate(...) (the CSS/HTML, retaining a scoped line_length disable since the body is intentionally one long CSS rule per line) into separate helpers. The public demo() shrinks to ~30 lines. Violations stay at 0. swift test 284/284. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc8ee789b6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| writeHTMLWithInlineAttachments(context: context, into: &content, terminating: false) | ||
| } else { | ||
| // Just text, no HTML | ||
| content += "--\(mainBoundary)\r\n" | ||
| content += "Content-Type: text/plain; charset=UTF-8\r\n" | ||
| content += "Content-Transfer-Encoding: \(textEncoding)\r\n\r\n" | ||
| content += "\(textBody)\r\n\r\n" | ||
| } | ||
|
|
||
| // Add regular attachments | ||
| for attachment in self.regularAttachments { | ||
| content += "--\(mainBoundary)\r\n" | ||
| content += "Content-Type: \(attachment.mimeType)\r\n" | ||
| content += "Content-Transfer-Encoding: base64\r\n" | ||
| content += "Content-Disposition: attachment; filename=\"\(attachment.filename)\"\r\n\r\n" | ||
|
|
||
| // Encode attachment data as base64 | ||
| let base64Data = attachment.data.base64EncodedString(options: [.lineLength76Characters, .endLineWithCarriageReturn]) | ||
| content += "\(base64Data)\r\n\r\n" | ||
| } | ||
|
|
||
| // End main boundary | ||
| content += "--\(mainBoundary)--\r\n" | ||
| } else if hasHtmlBody && hasInlineAttachments { | ||
| // HTML with inline attachments but no regular attachments - use multipart/related | ||
| content += "Content-Type: multipart/related; boundary=\"\(relatedBoundary)\"\r\n\r\n" | ||
| content += "This is a multi-part message in MIME format.\r\n\r\n" | ||
|
|
||
| // First add the multipart/alternative part | ||
| content += "--\(relatedBoundary)\r\n" | ||
| content += "Content-Type: multipart/alternative; boundary=\"\(altBoundary)\"\r\n\r\n" | ||
|
|
||
| // Add text part | ||
| content += "--\(altBoundary)\r\n" | ||
| content += "Content-Type: text/plain; charset=UTF-8\r\n" | ||
| content += "Content-Transfer-Encoding: \(textEncoding)\r\n\r\n" | ||
| content += "\(textBody)\r\n\r\n" | ||
|
|
||
| // Add HTML part | ||
| content += "--\(altBoundary)\r\n" | ||
| content += "Content-Type: text/html; charset=UTF-8\r\n" | ||
| content += "Content-Transfer-Encoding: \(textEncoding)\r\n\r\n" | ||
| content += "\(htmlBody ?? "")\r\n\r\n" | ||
|
|
||
| // End alternative boundary | ||
| content += "--\(altBoundary)--\r\n\r\n" | ||
|
|
||
| // Add inline attachments | ||
| for attachment in self.inlineAttachments { | ||
| content += "--\(relatedBoundary)\r\n" | ||
| content += "Content-Type: \(attachment.mimeType)" | ||
| content += "; name=\"\(attachment.filename)\"\r\n" | ||
| content += "Content-Transfer-Encoding: base64\r\n" | ||
|
|
||
| if let contentID = attachment.contentID { | ||
| content += "Content-ID: <\(contentID)>\r\n" | ||
| } | ||
|
|
||
| content += "Content-Disposition: inline; filename=\"\(attachment.filename)\"\r\n\r\n" | ||
|
|
||
| // Encode attachment data as base64 | ||
| let base64Data = attachment.data.base64EncodedString(options: [.lineLength76Characters, .endLineWithCarriageReturn]) | ||
| content += "\(base64Data)\r\n\r\n" | ||
| content += "Content-Type: multipart/alternative; boundary=\"\(context.altBoundary)\"\r\n\r\n" | ||
| writeAlternativeTextAndHTML(context: context, into: &content, terminating: false) |
There was a problem hiding this comment.
Close nested multipart boundaries before attachments
When an email has both HTML and regular attachments, this now calls the nested multipart/related/multipart/alternative writers with terminating: false, but writeMultipartMixed never emits the corresponding closing --related-- or --alt-- boundary before starting the next --mainBoundary attachment part. That produces malformed MIME for common HTML-with-attachment messages (and for HTML+inline+regular attachments), so clients may parse regular attachments as part of the nested multipart or ignore them; the previous implementation closed the nested boundary before adding regular attachments.
Useful? React with 👍 / 👎.
Summary
Adopt SwiftLint as the lint gate, then clean up violations incrementally — file by file — so CI verifies each step.
This PR replaces #161 (which had a tangled bisect/merge history). The new branch starts from current
mainand adds:.swiftlint.yml: indented switch cases + RFC-named identifier exemptions.editorconfig: 4-space indent, LF, trim trailing whitespace.github/workflows/swiftlint.yml: macOS runner,swiftlint lint --strictscripts/hooks/pre-commit: auto-fix + strict lint on staged filesscripts/install-hooks.sh: one-time hook installerCI will be red at this commit, on purpose — it surfaces the baseline (~2348 violations across 167 files).
Plan for follow-up commits
swiftlint --fixpass — mechanical: trailing whitespace, syntactic sugar, vertical whitespace, trailing commas, trailing newlines, implicit optional init. Drops baseline by ~1450.switch_case_alignment(527),line_length(167),identifier_name(62),function_body_length(31),cyclomatic_complexity(26), and the long tail.Each follow-up commit pushes so CI can verify the violation count drops.
Test plan
swift build,swift testremain passing throughout🤖 Generated with Claude Code