DEV Community

Cover image for Send Welcome Emails using Appwrite's Swift Cloud Functions
Christy Jacob for Appwrite

Posted on

Send Welcome Emails using Appwrite's Swift Cloud Functions

Cover Image

For all the Swift developers out there, we have good news! We just announced our Swift SDK to work with server side APIs of Appwrite. You can learn more about our Swift SDK in our Swift SDK announcement post.

In this tutorial however, we are going to write, deploy and run our very own Welcome Email Cloud Function with Swift. Let's get started.

🤔 What are Appwrite Cloud Functions?

Appwrite Cloud Functions are a way for you to extend and customize your Appwrite BaaS functionality by allowing you to execute custom code. Appwrite can execute your function either explicitly or in response to any Appwrite system event like account creation, user login, database updates and much more. You can also schedule your functions to run according to a CRON schedule or trigger them manually by hitting an HTTP endpoint using the Appwrite client or server APIs.

🗒️ Prerequisites

In order to continue with this tutorial, you'll need to have the latest version of Appwrite installed and an Appwrite project setup to test this function. If you have not already installed Appwrite, please do so. Installing Appwrite is really simple. Based on your operating system, run one of the following commands and installation should be complete in less than 2 minutes.

Unix

docker run -it --rm \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
    --entrypoint="install" \
    appwrite/appwrite:0.11.0
Enter fullscreen mode Exit fullscreen mode

Windows CMD

docker run -it --rm ^
    --volume //var/run/docker.sock:/var/run/docker.sock ^
    --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
    --entrypoint="install" ^
    appwrite/appwrite:0.11.0
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell

docker run -it --rm ,
    --volume /var/run/docker.sock:/var/run/docker.sock ,
    --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
    --entrypoint="install" ,
    appwrite/appwrite:0.11.0
Enter fullscreen mode Exit fullscreen mode

You can also find detailed installation instructions in the official Appwrite installation docs.

Another requirement to follow along is that you have a Mailgun account with a valid Mailgun Domain and API Key. With that aside, we're ready to get started.

🔓️ Activate Swift Functions Runtime

The Swift runtime needs to be enabled for you to be able to use it. This can be done easily using environment variables. The environment variables can be found in the .env file located in the Appwrite installation folder. If it's not present already, you'll need to add the following key-value pair to the .env file.

_APP_FUNCTIONS_RUNTIMES=swift-5.5
Enter fullscreen mode Exit fullscreen mode

Next, restart your stack using docker-compose up -d

🔑 Initialize Your Swift Function

First, create a project folder where you will create all the necessary files for your function. We will call this folder welcome-email. Once inside this folder, you can create a new Swift project with the following command.

docker run --rm -it -v $(pwd):/app -w /app swift:5.5 swift package init WelcomeEmail
Enter fullscreen mode Exit fullscreen mode

This will initialize a new Swift package project. It should create bunch of files, important ones to notice are

.
├── Sources/WelcomeEmail/main.swift
└── Package.swift
Enter fullscreen mode Exit fullscreen mode

➕ Add Appwrite Swift SDK dependency

Open the welcome-email folder in your favorite IDE and add the following code to your Package.swift file.

import PackageDescription

let package = Package(
    name: "WelcomeEmail",
    dependencies: [
        .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "WelcomeEmail",
            dependencies: [
                .product(name: "AsyncHTTPClient", package: "async-http-client"),
            ])
    ]
)
Enter fullscreen mode Exit fullscreen mode

Here we add the async-http-client SDK for swift under dependencies as well as under the executable's target dependencies.

✍️ Write your Function

Open Sources/WelcomeEmail/main.swift and fill in the following code:

import AsyncHTTPClient
import Foundation

func sendSimpleMessage(name: String, email: String) throws  {
    let message = "Welcome \(name)!"
    let targetURL = "https://api.mailgun.net/v3/\(MAILGUN_DOMAIN)/messages"
    let params =  [
        "from" : "Excited User <hello@appwrite.io>",
        "to" : email,
        "subject" : "hello",
        "text" : message
    ]

    var request: HTTPClient.Request
    do {
        request = try HTTPClient.Request(
            url: targetURL,
            method: .RAW(value: "POST")
        )

        let auth = "api:\(MAILGUN_API_KEY)".data(using: String.Encoding.utf8)!.base64EncodedString()
        request.headers.add(name: "Content-Type", value: "multipart/form-data")
        request.headers.add(name: "Authorization", value: "Basic \(auth)")

        buildMultipart(&request, with: params)

        httpClient.execute(request: request).whenComplete { result in
            switch result {
            case .failure(let error):
                print("Error: \(error)")
            case .success(let response):
                if response.status == .ok {
                    print("Message sent!")
                } else {
                    print("Error: \(response.status)")
                }
            }

            group.leave();
        }

    } catch let error {
        print(error)
        return
    }
}

let MAILGUN_DOMAIN = ProcessInfo.processInfo.environment["MAILGUN_DOMAIN"] ?? "";
let MAILGUN_API_KEY = ProcessInfo.processInfo.environment["MAILGUN_API_KEY"] ?? "";
let APPWRITE_FUNCTION_EVENT_DATA = ProcessInfo.processInfo.environment["APPWRITE_FUNCTION_EVENT_DATA"] ?? "{}"

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
let group = DispatchGroup()

do {
    let data = Data(APPWRITE_FUNCTION_EVENT_DATA.utf8)

    guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        throw "Unable to parse APPWRITE_FUNCTION_EVENT_DATA"
    }

    guard let name = json["name"] as? String else {
        throw "Unable to parse name"
    }

    guard let email = json["email"] as? String else {
        throw "Unable to parse email"
    }

    group.enter()
    try sendSimpleMessage(name: name, email: email)
    group.wait()

} catch let error {
    print(error)
}
Enter fullscreen mode Exit fullscreen mode

The environment variables that we are accessing here are either already available or are later set in the Appwrite Function's settings.

Next create a new file under Sources/WelcomeEmail/File.swift that will house one of our helper classes

import NIO

open class File {

    public let name: String
    public var buffer: ByteBuffer

    public init(name: String, buffer: ByteBuffer) {
        self.name = name
        self.buffer = buffer
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to create one more file Sources/WelcomeEmail/Utils.swift for some of our utility functions.

import AsyncHTTPClient
import NIO

let DASHDASH = "--"
let CRLF = "\r\n"
let boundaryChars = "abcdefghijklmnopqrstuvwxyz1234567890"

func randomBoundary() -> String {
    var string = ""
    for _ in 0..<16 {
        string.append(boundaryChars.randomElement()!)
    }
    return string
}

func buildMultipart(
    _ request: inout HTTPClient.Request,
    with params: [String: Any?] = [:]
) {
    func addPart(name: String, value: Any) {
        bodyBuffer.writeString(DASHDASH)
        bodyBuffer.writeString(boundary)
        bodyBuffer.writeString(CRLF)
        bodyBuffer.writeString("Content-Disposition: form-data; name=\"\(name)\"")

        if let file = value as? File {
            bodyBuffer.writeString("; filename=\"\(file.name)\"")
            bodyBuffer.writeString(CRLF)
            bodyBuffer.writeString("Content-Length: \(bodyBuffer.readableBytes)")
            bodyBuffer.writeString(CRLF+CRLF)
            bodyBuffer.writeBuffer(&file.buffer)
            bodyBuffer.writeString(CRLF)
            return
        }

        let string = String(describing: value)
        bodyBuffer.writeString(CRLF)
        bodyBuffer.writeString("Content-Length: \(string.count)")
        bodyBuffer.writeString(CRLF+CRLF)
        bodyBuffer.writeString(string)
        bodyBuffer.writeString(CRLF)
    }

    let boundary = randomBoundary()
    var bodyBuffer = ByteBuffer()

    for (key, value) in params {
        switch key {
        case "file":
            addPart(name: key, value: value!)
        default:
            if let list = value as? [Any] {
                for listValue in list {
                    addPart(name: "\(key)[]", value: listValue)
                }
                continue
            }
            addPart(name: key, value: value!)
        }
    }

    bodyBuffer.writeString(DASHDASH)
    bodyBuffer.writeString(boundary)
    bodyBuffer.writeString(DASHDASH)
    bodyBuffer.writeString(CRLF)

    request.headers.remove(name: "content-type")
    request.headers.add(name: "Content-Length", value: bodyBuffer.readableBytes.description)
    request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(boundary)\"")
    request.body = .byteBuffer(bodyBuffer)
}

extension String: Error {}
Enter fullscreen mode Exit fullscreen mode

⚙️ Build the Function Binary

In order to deploy our function, we need to first build the project. Our runtime is based on the slim version of official Swift docker image, so we'll use the official Swift docker image to build our project.

From the welcome-email directory, run the following command

$ docker run --rm -it -v $(pwd):/app -w /app swift:5.5 swift build
Enter fullscreen mode Exit fullscreen mode

This should build the project. Ensure that your folder structure looks like this

.
├── .build/x86_64-unknown-linux-gnu/debug/WelcomeEmail
├── Package.swift
├── README.md
└── Sources
    └── WelcomeEmail
        ├── File.swift
        ├── main.swift
        └── Utils.swift
Enter fullscreen mode Exit fullscreen mode

There could be other files and folders as well, but you can ignore those.

☁️ Create a Function in Your Appwrite Console

Login to your Appwrite console and open the project of your choosing. On the sidebar, tap on the Functions menu. In the following screen, tap the Add Function button.

Image description

We'll call our Cloud function WelcomeEmail. and select swift-5.5 for the environment. Then tap Create.

🧑‍💻 Deploy Tag

Once your function is created, you'll be taken to the Function Overview screen. Click the Deploy Tag button at the bottom of the function overview page and then switch to the Manual tab.

Image description

Let's first create a tarfile that contains our function.

$ tar -zcvf code.tar.gz -C .build/x86_64-unknown-linux-gnu/ debug/WelcomeEmail
Enter fullscreen mode Exit fullscreen mode

Head back to the Deploy a New Tag dialog and upload the code.tar.gz that we just created and use ./WelcomeEmail for the Command.

✅ Activate tag

Once you deploy your tag, it will be listed under Tags on the Overview page. Activate your most recent tag ( if you have multiple versions ).

💡 Adding Triggers and Environment Variables

On the Functions page, switch to the Settings tab. A function can be triggered based on an event or a schedule you choose. This particular function should be triggered by the users.create and the account.create events. Select these events from the events section.

In the Variables section, tap the Add Variable button and add the following variables and click the Update button.

  • MAILGUN_API_KEY - Your Mailgun API Key.
  • MAILGUN_DOMAIN - Your Mailgun domain.

✨️ Verify it's working

It's about time to check if all our hard work has finally paid off! Let's create a user and verify that our Cloud Function has sent them a welcome email!

Head to the Users page from the Sidebar and click the Add User button. Give your user a valid name and email ID and click Create. Back in the Functions page, you can now examine the logs of your function execution.

If you're using a sandbox Mailgun account, ensure that the email ID is an Authorized Recipient

📚️ Resources

Hope you enjoyed this article! We love contributions and encourage you to take a look at our open issues and ongoing RFCs.

If you get stuck anywhere, feel free to reach out to us on our friendly support channels run by humans 👩‍💻.

Here are some handy links for more information:

Discussion (0)