# Hedwig sends SMTP mail from Swift with a queued, non-blocking API

> Hedwig is a Swift package that talks to SMTP servers over plain, SSL or STARTTLS, supports PLAIN, CRAM-MD5, LOGIN and XOAUTH2 authentication, and sends mail asynchronously through a queue. The newest tagged release is 1.1.0 from 2017, and the last push was on 2026-04-03.

**onevcat/Hedwig** — Send email to any SMTP server like a boss, in Swift and cross-platform

- Repository: https://github.com/onevcat/Hedwig
- Stars: 1,121 · Forks: 68
- Language: Swift
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/onevcat-hedwig

## One package, one job, and a non-blocking send

Hedwig is a Swift package supplying a set of high level APIs to send email to an SMTP server easily, and the README frames it for someone building a Swift server app.

The feature list is the specification. It connects to all SMTP servers through plain, SSL or TLS, that is STARTTLS, ports. It authenticates with PLAIN, CRAM-MD5, LOGIN or XOAUTH2. It sends mail with an HTML body and attachments. It lets you customise the validation method and mail headers, which the README describes as being for tracking your mail campaign. It supports queued mail sending without blocking your app, and you can even send mails concurrently.

That last point is the design decision worth understanding. The send call takes a completion closure:

```swift
let hedwig = Hedwig(hostName: "smtp.example.com", user: "foo@bar.com", password: "password")
let mail = Mail(
        text: "Across the great wall we can reach every corner in the world.", 
        from: "onev@onevcat.com", 
        to: "foo@bar.com", 
        subject: "Hello World"
)

hedwig.send(mail) { error in
    if error != nil { /* Error happened */ }
}
```

The mail is queued and the call returns, and the closure is invoked when the result is known. A server that sends a password reset email should not have a request thread blocked on a TCP round trip to a mail provider, and a queue lets several sends proceed at once instead of serialising on one connection.

For multiple mails, the API takes an array and gives you a progress callback per mail plus a completion callback with the sent and failed lists, so you can log the message IDs of the ones that went out.

The package is MIT licensed, cross-platform for macOS and Linux, works with Swift Package Manager, and the README describes it as fully tested and documented, with the documentation at onevcat.github.io/Hedwig.

## The transport options, and what the defaults imply

The configuration surface is where you decide how the connection is made, and the defaults are worth reading carefully.

```swift
let hedwig = Hedwig(
        hostName: "smtp.example.com", 
        user: "foo@bar.com", 
        password: "password",
        port: 1234,     // Determined from secure layer by default
        secure: .plain, // .plain (Port 25) | .ssl (Port 465) | .tls (Port 587) (default)
        validation: .default, // You can set your own certificate/cipher/protocols
        domainName: "onevcat.com", // Used when saying hello to STMP Server
        authMethods: [.plain, .login] // Default: [.plain, .cramMD5, .login, .xOauth2]        
)
```

The three secure layers map to the ports you already know: plain is port 25, ssl is port 465, and tls, meaning STARTTLS, is port 587 and is the default. The port is determined from the secure layer by default, so in the common case you set the layer and the port follows.

The default authMethods list is the full set, plain, cramMD5, login and xOauth2, which means Hedwig will try the server's supported mechanisms in an order it chooses. Narrowing that list, as in the example, matters for two reasons: it avoids advertising a weaker mechanism than you want to use, and it avoids a round trip of rejected AUTH commands on servers that support several.

The validation option is the TLS trust configuration, and the comment says you can set your own certificate, cipher and protocols rather than the default. domainName is what Hedwig says in the EHLO greeting, which matters for providers that require a specific hostname to be presented.

The XOAUTH2 support is the one that matters most for hosted providers, since it is how you authenticate with an OAuth2 access token rather than a password.

## Attachments, inline images and CC parsing

The attachment model is more capable than the name suggests, and the examples in the README show the three ways to build one.

An attachment can come from HTML content, from a local file path, or from raw data. The HTML case is the multipart body of the message:

```swift
let attachment = Attachment(htmlContent: "<html><body><h1>Title</h1><p>Content</p></body></html>")
let mail = Mail(
        text: "Fallback text", 
        from: "onev@onevcat.com", 
        to: "foo@bar.com", 
        subject: "Title", 
        attachments: [attachment]
)
```

The text is the plain text alternative for clients that will not render HTML, which is still the right thing to send.

The file path case supports inline images with a Content-ID header, which is how you reference an embedded image from the HTML body:

```swift
let imageAttachment = Attachment(
        filePath: imagePath, 
        inline: true, 
        additionalHeaders: ["Content-ID": "hedwig-image"] 
)
let html = Attachment(
        htmlContent: "<html><body>A photo <img src=\"cid:hedwig-image\"/></body></html>", 
        related: [imageAttachment]
)
```

The related array is what makes the inline image a related part of the HTML part rather than a separate attachment, and the recommendation in the README is to mark an image that is only embedded this way as related.

The raw data case takes a mime type and a file name, so you can send a generated file such as JSON without writing it to disk first.

CC and BCC are handled as strings, and the README points out that addresses will be parsed for you and that the group syntax works too, so cc can be a comma separated list with display names such as Wei Wang <onev@onevcat.com>, and bcc can use a group name with a colon. Handling that parsing in the package rather than in every call site is the kind of small convenience that matters when you have a mailing list.

## Swift Package Manager, and a manifest that shows its age

The installation instructions use the Swift 3 era PackageDescription syntax:

```swift
import PackageDescription

let package = Package(
    name: "YourAwesomeSoftware",
    dependencies: [
        .Package(url: "https://github.com/onevcat/Hedwig.git", 
                 majorVersion: 1)
    ]
)
```

Then swift build, and the README adds a note to grab a cup of coffee.

The.Package(url:majorVersion:) spelling and the majorVersion label are both Swift 3 and Swift 4 APIs. Modern Swift Package Manager uses.package(url:from:) or.package(url:), and the tools version declaration is required in current manifests. A project on a current toolchain will need to adapt the manifest, and on a project using a dependency manager that pins Swift versions, an old manifest can be a blocker rather than an inconvenience.

The badges in the README tell the same story. The Swift version badge reads Swift 3.0, the platform badge covers macOS and Linux, and the CI is Travis with a Codecov badge and a Codebeat badge.

The repository layout is a Swift package with the standard structure: Package.swift at the root, Sources/ and Tests/ directories, a docs/ directory, a CHANGELOG.md, a fastlane/ directory for release automation, and a Package.pins file, which is the dependency resolution file from the Swift 3 and 4 era, since the current lock file is Package.resolved. There is also a.ruby-version and a Gemfile, which are there for the fastlane tooling rather than for the library itself.

The licence is MIT, with a LICENSE file at the root, and the CocoaPods badge in the README shows it was also distributed as a pod.

## Where Hedwig is the wrong choice

The first limit is the release history, and it is not a small one. The newest tagged release is 1.1.0 from 2017, with 1.0.1 and 1.0.0 both from January 2017. The last push was on 2026-04-03, so work does happen, but there is no tagged release in nine years, and the repository is not archived.

That combination is ambiguous and worth being careful about. A project can be quietly maintained without cutting releases, and the changelog and the commit history are where you would look. But for a dependency you are adding to a server, a release tag is the thing you pin against, and there is nothing recent to pin to. Anyone evaluating this should read the CHANGELOG.md and the recent commits rather than assuming the tip of master is a supported version.

The second limit is the Swift version. The manifest syntax in the README predates the current PackageDescription API by a large margin. Even if the source compiles, the manifest has to be rewritten to be usable in a current project, and if the source itself uses APIs removed in later Swift versions, that is a much bigger job than a manifest fix. The README gives no Swift version requirement beyond the 3.0 badge.

The third is scope. Hedwig sends mail over SMTP and does the protocol. It does not do deliverability: there is no bounce handling, no DKIM signing, no SPF or DMARC configuration, no suppression list, and no reputation management. Those are the things that decide whether your transactional mail reaches an inbox, and they live in the mail provider, not in the client. For a project that has never sent much mail, that is fine. For a project where email is the product, a client library is the wrong layer to buy your deliverability.

The fourth is the security of the credentials in the example code. The README shows a password passed into the initialiser, and the same pattern appears in every snippet, which is the normal shape for an SMTP client but means the password lives in a configuration file, an environment variable or, as the Forward Email example shows, a generated dashboard password. That is standard practice and not a criticism of Hedwig, but it is a thing to plan for rather than discover.

## Conclusion

Hedwig suits a Swift server that needs to send transactional mail and wants one small package rather than a mail platform, and it is the wrong tool for a project that needs deliverability tooling, bounce handling at scale or a maintained dependency. Before adopting it, check the Swift toolchain it targets, because the installation instructions in the README use the Swift 3 era PackageDescription syntax and the newest tagged release is 1.1.0 from 2017, so a current Swift project will hit friction in the manifest. Verify first that your SMTP provider supports one of the four authentication methods Hedwig offers, and read the CHANGELOG.md before assuming the tip of master is what you expect.

## FAQ

### What is Hedwig?

It is a Swift package that supplies high level APIs for sending email to an SMTP server. It connects over plain, SSL or STARTTLS ports, authenticates with PLAIN, CRAM-MD5, LOGIN or XOAUTH2, supports HTML bodies and attachments, and sends mail through a queue without blocking the app.

### How do I add Hedwig to a Swift project?

Add the repository URL to your Package.swift as a dependency with majorVersion 1, then run swift build. The README's manifest uses the Swift 3 era .Package(url:majorVersion:) syntax, so a current project will need to adapt it to the current PackageDescription API.

### How do I configure SMTP security and authentication in Hedwig?

The Hedwig initialiser takes hostName, user and password, plus port, secure which is .plain, .ssl or .tls, validation for your own certificate, cipher and protocol settings, domainName used in the SMTP greeting, and authMethods. The port defaults from the secure layer, with .plain on 25, .ssl on 465 and .tls on 587.

### Does Hedwig support inline images and attachments?

Yes. An Attachment can be built from htmlContent, from a filePath with inline and additionalHeaders such as Content-ID, or from raw data with a mime type and name. The related parameter marks a part as related to the HTML part, which the README recommends for images that are only embedded.

### How current is Hedwig?

The newest tagged release is 1.1.0 from 2017, with 1.0.1 and 1.0.0 from January 2017, and the repository is not archived. The last push was on 2026-04-03, so there is activity but no recent release, and the project is MIT licensed.

## Sources

- [Issues](https://github.com/onevcat/Hedwig/issues)
- [License: MIT](https://github.com/onevcat/Hedwig/blob/master/LICENSE)
- [onevcat/Hedwig on GitHub](https://github.com/onevcat/Hedwig)
- [README](https://github.com/onevcat/Hedwig/blob/master/README.md)
- [Releases](https://github.com/onevcat/Hedwig/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/onevcat-hedwig
