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
MockEngine
for 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 = "user@example.com",
password = "secure-password"
)
// Or authenticate with existing account
val existingClient = ApiClient.authenticateExisting(
engine = mailTmEngine(),
address = "user@example.com",
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("test@example.com", "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 required
val 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":"test@example.com"}"""),
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
val client = ApiClient.create(mockEngine)
val account = client.createAccount("test@example.com", "password")
assertEquals("123", account.id)
assertEquals("test@example.com", 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.