A mailbox app (Kotlin)
Kotlin on Android: sign in, read, stay in step, send.
An Android mail app in Kotlin: sign in with ZiloOne, show folders and messages, stay in step as mail arrives, send with attachments. The same steps work in Swift, Flutter or anything else.
What you'll use#
- AppAuth for Android for signing in (PKCE, the system browser, refresh).
- OkHttp (or Ktor) and kotlinx.serialization for the API.
- Permissions:
openid email profile mail.read mail.write mail.send.
- Register the appAt Your apps › New app: kind Mobile or desktop, callback
com.acme.mail:/callback, the mail permissions, anyone with ZiloOne. - Sign inKOTLIN
// build.gradle: implementation("net.openid:appauth:0.11.1") // manifest: <data android:scheme="com.acme.mail"/> on RedirectUriReceiverActivity (appAuthRedirectScheme) val issuer = Uri.parse("https://accounts.zilomail.in/oidc") AuthorizationServiceConfiguration.fetchFromIssuer(issuer) { config, ex -> val request = AuthorizationRequest.Builder(config!!, CLIENT_ID, ResponseTypeValues.CODE, Uri.parse("com.acme.mail:/callback")) .setScopes("openid", "email", "profile", "mail.read", "mail.write", "mail.send") .build() signIn.launch(authService.getAuthorizationRequestIntent(request)) } // In the result callback: val response = AuthorizationResponse.fromIntent(data!!) authService.performTokenRequest(response!!.createTokenExchangeRequest()) { tokens, ex -> authState.update(tokens, ex) // AuthState keeps and refreshes tokens secureStore.save(authState.jsonSerializeString()) // EncryptedSharedPreferences } - Call the API with a fresh tokenKOTLIN
val api = "https://api.zilomail.in/v1" suspend fun <T> withToken(block: suspend (String) -> T): T = suspendCoroutine { cont -> authState.performActionWithFreshTokens(authService) { token, _, ex -> if (ex != null) cont.resumeWithException(ex) else CoroutineScope(Dispatchers.IO).launch { cont.resume(block(token!!)) } } } suspend fun get(path: String): JsonObject = withToken { token -> val req = Request.Builder().url(api + path).header("Authorization", "Bearer $token").build() client.newCall(req).execute().use { res -> val body = res.body!!.string() if (!res.isSuccessful) throw ApiException(Json.parseToJsonElement(body).jsonObject["error"]!!.jsonObject) Json.parseToJsonElement(body).jsonObject } } - Folders and the inboxOpen one withKOTLIN
val folders = get("/mail/folders")["data"]!!.jsonArray // name, role, total, unread val inbox = get("/mail/messages?folder=inbox&limit=50") val messages = inbox["data"]!!.jsonArray // id, from, subject, snippet, read… val next = inbox["next"]?.jsonPrimitive?.contentOrNull // more: ?cursor=$nextGET /mail/messages/{id}(text, cleaned HTML, attachments), then mark it read withPATCH { "read": true }. - Stay in stepGet the feed's position once, then hold a request open: it answers as soon as anything changes (new mail, read on the phone's other app, moved in webmail, deleted).Run it in a foreground service while the app is open, and a WorkManager job every 15 minutes when it isn't.KOTLIN
var cursor = get("/mail/changes")["cursor"]!!.jsonPrimitive.content // "now" while (isActive) { val page = get("/mail/changes?since=$cursor&wait=25") for (c in page["data"]!!.jsonArray) { val change = c.jsonObject when (change["kind"]!!.jsonPrimitive.content) { "created" -> fetchAndInsert(change["messageId"]!!.jsonPrimitive.content) "updated" -> refreshFlagsAndFolder(change["messageId"]!!.jsonPrimitive.content) "deleted" -> removeLocally(change["messageId"]!!.jsonPrimitive.content) } } cursor = page["cursor"]!!.jsonPrimitive.content } // 410 cursor_expired (away more than 30 days): list the folders again and start afresh. - Send, with an attachmentKOTLIN
val body = buildJsonObject { putJsonArray("to") { add("priya@example.com") } put("subject", "Quote for October") put("text", "Hi Priya, the quote is attached.") put("inReplyTo", original.messageId) // a reply: threads it putJsonArray("attachments") { addJsonObject { put("filename", "quote.pdf") put("contentType", "application/pdf") put("content", Base64.encodeToString(pdfBytes, Base64.NO_WRAP)) } } } post("/mail/send", body, idempotencyKey = UUID.randomUUID().toString()) // 202: queued, copy in Sent
Drafts are ordinary messages in Drafts (/mail/drafts), shared with webmail and IMAP apps: save as the person types, send with POST /mail/drafts/{id}/send.