Sending email from Swift Vapor
Like most services these days, my Vapor side project needs to send welcome emails when a user signs up for the service. While I could write my own SMTP client, it would still require a lot of DNS setup to make sure those emails made it through various spam countermeasures. Given this is a side project, and sending email is a minor component of it, that didn’t seem worthwhile to me. So I decided to use a 3rd party service who would take care of all of that.
I chose Mailgun for a couple of reasons:
- I’m hosting my side project on Heroku, and they provide a convenient Mailgun add on that I can provision.
- Mailgun offers a free tier, appropriate for my side project
- There’s already a Swift package for Mailgun’s API.
The Swift package for Mailgun got me most of the way there. However, I still needed a bit of infrastructure to render the emails, and then to validate the emails were correctly sent in my integration tests.
In this post I’m going to cover how I went about generating the email content, sending the email to Mailgun, and finally how I ensured I could test it all. I’m not going to cover how to set up Mailgun and all the DNS stuff because that’s very app specific, and is documented in Mailgun’s docs already.
Example use
I find it helpful when building a subsystem to sketch out what the use site will look like. It helps me determine if I’m building something that will meet my needs, and gives me a concrete target to build toward. With that in mind, I thought the ideal for my project was to define an email template for each kind of email I wanted to send, have a method that could render that down to a final email to send, then send it via an abstract email delivery service. The email body allows both text and html, and the body template was defined in Leaf.
Here’s how that looked in code:
struct WelcomeData: Encodable {
let verifyLink: String
init(user: User) {
self.verifyLink = "myapp://verify/\(user.verifyToken)"
}
}
...
let template = EmailTemplate(from: "donotreply@myapp.com",
to: email,
subject: "Welcome to MyApp!",
bodyTemplate: "WelcomeEmail")
let welcomeData = WelcomeData(user: user)
return template.render(welcomeData, on: container).flatMap { message -> Future<Void> in
let deliveryService = try container.make(EmailDeliveryService.self)
return deliveryService.send(message, on: container)
}
The first step is to create the EmailTemplate
for the email I want to send and render it. The from
, to
, and subject
properties are String
s that will be passed through unchanged to the final email. The bodyTemplate
is the base name for the Leaf templates that will be rendered in the render()
method. WelcomeData
is the Leaf context for the templates; it defines anything that the the Leaf template will need. I like to think of it as a view model. It takes a model object and transforms it into values that the view (i.e. the Leaf template) needs. In this example, the WelcomeEmail template needs an email verification link, so WelcomeData
constructs that based on a token assumed to be on the User
model. To render the EmailTemplate
into something that can be sent, render()
is called, passing in WelcomeData
.
The second step is to send the resulting EmailMessage
. The dependency injection framework is asked to produce a EmailDeliveryService
object, which can send an EmailMessage
. EmailDeliveryService
is a protocol, meaning it can be swapped out later, without the client code knowing or caring. This enables testing fakes to be injected during tests, as well as making it possible to move to a new email service should I ever decide to do that.
That covers the Swift code for creating and sending the email. I still need to define the Leaf templates though. I want to send both plain text and HTML MIME parts in my email, so regardless of the user’s email app they’ll see something reasonable. Since the email body template parts are Leaf templates, I put them in the standard location: Resources/Views
. I also follow a naming convention so EmailTemplate.render()
can find them at runtime.
Here’s the contents of WelcomeEmail.html.leaf
:
<html>
<head></head>
<body>
<p>Hello!</p>
<p>Welcome to MyApp!</p>
<p><a href="#(verifyLink)">Verify your email address</a></p>
<p>The MyApp Team.</p>
</body>
</html>
And the contents of WelcomeEmail.text.leaf
:
Hello!
Welcome to MyApp!
Verify your email address by clicking on this link: #(verifyLink)
The MyApp Team.
Both templates represent the same email, but the content changes based on the format they’re being rendered into. The #(verifyLink)
placeholder is filled in with value in WelcomeData.verifyLink
.
Now that I’ve defined my target API, I can start building the implementation.
Rendering email with Leaf
First I need to define what an email message is, because it is the type everything else is dependent on. The EmailTemplate
needs to render to it, and EmailDeliveryService
needs to send it. I decided to define my own types for this because it reduces coupling on a specific service, plus it more accurately represents what my app thinks an email is. Also, the necessary types are pretty simple, so I did’t think they’d increase my maintenance burden any.
Here’s my definition:
struct EmailBody {
let text: String
let html: String
}
struct EmailMessage {
let from: String
let to: String
let subject: String
let body: EmailBody
}
My app’s idea of an email is simple. It has a from
, to
, subject
, and body
fields. The only thing that might look out of the ordinary is the the body
has two parts: HTML and plain text. My app doesn’t care about attachments or other features, so they don’t show up here.
With email defined, I could create the EmailTemplate
which takes care of rendering Leaf templates down to a EmailMessage
. I started by defining the properties of the EmailTemplate
:
import Vapor
import Leaf
import TemplateKit
struct EmailTemplate {
private let from: String
private let to: String
private let subject: String
private let bodyTemplate: String
init(from: String, to: String, subject: String, bodyTemplate: String) {
self.from = from
self.to = to
self.subject = subject
self.bodyTemplate = bodyTemplate
}
...
}
The template is the same as the EmailMessage
with the exception of bodyTemplate
, which is the base name of the Leaf templates for the email body. Most of the work of EmailTemplate
is to convert the bodyTemplate
into a EmailBody
. The top level render method looks like:
struct EmailTemplate {
...
private static let htmlExtension = ".html"
private static let textExtension = ".text"
func render<E>(_ context: E, on container: Container) -> Future<EmailMessage> where E: Encodable {
let htmlRender = render(bodyTemplate + EmailTemplate.htmlExtension, context, on: container)
let textRender = render(bodyTemplate + EmailTemplate.textExtension, context, on: container)
return htmlRender.and(textRender)
.map { EmailBody(text: $1, html: $0) }
.map { EmailMessage(from: self.from, to: self.to, subject: self.subject, body: $0) }
}
...
}
The render()
method takes the Leaf template context and a dependency injection container, and returns the promise of an EmailMessage
. The implementation relies on a helper render()
method that renders one Leaf template at a time. This top level render()
calls it twice: once for the plain text template, and once for the html template. It uses the and
operator to let the template renders run concurrently if possible, then combines the results into an EmailBody
, before a final map
that mixes in the from
, to
, and subject
fields.
The helper render()
method is similarly straight forward:
enum EmailError: Error {
case invalidStringEncoding
case emailProviderNotConfigured
}
...
struct EmailTemplate {
...
private func render<E>(_ name: String, _ context: E, on container: Container) -> Future<String> where E: Encodable {
do {
let leaf = try container.make(LeafRenderer.self)
return leaf.render(name, context).map { view in
guard let str = String(data: view.data, encoding: .utf8) else {
throw EmailError.invalidStringEncoding
}
return str
}
} catch let error {
return container.future(error: error)
}
}
}
The helper method here takes the full Leaf template name, its context, a dependency injection container and returns a promise of String
, which is the fully rendered body. To achieve this, it creates a LeafRenderer
and asks it to render the template. This results in view data, which it decodes into a String
and returns. If any error is thrown, it’ll convert it into a rejected promise for convenience.
The email template rendering is fairly simple, but creating the EmailTemplate
type reduces the boilerplate code needed for sending an email.
Sending email
I now have a fully rendered email message, and I need to send it. As I mentioned up top, I’m used a 3rd party Swift package to actually talk to Mailgun’s API. However, I wanted an easy way to inject testing fakes, and the ability to swap out email services later if necessary. So I’m first going to show how I integrated the Swift Mailgun package, then how I abstracted it with a generic interface that can be faked.
Since it’s a Swift package, I added it to my Package.swift
file:
dependencies: [
...
.package(url: "https://github.com/twof/VaporMailgunService.git", from: "1.1.0"),
...
],
...
targets: [
...
.target(name: "App", dependencies: [
...
"Mailgun",
...
]),
...
]
Running vapor update
from the command line pulled down the package and updated all my dependencies. I decided to use a Provider
to set up the Mailgun package in my app:
import Vapor
import Mailgun
struct MailgunProvider: Provider {
private let config: MailgunConfigurationType
init(mailgunConfig: MailgunConfigurationType) {
self.config = mailgunConfig
}
func register(_ services: inout Services) throws {
services.register(Mailgun(apiKey: config.apiKey, domain: config.domain), as: EmailDeliveryService.self)
}
func didBoot(_ container: Container) throws -> EventLoopFuture<Void> {
return .done(on: container)
}
}
There’s not much to this. The only interesting bit is the register()
implementation. It registers the Mailgun
service from the Mailgun
framework as the implementation for the EmailDeliveryService
protocol. It uses apiKey
and domain
fields from the configuration passed in, which will come from the appropriate environment configuration. In my case, since I’m using Heroku, the production environment configuration will pull from the MAILGUN_API_KEY
and MAILGUN_DOMAIN
environment variables. Additionally, the production configuration will take care of registering this provider.
I decided to use a provider pattern for the sake of the testing configuration. The production provider here doesn’t really need to be a provider; it only registers one service. But since the testing configuration does make full use of the Provider
protocol, I decided to make the production configuration follow suite.
Now that I had Mailgun in my app, I needed to put it behind a generic protocol so all the client code could be agnostic about the email service being used. I started by defining a simple protocol:
import Vapor
protocol EmailDeliveryService: Service {
func send(_ message: EmailMessage, on container: Container) -> Future<Void>
}
An EmailDeliveryService
is a Service
(in the dependency injection sense) that implements a send()
method. The send()
method takes an EmailMessage
and returns a Void
promise, which can be used to know if the send succeeded or failed.
For the final bit of sending an email, I need to conform the Mailgun
service to my generic EmailDeliveryService
protocol:
import Vapor
import Mailgun
extension Mailgun: EmailDeliveryService {
func send(_ message: EmailMessage, on container: Container) -> Future<Void> {
do {
let mailgunMessage = Mailgun.Message(
from: message.from,
to: message.to,
subject: message.subject,
text: message.body.text,
html: message.body.html
)
return try self.send(mailgunMessage, on: container).transform(to: ())
} catch let error {
return container.future(error: error)
}
}
}
The implementation is intentionally as thin as possible. This is partly because it’s hard to test protocol extensions. If I had needed anything more complicated, I would have opted to wrap Mailgun
in another type that conformed to EmailDeliveryService
. In any case, this simply converts my EmailMessage
type into Mailgun’s version and sends it. It also wraps any thrown errors into a rejected promise for the convenience of any calling code.
And, with that, my app is now sending email via Mailgun! But is it sending the correct emails? Well…
Testing
The goal of a couple of design decisions was to make the testing of emails possible, or at least easier. The choice of abstracting out the email service into a generic protocol means I can inject a testing fake. Making the email template rendering separate from the email sending means I can still test the template rendering, even if I swap out the email service with a fake.
For my integration testing, I didn’t actually want to send any emails to Mailgun. That means my tests aren’t full integration tests, and won’t catch a misconfigured Mailgun setup. But I didn’t want my integration tests dependent on an external, non-local service to run; that would make them too flaky. Plus I’d likely run into an Mailgun API quota pretty quick. However, even with this limitation, I was able to verify that the correct emails got sent at the correct time.
Like with building the initial email types, I prefer to start out with what a final test might look like. Here’s a simplified test from my app that validates a welcome email was sent:
func testCreate_emailShouldSend() throws {
app.emailDeliveryService.send_stubbed?.succeed()
...
// Do something that should send an email
...
XCTAssertTrue(app.emailDeliveryService.send_wasCalled)
XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.to, "bob.jimbob@example.com")
XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.from, "donotreply@myapp.com")
XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.subject, "Welcome to MyApp!")
let link = "myapp://verify/\(user!.verifyToken)"
XCTAssertTrue(app.emailDeliveryService.send_wasCalled_withMessage!.body.html.contains(link))
XCTAssertTrue(app.emailDeliveryService.send_wasCalled_withMessage!.body.text.contains(link))
}
The first line tells the email service testing fake that the next call to send()
should return success. Next the test calls into the app in a way that should send a welcome email (as represented by the comment). The final lines assert that send was called, and with the correct parameters. The test also validates that the most important piece of information — the verify link — appears in both the plain text and HTML parts of the email message. I didn’t do a full text comparison because most of the body is static content, and comparing all of it makes the test more fragile than it needs to be.
With this test written, I can work backwards and define what my email service testing fake should implement.
import Vapor
import Mailgun
final class FakeEmailDeliveryService: EmailDeliveryService {
var send_wasCalled = false
var send_wasCalled_withMessage: EmailMessage?
var send_stubbed: Promise<Void>?
func send(_ message: EmailMessage, on container: Container) -> Future<Void> {
send_wasCalled = true
send_wasCalled_withMessage = message
return send_stubbed!.futureResult
}
...
}
The testing fake, FakeEmailDeliveryService
, records if send()
was called along with the EmailMessage
it was called with. It only keeps track of the last message because my tests only send one at a time. The fake also has the ability to return a stubbed value from send()
. This is useful for validating what happens if there’s a failure on the email service’s end. The fake send()
assumes that the stubbed promise has been allocated elsewhere before it’s invoked.
Speaking of allocating the stubbed promise, there’s a bit of cleanup that’s required because of the way Vapor’s promises work:
final class FakeEmailDeliveryService: EmailDeliveryService {
...
deinit {
// Vapor does not like a promise created but not resolved before it is destroyed. It calls them "leaked" and crashes the tests. So make sure nothing is "leaked" in our tests.
send_stubbed?.succeed()
}
}
As the comment states, Vapor will crash a test if there are any promises left unresolved. This happens in any of my tests that don’t exercise the email functionality of the app. I could go through all of those tests and add code to resolve the send_stubbed
promise, but that’d be verbose and tedious. Instead, I opted to have deinit
forcefully resolve the promise if it hasn’t already been.
The FakeEmailDeliveryService
needs to be registered with the dependency injection system so that code asking for a EmailDeliveryService
will get an instance of it. As with the production version of EmailDeliveryService
, I used a Provider
:
struct TestMailProvider: Provider {
var emailDeliveryService = FakeEmailDeliveryService()
func register(_ services: inout Services) throws {
services.register(emailDeliveryService, as: EmailDeliveryService.self)
}
func didBoot(_ container: Container) throws -> EventLoopFuture<Void> {
emailDeliveryService.send_stubbed = container.eventLoop.newPromise()
return .done(on: container)
}
}
The first thing the provider does is create the FakeEmailDeliveryService
and stash in a public member variable. It does this so tests can get ahold of it and validate the send()
parameters, or resolve its return value. The register()
method then registers the fake as the implementation of EmailDeliveryService
. The didBoot()
method takes care of creating the unresolved promise for send()
‘s stubbed return value. Creating promises require an event loop, and didBoot()
is the earliest place in the test code that I had access to one. I chose to allocate the stubbed promise this early because it allowed tests to assume its existence during set up without worrying about race conditions.
With the registering of TestMailProvider
all of testing fakes are set up and ready to be used. However, FakeEmailDeliveryService
wasn’t yet accessible to the test cases, which were expecting it as a property on TestApplication
called emailDeliveryService
. The rest of this section is showing the plumbing of TestMailProvider.emailDeliveryService
property up through to TestApplication
.
I started at the TestingConfiguration
level, which is where the TestMailProvider
is created and registered:
struct TestingConfiguration: ConfigurationType {
...
let mailProvider = TestMailProvider()
func configure(_ services: inout Services) throws {
try services.register(mailProvider)
}
...
}
This exposes TestMailProvider
on the TestConfiguration
, which TestApplication
can then use:
class TestApplication {
...
private let configuration: TestingConfiguration
...
var emailDeliveryService: FakeEmailDeliveryService {
return configuration.mailProvider.emailDeliveryService
}
...
}
And now my tests could validate emails by using the FakeEmailDeliveryService
exposed on TestApplication
.
Conclusion
My Vapor side project needed to send emails on user registration, which could potentially be a complicated setup. Since email isn’t a major feature of my app, it made sense to delegate sending emails out to a third party service like Mailgun. Although there’s a convenient Mailgun Swift package, I still needed to build out infrastructure for rendering emails from Leaf templates and abstracting out the emailing sending so I can test my app’s email handling.