Alexander Steiner
Home
Blog

Email Content DSL in Swift

While building server-side Swift code you probably come along the requirement of sending an email to users. Let it be a confirmation, newsletter signup or a notification message. So we need some kind of email sending possibility. There are a lot of different email services to pick from when speaking about server-side development. (I have good experiences with https://github.com/LiveUI/MailCore but there are other options to pick from). But the advantages and disadvantages of different packages will not be further discussed in this article.

While sending emails it is possible to style them with HTML but also to include a text-based version. This helps email clients to choose what to display to our users.

But how to maintain one single source of message content while supporting HTML and also plain text messages? One solution is building our own simple Swift DSL around the message content, and that's what we are going to do in the next few paragraphs.

Separation of Concerns

First, we do not want to write the same code for a title or a paragraph for both plain text and HTML over and over again in different email messages. So let's make that more reusable and create separated components. Then we can reuse these components in different messages.

Our Heading and Paragraph components look like this. Because we use a struct with the auto-generate constructor we can simply create a new object by calling Heading(title: "Welcome to our service").

Now we have an example of components and we know how we can compose them.

struct Heading {
    var html: String {
        "<h1>" + title + "</h1>"
    }
    let title: String
}

struct Paragraph {
    var html: String {
        "<p>" + content + "</p>"
    }
    let content: String
}

// Usage
let message = [
    Heading(title: "Welcome to our service").html,
    Paragraph(content: "Thank you for signing up for our newsletter!").html
]
.joined(separator: "\n")

Let's create a protocol

In the end, we want an element to not only be able to generate HTML code, but also a plain text version. Currently, we have the html property that gives us one of two required properties. But we can additionally add a property that gives us the plain text message. The following protocol represents these requirements quite well:

Later we can access the html property as well as the plainText property of our content and pass both into the email before sending it successfully.

protocol MailContentElement {
    var html: String { get }
    var plainText: String { get }
}

And this is already the foundation. Now we need our previous create elements to conform to this protocol. So how does the actual implementation of the elements look like?

Example implementations

We can take our previous written Heading and Paragraph components and conform them to the newly introduced protocol.

struct Heading: MailContentElement {
    var html: String {
        "<h1>" + title + "</h1>"
    }

    var plainText: String {
        title
    }
    let title: String
}

struct Paragraph: MailContentElement {
    var html: String {
        "<p>" + content + "</p>"
    }

    var plainText: String {
        content
    }

    let content: String
}

Action

But what about actions? Here we can see an implementation of a link action. We can see that in the plain text representation an additional text is added. So we can also add custom hints etc. to our plain text or HTML representation.

struct Link: MailContentElement {
    var html: String {
        "<a href='\(target)'>\(title)</a><br />Or copy this link manually: \(target)<br />"
    }

    var plainText: String {
        "\(title): \(target)"
    }

    let title: String
    let target: String
}

Combining multiple elements

Most of the time we don't only have one element at a time in an email but a combination of multiple components that we want to be stacked together. Instead of manually joining all elements as we did previously we create a wrapper structure that does this job for us but also conforms to the MailContentElement protocol.

struct MailContent: MailContentElement {
    private let elements: [MailContentElement]

    init(_ elements: [MailContentElement]) {
        self.elements = elements
    }

    var html: String {
        elements
            .map({ $0.html })
            .joined(separator: "<br />")
    }

    var plainText: String {
        elements
            .map({ $0.plainText })
            .joined(separator: "\n")
    }
}

Usage

By combining the different components and appending them in the MailContent list-like structure, we have our content in place. Now we can access both, the HTML as well as the plain text, of such elements.

let emailContent: MailContentElement = MailContent([
    Heading(title: "Welcome to our Service"),
    Paragraph(content: "We welcome you to our service. Great that you signed up."),
    Paragraph(content: "To confirm your email, please open the following link in your browser."),
    Link(title: "Confirm your account", target: confirmationLink),
])
emailContent.html // HTML message
emailContent.plainText // Plain text message

An example for sending this mail content via the above-mentioned package "MailCore" looks like this. We can also add a helper function to encapsulate the functionality in case we want to switch the underlying library in the future.

import MailCore

// Function to encapsulate the email sending functionality
func send(to: String, subject: String, content: MailContentElement) throws -> Future<Void> {
    let mail = Mailer.Message(from: "Alexander Steiner <info@alexsteiner.de>", to: "mail@domain.com", subject: subject, text: content.plainText, html: content.html)
    return try container.make(Mailer.self)
        .send(mail, on: container)
        .map ({ mailResult in
            switch mailResult {
            case .success: return ()
            default: throw Error.sendingFailed
            }
        })
}

// Usage in a server side Swift framework (for example Vapor)
func handleApiRequest(request: Request) throws -> Future<HTTPStatus> {
    try send(to: "mail@domain.com", subject: "🥳 Sign Up", content: emailContent, container: request)
        .transform(to: .noContent)
}

Conclusion

This DSL like structure written in Swift is a good start to make email content more abstract and support accessibility for plain text emails. It gives us the flexibilities to add more elements and stack them together to fulfil our needs.

But as usual, it can still be expanded. For example, we could create element groups with specific style attributes like font color, size or weight. Other attributes could be link action styles or custom images as backgrounds. All of this can be added to this base structure.

Also, there are other options to generate HTML and plain text for emails using Leaf as the article by Andy Finnell describes here. But in his example, two separate template files need to be maintained separately.

So what do you think about creating a DSL for email content? Have you ever considered supporting plain text emails and if so, how have you done that? Let me know your thoughts on Twitter or via email.

Tagged with: