Skip to content

About

SMTP Client for Perfect. Completely updated for Swift 6, modern concurrency and standards using open source tools from the Swift ecosystem.

Topics

Resources

Stars

24 stars

Watchers

4 watching

Forks

Latest commit

 

History

182 Commits

Folders and files

Repository files navigation

Perfect - SMTP 简体中文

Swift 6.2+ Platforms macOS 12+ | Linux License Apache 2.0

Perfect-SMTP is a from-scratch Swift 6 / SwiftNIO SMTP client. It is not a wrapper around libcurl or any other mail library — it drives the SMTP wire protocol itself, including its own STARTTLS state machine, connection pooling, DKIM signing, and MTA-STS policy enforcement.

It ships three delivery strategies (relay through an existing SMTP host, hand off to a local MTA like Postfix/sendmail, or resolve MX records and deliver directly), so you can pick the one that matches how you already operate mail, or let Perfect-SMTP be the terminal MTA itself.

6.0.0 is a complete rewrite of the libcurl-based Perfect-SMTP 5.x. It is not a drop-in upgrade; see Migrating from 5.x. The old code is preserved on the legacy branch and the 5.0.0 tag.

This package is domain-agnostic by design — it has no Lasso-specific code and no Lasso dependency. It is, however, a core dependency of Perfect-Lasso — a Swift reimplementation of the Lasso language, still in active development — which uses it to implement its email_send tag.

Features

  • Hand-rolled SMTP client on SwiftNIO — its own STARTTLS upgrade sequence with byte-precise buffer discipline against injection/downgrade attacks, connection pooling with circuit breaking, and PIPELINING support.
  • DKIM signing (RFC 6376) — RSA-SHA256 and Ed25519-SHA256 (RFC 8463), including dual-signing, with automatic oversigning of security-sensitive headers and a DMARC-alignment lint.
  • Three delivery strategies — RelayTransport (an ESP or existing SMTP relay), LocalMTATransport (hand off to sendmail/Postfix on the same host), and DirectMXTransport (resolve MX records and deliver directly, with its own retry queue and circuit breaker).
  • MTA-STS (RFC 8461) policy discovery, caching, and enforcement for direct-MX delivery, plus opportunistic STARTTLS by default.
  • SASL authentication — PLAIN, LOGIN, and XOAUTH2 (required by Gmail/Workspace, increasingly required by Microsoft 365).
  • Deliverability headers — List-Unsubscribe/List-Unsubscribe-Post (RFC 8058), Precedence, Auto-Submitted — the headers Gmail and Yahoo have required for bulk senders since February 2024.
  • Bulk/list-server ready — a bounded-concurrency batch send and an AsyncSequence-based streaming send for sending to millions of recipients without materializing them all in memory.
  • Structured delivery results — every send returns a per-recipient outcome (delivered, queued for retry, permanently failed, expired, ambiguous, or a transport-level failure) instead of a single pass/fail.

For anything beyond the basics below, see the full user guide.

Requirements

  • Swift 6.2 or later (swift-tools-version: 6.2, Swift 6 language mode)
  • macOS 12 or later, or Linux (CI covers Ubuntu 24.04 with Swift 6.2 and 6.4)

iOS, tvOS and watchOS are not supported: this is a server-side package (SwiftNIO sockets, and LocalMTATransport launches a local sendmail).

Dependencies are all Apple/SSWG packages: swift-nio, swift-nio-ssl, swift-log and swift-crypto (the last for DKIM; any 4.x release, so it resolves alongside other packages using swift-crypto 4).

Installation

Add the package to your Package.swift:

.package(url: "https://github.com/PerfectlySoft/Perfect-SMTP.git", from: "6.0.0")

It has two products:

Product Contents
PerfectSMTP Everything: the transports (RelayTransport, LocalMTATransport, DirectMXTransport), SMTPMailer, the connection pool, the DNS resolver and MTA-STS. Re-exports PerfectSMTPCore.
PerfectSMTPCore Message model, MIME composer, header/body encoders and the DKIM signer, with no networking. Depend on it directly only if you compose or sign messages without sending them.
.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "PerfectSMTP", package: "Perfect-SMTP"),
    ]
)

Quick start

Send one email through an existing SMTP relay (a corporate MTA or an ESP like SendGrid/Postmark/SES):

import NIOPosix
import PerfectSMTP

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)

let transport = RelayTransport(
    config: RelayConfig(
        host: "smtp.example.com",
        port: 587,
        tls: .startTLS,
        auth: .plain(username: "postmaster@example.com", password: "secret")
    ),
    group: group
)
let mailer = SMTPMailer(transport: transport)

var message = EmailMessage(from: EmailAddress(displayName: "Ops", address: "ops@example.com"))
message.to = [EmailAddress(address: "user@dest.com")]
message.subject = "Hello from Perfect-SMTP"
message.textBody = "Hi there!"

let results = try await mailer.send(message, envelopeFrom: .address("ops@example.com"))
for result in results {
    print(result.recipient, result.outcome)
}

await transport.shutdown()
try await group.shutdownGracefully()

MultiThreadedEventLoopGroup comes from SwiftNIO's NIOPosix, which PerfectSMTP already depends on; if your build requires every imported module to be a declared dependency, add .package(url: "https://github.com/apple/swift-nio.git", from: "2.86.0") and .product(name: "NIOPosix", package: "swift-nio").

That's it for the basic case. For DKIM signing, direct-MX delivery, authentication options, bulk sending, and deliverability headers, see the user guide.

Security defaults

You get these without configuring anything:

  • TLS certificates are verified. Every TLS connection (.startTLS, .implicit, and DirectMXTransport's STARTTLS attempts) uses TLSConfiguration.makeClientConfiguration(): full chain verification plus a name check. A relay reached by name, a direct-MX host under an MTA-STS policy, and the MTA-STS policy host must present a DNS subjectAltName for that name: an IP-address SAN alone, which NIOSSL would accept for the dialed address, isn't enough. An IP-literal host is checked against its IP SAN, and opportunistic direct MX without a policy uses NIOSSL's check as is. Only a real IP literal is dialed without SNI; any other name that isn't a valid host name fails before connecting. A failed certificate is reported as SMTPError.tlsCertificateInvalid.
  • .startTLS never downgrades. If the server doesn't advertise STARTTLS, or answers it with an error such as 454, the connection fails instead of continuing in plaintext. Bytes injected around the upgrade (the CVE-2026-41319 class) fail it with .starttlsInjection.
  • No AUTH in cleartext. A RelayConfig with credentials and tls: .none throws RelayTransportError.authenticationRequiresTLS before dialing. Set allowsAuthenticationWithoutTLS: true only for a relay you trust, such as a loopback MTA or a test server.
  • Strict syntax wherever text reaches the wire.
    • Addresses (MAIL FROM, RCPT TO, From/To/Cc/Reply-To/Sender) must be plain local@domain mailboxes: a dot-atom local part, and an LDH domain or an [address literal]. No quoted local parts, <, >, whitespace or control characters, so nothing can smuggle an ESMTP parameter or a second mailbox. Non-ASCII addresses are rejected (SMTPUTF8 isn't implemented); pass IDN domains in A-label form.
    • The EHLO name, TLS server name, DKIM d=/s= and messageIDDomain must be LDH host names.
    • Extra-header names must be RFC 5322 field names, and the routing-critical names (Bcc, To, From...) are refused.
    • Header values, message IDs, content types and content IDs are refused if they contain CR, LF, other C0 or C1 controls, or U+2028/U+2029. Subjects and display names have those stripped and are RFC 2047-encoded.
    • Headers are folded to 78 octets (refused if they can't be kept under 998). The DATA payload has its line endings normalized to CRLF and is dot-stuffed. Bcc addresses only ever go into RCPT TO.
  • Every network step has a timeout: TCP connect (30 s), each SMTP reply including the greeting, EHLO, STARTTLS and the TLS handshake (300 s), the final reply after DATA (600 s), each DNS query (5 s), the whole MTA-STS policy fetch (60 s, and at most 64 KiB of body, enforced while reading), and the sendmail process (60 s). All are configurable.
  • No secrets in logs. The library logs only the DKIM alignment warning and MTA-STS decisions (domains, hosts, errors), never message bodies or credentials. RelayConfig.Auth, SASLPlain, SASLLogin, XOAuth2 and SigningKey print redacted descriptions, and their mirrors (dump, the debugger) hide the secret too.
  • Direct MX delivery:
    • It only dials globally reachable unicast addresses. Every IANA special-purpose IPv4 and IPv6 range is refused (loopback, private, 0.0.0.0/8, link-local, CGNAT, documentation, benchmarking, multicast, reserved), including IPv4 addresses embedded in mapped, NAT64 and 6to4 IPv6 addresses, unless allowPrivateAddresses is set.
    • DNS answers must match the question, and only records for the asked name count. MX exchanges must be LDH host names, and a null MX anywhere in the set means "no mail".
    • STARTTLS is opportunistic with no MTA-STS policy: a host that doesn't offer it, answers 454, or fails the handshake or certificate is retried in plaintext. It is never retried in plaintext after detected injection or with its circuit breaker open, nor under .fixed(.startTLS) or an MTA-STS enforce policy.
    • Under an MTA-STS policy (testing or enforce), the certificate must also carry a DNS subjectAltName for the MX host.
    • Unreachable hosts, timeouts, TLS failures and DNS timeouts or SERVFAIL are temporary and queued for retry (RFC 5321 §4.5.4.1), as is an MTA-STS enforce violation. NXDOMAIN, null MX and 5yz replies are final. Retries back off exponentially (5 minutes doubling to a 4-hour cap, with jitter) for up to 5 days. The retry queue is in memory only: pending retries are lost if the process exits.

Migrating from 5.x

5.x wrapped libcurl through Perfect-CURL: you built an SMTPClient with a URL and credentials, filled in an EMail, and called a blocking send(completion:). 6.0.0 shares no types with it, so a port is a rewrite of the call site, not a find-and-replace:

5.x 6.0.0
SMTPClient(url: "smtp://host", username:, password:) RelayTransport(config: RelayConfig(host:port:tls:auth:), group:)
smtps:// URL tls: .implicit (port 465); tls: .startTLS for port 587
Recipient(name:address:) EmailAddress(displayName:address:)
EMail(client:) EmailMessage(from:), sent with SMTPMailer(transport:).send(_:envelopeFrom:)
email.content / .html / .text message.htmlBody / message.textBody (set both for multipart/alternative)
email.bcc the bcc: parameter of send (never written to a header)
email.attachments (file paths) message.attachments (Attachment(filename:contentType:data:))
try email.send { code, header, body in } let results = try await mailer.send(...), one DeliveryResult per recipient

What changes in behavior: sending is async; the envelope sender is explicit (envelopeFrom: .address(...) or .null); STARTTLS is mandatory when requested; credentials over a plaintext connection are refused (see above); and the 5.x bugs that wrote a Bcc: header to every recipient and emitted a mislabeled quoted-printable subject are gone. There is no libcurl or OpenSSL system dependency any more, so the old libssl-dev/libcurl install notes no longer apply.

The user guide explains why this is a rewrite rather than an update.

Testing

swift test

The whole suite is hermetic: no environment variables, no internet, no DNS and no external mail server. Tests that need sockets (a real STARTTLS handshake, a full DirectMX delivery, the DNS resolver, the MTA-STS fetcher) run against in-process fake servers on 127.0.0.1. CI runs it on Linux and macOS in debug and release with -warnings-as-errors, plus a Linux job in a container started with --network none.

There is no live-integration tier. To check your own setup against a real SMTP server, point a RelayTransport at a local MailHog or smtp4dev instance; see Testing your integration.

License

Apache License 2.0 — see LICENSE.

About

SMTP Client for Perfect. Completely updated for Swift 6, modern concurrency and standards using open source tools from the Swift ecosystem.

Topics

Resources

Stars

24 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages