Skip to content

1.11.0

Latest

Choose a tag to compare

@odrobnik odrobnik released this 15 Aug 14:21
a2d4a94

Highlights

SwiftMail 1.11.0 replaces three hardcoded policies with configuration, and fixes two ways selective header fetches lost data. SMTP submission moves off a single 10-second timer onto per-stage RFC 5321 §4.5.3.2 budgets you can override (#209); move gains a fallback policy so callers can require a real MOVE rather than silently emulating it (#208); and EXAMINE joins SELECT for read-only mailbox access (#211). On the fetch side, MessageInfo now populates from BODY[HEADER.FIELDS (…)] when ENVELOPE was not requested (#206), and repeated field instances — a message carrying two List-Unsubscribe headers — survive to the caller instead of the last one winning (#213).

Every new parameter has a default, and both changed move signatures keep a source-compatible overload. The three behavior changes worth reading before upgrading are the new SMTP timeout defaults, MOVE no longer requiring UIDPLUS, and COPYUID evidence now failing closed.

Changes

Configurable, RFC-aligned SMTP submission timeouts (#209)

  • New SMTPSubmissionTimeouts, passed as submissionTimeouts: on SMTPServer.init, budgets each stage separately: mailFromResponse, recipientResponse, dataResponse, contentUpload, contentResponse. Defaults follow RFC 5321 §4.5.3.2 — 5 min for MAIL FROM and each RCPT TO, 2 min for DATA, 3 min per content buffer, 10 min for the final reply.
  • Behavior change: the previous budgets were 30 s / 10 s / 30 s and a single 10-second timer that started before the message finished flushing and stayed armed while waiting for the final 250. A slow or attachment-bearing send could therefore time out as ambiguous after the server had already accepted the message — exactly the outcome a retrying outbox must not act on. Callers who want the old aggressive deadlines can pass their own SMTPSubmissionTimeouts.
  • Message content is now written as sequential bounded 64 KiB buffers, each with a fresh contentUpload budget, so total upload allowance scales with a message that is making progress while one genuinely stalled buffer still fails. The contentResponse timer starts only after the last buffer flushes, so upload time cannot eat the server-processing allowance.
  • Acceptance classification got sharper with it: a content-phase failure before the end-of-data terminator is handed to the transport is now provably notAccepted rather than ambiguous, so fewer failures are marked unsafe to retry. After the terminator, the previous ambiguity rules stand.
  • New SMTPSendError.timeoutStage (commandWrite, commandResponse, contentUpload, contentResponse) says which budget expired, set whenever reason is timedOut and the timeout came from SwiftMail. CustomStringConvertible reports it too.
  • Public operations on one SMTPServer now serialize through an internal gate, so a connect, send, and disconnect racing on the same instance can no longer interleave writes on the channel.
  • Timeout customization and the stage-specific diagnostics are documented in the SMTP getting-started guide.

Configurable MOVE fallback and verified COPYUID evidence (#208)

  • New MoveFallbackPolicy on the existing move APIs of IMAPServer and IMAPNamedConnection: .copyStoreExpunge (the default, unchanged behavior) or .disabled, which requires the server's MOVE extension and throws IMAPError.commandNotSupported before emitting any manipulation command rather than emulating the move. The no-fallback overloads remain, so existing call sites compile and behave identically.
  • Behavior change: UID moves no longer require UIDPLUS to use MOVE. Eligibility was MOVE && (not UID || UIDPLUS); UIDPLUS governs optional mapping evidence and targeted expunge, not whether UID MOVE is legal. Servers advertising MOVE without UIDPLUS now get one atomic MOVE instead of COPY + STORE \Deleted + EXPUNGE.
  • MOVE and UIDPLUS capability tokens are now matched case-insensitively per RFC 3501, so a server spelling them Move or UidPlus is no longer treated as lacking them.
  • COPY and MOVE both surface CopyUID, including from the untagged OK that RFC 6851 recommends servers emit before the EXPUNGE responses.
  • Behavior change: COPYUID evidence fails closed. Duplicate UIDs, a wildcard/4294967295 bound that RFC 4315 forbids in COPYUID, source UIDs that were never requested, and conflicting tagged-vs-untagged mappings now throw the new IMAPError.malformedCopyUIDAfterTaggedOK instead of being returned or collapsed into nil. That error explicitly means the command completed — reconcile or resync, never resend.

Read-only mailbox selection with EXAMINE (#211)

  • New examineMailbox(_:) on IMAPServer and IMAPNamedConnection issues EXAMINE and returns the same Mailbox.Selection as select, with the server enforcing read-only access — no STORE, no EXPUNGE, and no implicit \Seen from fetching while the mailbox stays selected this way.
  • Supersedes #207 by @dariuscorvus, keeping its original commits and adding the review fixes.

MessageInfo populated from selective header fields (#206)

  • FetchMessageInfoOptions lets a caller skip ENVELOPE and request specific headers instead, but the handler treated every standard header as a presumed ENVELOPE duplicate and filtered it out — leaving subject, from, to, cc, bcc, date, messageId, and inReplyTo empty on exactly the fetches that asked for them as a protocol-safe fallback.
  • Those fields are now filled from the BODY[HEADER.FIELDS (…)] literal when they would otherwise be nil or empty. When both representations are present, parsed ENVELOPE values stay authoritative, including explicit empty strings.
  • The header literal is decoded per line with the EML parser's total UTF-8 → Latin-1 policy, so one raw 8-bit legacy field no longer discards the whole block.
  • Thanks to @jdudley.

Repeated header fields preserved (#213)

  • Selective header fetches lost repeated field instances before they reached consumers: EMLParser keys values by lowercased field name, so a second List-Unsubscribe overwrote the first — a message offering both a mailto: and an https: unsubscribe surfaced only one.
  • New HeaderField (Codable, Hashable, Sendable) — one RFC 5322 field instance with a lowercased name and an unfolded, trimmed value, normalized on every construction path including Decodable, so a hand-built field equals and filters alongside a parsed one.
  • New MessageInfo.additionalHeaderFields: [HeaderField]? is the lossless, wire-order companion to additionalFields. The dictionary keeps its last-value-wins behavior, so this is source-compatible. Closes #212.

Release-configuration test build fixed (#210)

  • Two internal read-only response-buffer test probes were wrapped in #if DEBUG while the suite using them was compiled unconditionally, so swift test -c release failed to build before running a single test. The probes are now available in both configurations, keeping the buffer-limit suite active in release builds. No public API or runtime change. Thanks to @tabmail-kmyi.

Validation

  • swift test — 495 tests in 62 suites, 0 failures (macOS, Swift 6.3)
  • CI green on main: macOS, iOS, Linux, Android, SwiftLint (the Windows job stays manual-dispatch only, still blocked on apple/swift-nio-ssl#567)

Full Changelog: 1.10.0...1.11.0