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 assubmissionTimeouts:onSMTPServer.init, budgets each stage separately:mailFromResponse,recipientResponse,dataResponse,contentUpload,contentResponse. Defaults follow RFC 5321 §4.5.3.2 — 5 min forMAIL FROMand eachRCPT TO, 2 min forDATA, 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 ownSMTPSubmissionTimeouts. - Message content is now written as sequential bounded 64 KiB buffers, each with a fresh
contentUploadbudget, so total upload allowance scales with a message that is making progress while one genuinely stalled buffer still fails. ThecontentResponsetimer 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
notAcceptedrather thanambiguous, 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 wheneverreasonistimedOutand the timeout came from SwiftMail.CustomStringConvertiblereports it too. - Public operations on one
SMTPServernow 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
MoveFallbackPolicyon the existingmoveAPIs ofIMAPServerandIMAPNamedConnection:.copyStoreExpunge(the default, unchanged behavior) or.disabled, which requires the server's MOVE extension and throwsIMAPError.commandNotSupportedbefore emitting any manipulation command rather than emulating the move. The no-fallbackoverloads 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 whetherUID MOVEis 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
MoveorUidPlusis no longer treated as lacking them. - COPY and MOVE both surface
CopyUID, including from the untaggedOKthat RFC 6851 recommends servers emit before the EXPUNGE responses. - Behavior change: COPYUID evidence fails closed. Duplicate UIDs, a wildcard/
4294967295bound that RFC 4315 forbids in COPYUID, source UIDs that were never requested, and conflicting tagged-vs-untagged mappings now throw the newIMAPError.malformedCopyUIDAfterTaggedOKinstead of being returned or collapsed intonil. That error explicitly means the command completed — reconcile or resync, never resend.
Read-only mailbox selection with EXAMINE (#211)
- New
examineMailbox(_:)onIMAPServerandIMAPNamedConnectionissuesEXAMINEand returns the sameMailbox.Selectionasselect, with the server enforcing read-only access — noSTORE, noEXPUNGE, and no implicit\Seenfrom 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)
FetchMessageInfoOptionslets a caller skipENVELOPEand request specific headers instead, but the handler treated every standard header as a presumed ENVELOPE duplicate and filtered it out — leavingsubject,from,to,cc,bcc,date,messageId, andinReplyToempty 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:
EMLParserkeys values by lowercased field name, so a secondList-Unsubscribeoverwrote the first — a message offering both amailto:and anhttps:unsubscribe surfaced only one. - New
HeaderField(Codable, Hashable, Sendable) — one RFC 5322 field instance with a lowercasednameand an unfolded, trimmedvalue, normalized on every construction path includingDecodable, so a hand-built field equals and filters alongside a parsed one. - New
MessageInfo.additionalHeaderFields: [HeaderField]?is the lossless, wire-order companion toadditionalFields. 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 DEBUGwhile the suite using them was compiled unconditionally, soswift test -c releasefailed 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