Sending Transactional Emails Using Ktor, Kotlin, and MailerSend

In this, hands-on tutorial I will show you how to send transactional emails using Ktor, Kotlin, and MailerSend.
This is the featured image for article about how to send transatcional emails with Ktor, Kotlin, and MailerSend and contains Ktor and MailerSend logos in the foreground and a desk setup in the blurred background.

Hello and welcome to my next, hands-on tutorial 🙂 This time, I would like to show you how to send transactional emails using Ktor, Kotlin, and MailerSend.

Sooner or later, every developer will have to implement email-sending functionality in their application and it’s worth checking out various solutions, to pick the one that will best meet our needs.

And although the term “transactional emails” sounds complicated, in this tutorial I will prove how easily we can use them with Ktor and Kotlin.

Video Tutorial

If you prefer video content, then check out my video:

 

If you find this content useful, please leave a subscription  😉

Prerequisites

Before we start, let’s see what exactly we will need for this tutorial.

First of all, we will need a MailerSend account. The free version will be fair enough for the purpose of learning and even simple projects.

Note: the above link is a referral and if you will purchase any paid plan, I’ll get a small commission for that. To be clear: this fact affects the merits of this article in no way 🙂

Additionally, you will need to have a domain, which you can verify. This is necessary, in order to work with any transactional email providers.

Configure Domain in MailerSend

I won’t go into much detail in this paragraph, because DNS settings will be dependent on your domain registrar. Nevertheless, let’s take look at how to add a new domain to MailerSend.

So, assuming we created our account and verified the email along with their policies, we should see the following:

 

Screenshot presents MailerSend dashboard.

 

As the next step, let’s click the Start button next to the “Add a sending domain”:

 

Screenshot presents a modal, which allows user to enter a domain name, which we would like to register for transactional emails sending.

 

Right here, let’s specify the domain name- test.codersee.com in my case- and confirm with the Add domain button.

Nextly, we should see the Domain verification modal:

 

Screenshot presents domain verification modal with SPF, DKIM and RETURN-PATH settings to copy

 

As we can see, in order to verify our domain, we need to navigate to our domain registrar and modify the DNS settings using the values specified in the modal. When it’s done, let’s click the Check/Re-check now button.

Note: DNS propagation can take some time, so don’t worry if you see the error messages (just like above). In such a case, give it some time and please get back to this page. MailerSend will send you an e-mail when everything is verified, as well.

If everything succeeded, we should see the following page:

 

 

One important note before we head to the next part. At this point, we will be able to send emails only to addresses and domains we previously verified– the e-mail address we use for registration and all addresses in a verified domain in our case.

Nevertheless, if you would like to send emails outside the domain, you will need to get approval.

Generate API Key

In order to send transactional emails with MailerSend in our Ktor app, we will need to generate a new API key.

To do so, let’s navigate to our domains (Email -> Domains in the left sidebar) and click the manage button:

 

Screenshot presents the test.codersee.com domain details page.

 

On this page, we can manage API tokens and configure plenty of other things, like webhooks or tracking.

Anyway, let’s click the Generate new token button:

 

Screenshot presents a MailerSend modal, where we can set a name for the API token and permission level for it.

 

As we can see, right here we can specify the name and access granted to the new token. And although for simplicity we will stick with “Full access”, in real-life scenarios we should always follow the principle of least privilege.

Finally, let’s click the Create token:

Screenshot shows a modal with a censored API token value

 

As can be seen, the API token was created successfully and it’s time to save the value.

Note: please persist this value securely and never share it with anyone!

Generate Ktor Project

With all of that being done in MailerSend, we can finally switch to the Ktor & Kotlin part of our tutorial about transactional emails.

As always, let’s navigate to https://start.ktor.io/ and specify desired project settings:

 

The image shows Ktor Project Generator page, which we use to generate app skeleton, which then will be used to implement transactional emails with MailerSend.

Following, let’s click the Add plugins (we don’t need anything else), generate the project, and import it to our IDE.

Nextly, let’s add the Ktor client and Jackson dependencies inside the build.gradle.kts:

implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
implementation("io.ktor:ktor-client-okhttp:$ktor_version")

implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-jackson:$ktor_version")

Of course, Jackson is not the only option here, and GSON, or kotlinx.serialization are well supported in Ktor, as well.

 

Image shows two ebooks people can get for free after joining newsletter

Configure Client

As the next step, let’s navigate to the application.conf file and add a custom MailerSend config:

mailersend {
  token = ${MAILERSEND_TOKEN}
  baseUrl = ${MAILERSEND_BASE_URL}
}

This way, we can pass the token and URL values securely through environment variables. And although the token will be unique, for the baseUrl, let’s set https://api.mailersend.com/v1/.

Nextly, let’s create the MailerSendClient.kt file and put the following:

fun Application.configureClient() {

  val token = environment.config.property("mailersend.token").getString()
  val baseUrl = environment.config.property("mailersend.baseUrl").getString()

  val mailerSendClient = HttpClient {
    install(ContentNegotiation) {
      jackson()
    }
  }

  runBlocking {
    // To be done in the next paragraphs
  }
}

As we can see, the above function reads configuration properties and installs the ContentNegotiation plugin using Jackson. Moreover, we’ve prepared the runBlocking block, where we will invoke our future functions.

Lastly, let’s navigate to the Application.kt file and make use of our config:

fun Application.module() {
  configureClient()
}

At this point, we can run our application and verify that it is working, as expected. If we forget to set environment variables correctly, we should see something like this:

Exception in thread “main” com.typesafe.config.ConfigException $ UnresolvedSubstitution: application.conf @ […] Could not resolve substitution to a value: ${MAILERSEND_BASE_URL}

Send Simple Email

With all of that done, we can finally start sending transactional emails. In this paragraph, we will learn how to send a simple email with MailerSend.

Implement EmailRequest

As the first step, let’s create a new data class called EmailRequest:

@JsonInclude(JsonInclude.Include.NON_NULL)
data class EmailRequest(
    val from: Recipient,
    val to: List<Recipient>,
    val subject: String,
    val text: String,
    val html: String,
    val variables: List<Variable>? = null
) {
    data class Recipient(
        val email: String,
        val name: String
    )

    data class Variable(
        val email: String,
        val substitutions: List<Substitution>
    ) {
        data class Substitution(
            @JsonProperty("var") val variable: String,
            val value: String
        )
    }
}

Basically, objects of this class will be serialized into JSONs when querying MailerSend API.

The @JsonInclude annotation states that if the value is set to null, then it should not be added to the JSON payload (so we won’t send "whatever": null) . Additionally, MailerSend requires us to send the “var” field inside the substitution. Nevertheless, var is a reserved keyword in Kotlin, so we have to make use of the @JsonProperty.

Use Ktor Client to Perform Requests

Following, let’s add a new function called sendSingleEmail:

suspend fun sendSingleEmail(
  mailerSendClient: HttpClient,
  url: String,
  token: String,
  emailRequest: EmailRequest
): HttpResponse =
    mailerSendClient.post(url) {
      headers {
        append(HttpHeaders.Authorization, "Bearer $token")
      }
      contentType(ContentType.Application.Json)
      setBody(emailRequest)
    }

As we can see, this one is responsible for sending POST requests to the passed URL with the Authorization header containing the Bearer token.

Moreover, the function takes the EmailRequest as an argument, which makes it reusable.

Send Transactional Client with Ktor Client

Lastly, let’s implement the sendSimpleMessage:

suspend fun sendSimpleMessage(
    mailerSendClient: HttpClient,
    baseUrl: String,
    token: String
) {
    val subject = "Hello from {\$company}!"
    val html = """
        Hello <b>{${'$'}name}</b>, nice to meet you!
    """.trimIndent()
    val text = """
        Hello {${'$'}name}, nice to meet you!
    """.trimIndent()

    val emailRequest = EmailRequest(
        from = EmailRequest.Recipient(
            email = "example@test.codersee.com",
            name = "Codersee"
        ),
        to = listOf(
            EmailRequest.Recipient(
                email = "example@test.codersee.com",
                name = "Pjoter"
            )
        ),
        subject = subject,
        html = html,
        text = text,
        variables = listOf(
            EmailRequest.Variable(
                email = "example@test.codersee.com",
                substitutions = listOf(
                    EmailRequest.Variable.Substitution(
                        variable = "company",
                        value = "Codersee"
                    ),
                    EmailRequest.Variable.Substitution(
                        variable = "name",
                        value = "Piotr"
                    )
                )
            )
        )
    )

    val response = sendSingleEmail(
        mailerSendClient = mailerSendClient,
        url = "$baseUrl/email",
        token = token,
        emailRequest = emailRequest
    )

    if (response.status == HttpStatusCode.Accepted)
        println("Email sent successfully.")
    else
        println("Error when sending email.")
}

And although the above code snippet consists of a lot of lines, it’s definitely easier than it looks.

Firstly, we prepare the EmailRequest instance, which after serialization, will become this JSON:

{
    "from": {
        "email": "example@test.codersee.com",
        "name": "Codersee"
    },
    "to": [
        {
            "email": "example@test.codersee.com",
            "name": "Pjoter"
        }
    ],
    "subject": "Hello from {$company}!",
    "html": "Hello <b>{$name}</b>, nice to meet you!"
    "variables": [
        {
            "email": "example@test.codersee.com",
            "substitutions": [
                {
                    "var": "company",
                    "value": "Codersee"
                },
                {
                    "var": "name",
                    "value": "Piotr"
                }
            ]
        }
    ]
}

Whereas most of the fields a pretty descriptive, let’s take a look at one, important thing- variables.

As we can see, we can inject variables into the email subject, or content by simply specifying them with a dollar sign inside curly braces, for example: {$someVariable}. This way, we can specify dynamic values for each recipient, like name and company in our example.

In the next lines, we simply send the request to the MailerSend API, and if the response status code is 202 Accepted, then the email was sent successfully:

 

Screenshot shows an example email send with MailerSend API using Ktor client.

 

Of course, in order to make it work, let’s invoke our function inside the configureClient:

runBlocking {
  sendSimpleMessage(mailerSendClient, baseUrl, token)
}

Send Emails With Templates

At this point, we already know how to send transactional emails with Ktor and MailerSend. Moreover, we’ve seen how to send HTML content, which is a great way to customize the template.

Nevertheless, if you’d like to learn how to make your life a bit easier and use a graphic builder for templates, then you’re gonna like this paragraph.

Prepare Template Using MailerSend Builder

As the first step, let’s navigate to the templates page: https://app.mailersend.com/templates.

Nextly, let’s click Create template button and select Drag & drop editor option:

 

Screenshot presents 3 options we can select when creating a new template: drag & drop editor, rich-text editor and HTML editor.

 

This way, we’re redirected to the Template gallery, which contains ~50 predefined templates, which we can adjust to our needs. In this tutorial, we will select the Reset password:

 

Image presents MailerSend template gallery with highlited Reset passoword template.

 

After we click Choose, we’re redirected to the builder page, where we can make adjustments:

 

Screenshot shows MailerSend template builder and edited template.

 

As we can see, when using the builder, we need to encapsulate variables with double curly brackets.

When we finish, let’s click the Save & Publish button and write down the Template ID on the next page- we will need it later.

Adjust EmailRequest Class

With that being done, let’s get back to the Ktor project and edit the EmailRequest class:

@JsonInclude(JsonInclude.Include.NON_NULL)
data class EmailRequest(
    val from: Recipient,
    val to: List<Recipient>,
    val subject: String,
    @JsonProperty("template_id") val templateId: String,
    val personalization: List<CustomPersonalization>
) {
    data class Recipient(
        val email: String,
        val name: String
    )

    data class CustomPersonalization(
        val email: String,
        val data: PersonalizationData
    ) {
        data class PersonalizationData(
            val name: String,
            @JsonProperty("my_super_generated_code") val code: String
        )
    }
}

This time, instead of specifying the content manually, we simply pass the template identifier and a list of personalizations.

In our case, the desired JSON looks, as follows:

{
  "from": {
    "email": "example@test.codersee.com",
    "name": "Codersee"
  },
  "to": [
    {
      "email": "example@test.codersee.com",
      "name": "Pjoter"
    }
  ],
  "subject": "Please Reset Your Password",
  "templateId": "v69oxl587pzl785k",
  "personalization": [
    {
       "email": "example@test.codersee.com",
       "data": {
         "name": "Pjoter",
         "code": "f7fcc37c-4b7a-4919-aa91-a5ac7edd55d7"
       }
     }
  ]
}

Implement Sending Functionality

With all of that prepared, let’s implement the sendMessageUsingTemplate:

suspend fun sendMessageUsingTemplate(
    mailerSendClient: HttpClient,
    baseUrl: String,
    token: String
) {
    val subject = "Please Reset Your Password"

    val emailRequest = EmailRequest(
        from = EmailRequest.Recipient(
            email = "example@test.codersee.com",
            name = "Codersee"
        ),
        to = listOf(
            EmailRequest.Recipient(
                email = "example@test.codersee.com",
                name = "Pjoter"
            )
        ),
        subject = subject,
        templateId = "v69oxl587pzl785k",
        personalization = listOf(
            EmailRequest.CustomPersonalization(
                email = "example@test.codersee.com",
                data = EmailRequest.CustomPersonalization.PersonalizationData(
                    name = "Pjoter",
                    code = UUID.randomUUID().toString()
                )
            )
        )
    )

    val response = sendSingleEmail(
        mailerSendClient = mailerSendClient,
        url = "$baseUrl/email",
        token = token,
        emailRequest = emailRequest
    )

    if (response.status == HttpStatusCode.Accepted)
        println("Email sent successfully.")
    else
        println("Error when sending email.")

As we can see, the logic itself remains almost exactly the same and the only thing, which changes, is the payload we send.

Lastly, let’s put the following in the configureClient and rerun the application:

runBlocking {
  sendSimpleMessage(mailerSendClient, baseUrl, token)
}

As a result, we should see the message “Email sent successfully.” and the following email in our inbox:

Image shows an example email from inbox.

Handling ErrorsEmails With Templates

I simply couldn’t finish this article about sending transactional emails with Ktor and MailerSend without showing how to handle errors.

And although we try our best to avoid them, sooner or later they will pop up. Thankfully, MailerSend API informs us in a pretty neat way about any issues. For example:

{
    "message": "The template_id must be an integer. (and 2 more errors)",
    "errors": {
        "template_id": [
            "The template_id must be an integer."
        ],
        "to.0.email": [
            "The to.0.email field is required."
        ],
        "variables.0.email": [
            "The variables.0.email field does not exist in to.*.email."
        ]
    }
}

Create ErrorResponse

As the first step, let’s implement the ErrorResponse class:

data class ErrorResponse(
  val message: String,
  val errors: Map<String, List<String>>
)

We will use this simple data class to deserialize JSON response into the object.

Implement handleError

Nextly, let’s add the handleError function:

suspend fun handleError(response: HttpResponse) {
    val statusCode = response.status.value
    val errorBody =  response.body<ErrorResponse>()
    println("Email sending failed with status code $statusCode and message '${errorBody.message}'. Errors:")

    errorBody.errors.forEach { (fieldName, fieldErrors) ->
        println("  * field name: [$fieldName]. Messages:")
        fieldErrors.forEach { error -> println("    - $error") }
    }
}

As we can see, this function takes the HttpResponse from MailerSend API and parses the JSON into the ErrorResponse instance. Thanks to that we can iterate and display all the errors.

Of course, as the last step, we need to modify our conditional statement, to make use of this snippet:

if (response.status == HttpStatusCode.Accepted)
  println("Email sent successfully.")
else 
  println("Error when sending email.")

Sending Emails With Ktor And MailerSend Summary

And that’s all for this article about how to send transactional emails using Ktor, Kotlin, and MailerSend.

As always, you can find the source code in this GitHub repository.

Happy to hear your thoughts, feedback, or ideas in the comments 🙂

Share this:

Related content

2 Responses

  1. Simply running a blocking coroutine during ktor app startup seems a bit odd dont it? Why not put it behind an endpoint?

    1. Hi and thank you for your input 🙂

      Well, the endpoint without the authorization seems a bit odd, also the REST API without a client app, seems a bit odd as well, etc, etc…

      When creating articles about particular topics I do my best to get straight to the point and focus on what’s the most important- so here the core topic is how to send e-mails with Ktor and MailerSend. Also, this way it becomes a bit more generic- the code focused on e-mail sending can be easily adjusted according to everyone’s needs- it can be used as a part of a backend app exposing some REST API, but also a client application.

Leave a Reply

Your email address will not be published. Required fields are marked *

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy