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:
As the next step, let’s click the Start button next to the “Add a sending domain”:
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:
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:
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:
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:
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:
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.
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:
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:
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:
After we click Choose, we’re redirected to the builder page, where we can make adjustments:
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:
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 🙂
2 Responses
Simply running a blocking coroutine during ktor app startup seems a bit odd dont it? Why not put it behind an endpoint?
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.