Status: Production Ready — Complete implementation with comprehensive error handling and all API endpoints.
A complete Kotlin Multiplatform (KMP) client for the mail.tm API, built on Ktor 3.x and kotlinx.serialization.
- Complete API coverage - All mail.tm endpoints implemented
- Smart error handling - Mail.tm specific exceptions with detailed error messages
- Authentication support - Bearer token management with automatic retry
- Rate limiting support - Built-in rate limit tracking (8 QPS compliance)
- Real-time updates - Server-Sent Events (SSE) configuration for Mercure hub
- Professional validation - RFC-compliant email validation with detailed constraints
- Helper functions - Convenient methods for common operations
- Works on Android, iOS - Full multiplatform support
- Mockable with Ktor's
MockEnginefor unit tests
// settings.gradle.kts
repositories {
google()
mavenCentral()
}
// build.gradle.kts
dependencies {
implementation("io.github.hasanyalmanbas:mail-tm-client:1.0.6")
}// settings.gradle
repositories {
google()
mavenCentral()
}
// build.gradle
dependencies {
implementation 'io.github.hasanyalmanbas:mail-tm-client:1.0.6'
}- Kotlin 1.9+ (or newer matching your toolchain)
- Ktor 3.x
- kotlinx.serialization 1.9+
- KMP targets you plan to build (Android/iOS)
import tm.mail.api.createMailTmClient
import tm.mail.api.ApiClient
import tm.mail.api.mailTmEngine
suspend fun demo() {
// Simple way - use convenience function
val client = createMailTmClient()
// Or create manually with platform engine
val manualClient = ApiClient(mailTmEngine())
// Create account and authenticate in one step
val authenticatedClient = ApiClient.createAccountAndAuthenticate(
engine = mailTmEngine(),
address = "[email protected]",
password = "secure-password"
)
// Or authenticate with existing account
val existingClient = ApiClient.authenticateExisting(
engine = mailTmEngine(),
address = "[email protected]",
password = "secure-password"
)
// Get all messages
val messages = authenticatedClient.getAllMessages()
println("You have ${messages.size} messages")
}val client = ApiClient(mailTmEngine())
// Get a random available domain and create account
val randomAccount = client.createRandomAccount("my-password")
println("Created account: ${randomAccount.address}")
// Authenticate and start using
val token = client.createToken(randomAccount.address, "my-password")
client.setToken(token.token)// Get unread messages only
val unreadMessages = client.getUnreadMessages()
// Get specific message details
val messageDetail = client.getMessageById("msg-id")
// Mark message as read
client.markMessageAsSeen("msg-id")
// Mark all messages as read
client.markAllMessagesAsSeen()
// Delete all messages
client.deleteAllMessages()
// Get message source
val source = client.getMessageSource("msg-id")try {
val account = client.createAccount("[email protected]", "password")
} catch (e: MailTmException.AccountAlreadyExists) {
println("Account already exists: ${e.message}")
// Access original API response
println("API Error: ${e.originalResponse?.error}")
} catch (e: MailTmException.InvalidDomain) {
println("Invalid domain: ${e.message}")
} catch (e: MailTmException.RateLimited) {
println("Rate limited, try again later")
} catch (e: MailTmException.NetworkError) {
println("Network error: ${e.message}")
// Access underlying cause
e.cause?.printStackTrace()
}// Advanced client configuration
val client = ApiClient.builder()
.baseUrl("https://api.mail.tm")
.enableLogging(true)
.requestTimeout(45_000L)
.maxRetries(5)
.build(engine)
// With custom configuration object
val config = ApiClientConfig(
enableLogging = true,
requestTimeoutMillis = 60_000L,
maxRetries = 3
)
val client = ApiClient.create(engine, config)// Check rate limits after API calls
client.getMessages()
val rateInfo = client.getLastRateLimitInfo()
rateInfo?.let { info ->
println("Remaining requests: ${info.remaining}/${info.limit}")
println("Reset time: ${info.reset}")
}// Setup SSE for real-time message notifications
val account = client.getMe()
val sseConfig = client.createSSEConfig(account.id)
// Use sseConfig.mercureUrl to connect to Server-Sent Events
// Platform-specific EventSource implementation requiredval message = client.getMessageById("message-id")
// Security verification information
message.verifications?.let { verifications ->
println("TLS: ${verifications.tls?.version}")
println("SPF Passed: ${verifications.spf}")
println("DKIM Passed: ${verifications.dkim}")
}
// JSON-LD context information
println("Context: ${message.context}")
println("Type: ${message.jsonLdType}")POST /token→createToken(address, password)- Get auth token- Token management →
setToken(token)- Set bearer token for requests
POST /accounts→createAccount(address, password)- Create new accountGET /accounts/{id}→getAccountById(id)- Get account detailsDELETE /accounts/{id}→deleteAccount(id)- Delete accountGET /me→getMe()- Get current account info
GET /domains→getDomains(page?)- List available domainsGET /domains/{id}→getDomainById(id)- Get domain details
GET /messages→getMessages(page?)- List messagesGET /messages/{id}→getMessageById(id)- Get message detailsDELETE /messages/{id}→deleteMessage(id)- Delete messagePATCH /messages/{id}→markMessageAsSeen(id, seen)- Mark as read/unreadGET /sources/{id}→getMessageSource(id)- Get raw message source
createAccountAndAuthenticate()- Create account and login in one stepauthenticateExisting()- Login with existing credentialsgetRandomAvailableDomain()- Get a random active domaincreateRandomAccount()- Create account with random usernamegetAllMessages()- Get all messages (handles pagination)getUnreadMessages()- Get only unread messagesmarkAllMessagesAsSeen()- Mark all messages as readdeleteAllMessages()- Delete all messages
The client provides specific exception types for different error scenarios:
MailTmException.BadRequest- 400 Bad RequestMailTmException.Unauthorized- 401 UnauthorizedMailTmException.NotFound- 404 Not FoundMailTmException.Conflict- 409 ConflictMailTmException.UnprocessableEntity- 422 Validation ErrorMailTmException.RateLimited- 429 Too Many RequestsMailTmException.Server- 5xx Server Errors
MailTmException.AccountAlreadyExists- Email already registeredMailTmException.InvalidCredentials- Wrong email/passwordMailTmException.InvalidDomain- Domain not validMailTmException.AccountDisabled- Account is disabledMailTmException.MessageNotFound- Message doesn't existMailTmException.DomainNotAvailable- Domain not availableMailTmException.QuotaExceeded- Storage quota exceeded
MailTmException.NetworkError- Connection/network issuesMailTmException.TimeoutError- Request timeout
All exceptions include the original API error response when available:
catch (e: MailTmException) {
println("Error: ${e.message}")
e.originalResponse?.let { response ->
println("API Error: ${response.error}")
println("Violations: ${response.violations}")
}
}The client is fully mockable using Ktor's MockEngine:
@Test
fun testCreateAccount() = runBlocking {
val mockEngine = MockEngine { request ->
respond(
content = ByteReadChannel("""{"id":"123","address":"[email protected]"}"""),
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
val client = ApiClient.create(mockEngine)
val account = client.createAccount("[email protected]", "password")
assertEquals("123", account.id)
assertEquals("[email protected]", account.address)
}- Fork the project
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Production Ready: This client provides complete mail.tm API coverage with robust error handling and convenient helper functions.