Перейти к содержанию

Ktor

Инструкция для приложений на Ktor (Kotlin, JVM). Mailexam подключается как SMTP-сервер через Jakarta Mail.

Что понадобится

  • Аккаунт Mailexam и проект с SMTP-учётными данными.
  • JDK 17+ и Kotlin 1.9+.

Скопируйте из приветственного письма (или кабинета) для вашего проекта:

  • ВАШ_ЛОГИН — SMTP-логин (например, xxxxx);
  • ВАШ_ПАРОЛЬ — SMTP-пароль (уникальная пара к логину);
  • хост — ВАШ_ЛОГИН.mailexam.ru (совпадает с логином).

1. Зависимости

Фрагмент build.gradle.kts:

plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.0"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm:3.0.0")
    implementation("io.ktor:ktor-server-netty-jvm:3.0.0")
    implementation("io.ktor:ktor-server-content-negotiation-jvm:3.0.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:3.0.0")
    implementation("com.sun.mail:jakarta.mail:2.0.1")
}

application {
    mainClass.set("ApplicationKt")
}

Создайте проект через Ktor Project Generator или вручную по структуре ниже.

2. Переменные окружения

Перед запуском задайте переменные (или используйте .env через вашу IDE):

MAILEXAM_LOGIN=ВАШ_ЛОГИН
MAILEXAM_PASSWORD=ВАШ_ПАРОЛЬ
MAILEXAM_PORT=587
MAIL_FROM=noreply@example.test

Хост SMTP: ВАШ_ЛОГИН.mailexam.ru.

Адрес отправителя

MAIL_FROM может быть любым тестовым адресом — письмо попадёт в Mailexam, а не реальному получателю.

Альтернативные порты

MAILEXAM_PORT=587

mail.smtp.starttls.enable=true

MAILEXAM_PORT=2525
MAILEXAM_PORT=25

mail.smtp.starttls.enable=false

3. Отправка почты

// src/main/kotlin/Mail.kt
import jakarta.mail.Authenticator
import jakarta.mail.Message
import jakarta.mail.PasswordAuthentication
import jakarta.mail.Session
import jakarta.mail.Transport
import jakarta.mail.internet.InternetAddress
import jakarta.mail.internet.MimeMessage
import java.util.Properties

object Mail {
    fun sendTest(to: String, subject: String, body: String) {
        val login = System.getenv("MAILEXAM_LOGIN")
            ?: error("MAILEXAM_LOGIN is not set")
        val password = System.getenv("MAILEXAM_PASSWORD")
            ?: error("MAILEXAM_PASSWORD is not set")
        val port = System.getenv("MAILEXAM_PORT")?.toInt() ?: 587
        val from = System.getenv("MAIL_FROM") ?: "noreply@example.test"

        val props = Properties().apply {
            put("mail.smtp.host", "$login.mailexam.ru")
            put("mail.smtp.port", port.toString())
            put("mail.smtp.auth", "true")
            put("mail.smtp.starttls.enable", (port == 587 || port == 2525).toString())
        }

        val session = Session.getInstance(props, object : Authenticator() {
            override fun getPasswordAuthentication(): PasswordAuthentication =
                PasswordAuthentication(login, password)
        })

        val message = MimeMessage(session).apply {
            setFrom(InternetAddress(from))
            setRecipients(Message.RecipientType.TO, InternetAddress.parse(to))
            setSubject(subject, "UTF-8")
            setText(body, "UTF-8")
        }

        Transport.send(message)
    }
}

4. Маршрут Ktor

// src/main/kotlin/Application.kt
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable

@Serializable
data class SendRequest(
    val to: String = "user@example.test",
    val subject: String = "Ktor + Mailexam",
    val body: String = "Тест Mailexam из Ktor",
)

fun main() {
    embeddedServer(Netty, port = 8080, host = "127.0.0.1", module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    install(ContentNegotiation) {
        json()
    }

    routing {
        post("/mail/test") {
            val payload = call.receive<SendRequest>()

            withContext(Dispatchers.IO) {
                Mail.sendTest(
                    to = payload.to,
                    subject = payload.subject,
                    body = payload.body,
                )
            }

            call.respond(mapOf("status" to "ok"))
        }
    }
}

Запуск и проверка:

./gradlew run
curl -X POST http://127.0.0.1:8080/mail/test \
  -H 'Content-Type: application/json' \
  -d '{"to":"user@example.test","subject":"Проверка","body":"Привет"}'

Письмо появится в кабинете Mailexam → ваш проект → входящие.

5. Локальная разработка и CI

Среда Рекомендация
local переменные окружения в Run Configuration IDE
CI секреты MAILEXAM_LOGIN, MAILEXAM_PASSWORD в GitLab CI/CD Variables

Пример для .gitlab-ci.yml:

variables:
  MAILEXAM_LOGIN: $MAILEXAM_LOGIN
  MAILEXAM_PASSWORD: $MAILEXAM_PASSWORD
  MAILEXAM_PORT: "587"
  MAIL_FROM: "noreply@example.test"

После интеграционного теста проверьте доставку через API Mailexam.

6. Типичные проблемы

Ошибка TLS или подключения

  • mail.smtp.host должен быть {логин}.mailexam.ru, логин в PasswordAuthentication — из письма.
  • Логин и пароль — пара из письма одного проекта.

Блокировка event loop

  • Вызывайте Transport.send в withContext(Dispatchers.IO), как в примере.

Письмо не в кабинете

  • Смотрите входящие того же проекта Mailexam.
  • Проверьте логи Ktor при исключении из Mail.sendTest.

См. также