Quick start
This walkthrough opens a real Gmail mailbox from a terminal, using a loopback redirect URI so no web server is needed.
1. Create Google credentials
- In the Google Cloud console, create an OAuth client ID of type Web application.
- Add
http://localhost:8080/oauth/gmail/callbackas an authorized redirect URI. - While the app is in testing, add your account as a test user on the OAuth consent screen.
2. Configure the environment
Export the credentials and the address you want to open:
export GMAIL_CLIENT_ID="1234567890-abc.apps.googleusercontent.com"
export GMAIL_CLIENT_SECRET="your-client-secret"
export GMAIL_ADDRESS="you@gmail.com"$env:GMAIL_CLIENT_ID = "1234567890-abc.apps.googleusercontent.com"
$env:GMAIL_CLIENT_SECRET = "your-client-secret"
$env:GMAIL_ADDRESS = "you@gmail.com"3. Implement the stores
MailKT asks you for two stores. For a local run, in-memory versions are enough:
/** Demonstration only. A real store persists opaque bytes durably, encrypted, with atomic replacement. */
class InMemoryTokenStore : TokenStore {
private val values = ConcurrentHashMap<String, ByteArray>()
override suspend fun load(key: TokenKey): ByteArray? = values[key.storageKey]?.copyOf()
override suspend fun save(key: TokenKey, value: ByteArray) {
values[key.storageKey] = value.copyOf()
}
override suspend fun delete(key: TokenKey) {
values.remove(key.storageKey)
}
}/** Demonstration only. A shared backend needs a store whose `consume` is atomic across instances. */
class InMemoryAuthorizationSessionStore : AuthorizationSessionStore {
private val pending = ConcurrentHashMap<String, PendingAuthorization>()
override suspend fun save(session: PendingAuthorization) {
pending[session.state] = session
}
override suspend fun consume(state: String): PendingAuthorization? = pending.remove(state)
}Demonstration only
These keep secrets in memory and lose them on restart. See Persistence boundary for what a production store must guarantee.
4. Authorize and open
A complete command-line program:
import dev.reapermaga.mailkt.client.use
import dev.reapermaga.mailkt.gmail.Gmail
import dev.reapermaga.mailkt.gmail.GmailConfig
import dev.reapermaga.mailkt.model.AuthorizationCallback
import dev.reapermaga.mailkt.model.MailAddress
import dev.reapermaga.mailkt.model.SpecialUse
import java.net.URI
suspend fun main() {
val email = MailAddress(System.getenv("GMAIL_ADDRESS"))
val redirect = URI("http://localhost:8080/oauth/gmail/callback")
val config = GmailConfig(
clientId = System.getenv("GMAIL_CLIENT_ID"),
clientSecret = System.getenv("GMAIL_CLIENT_SECRET"),
allowedRedirectUris = setOf(redirect),
)
val gmail = Gmail(config, InMemoryTokenStore(), InMemoryAuthorizationSessionStore())
println("Open: ${gmail.beginAuthorization(email, redirect).authorizationUrl}")
print("code: ")
val code = readln()
print("state: ")
val state = readln()
gmail.completeAuthorization(AuthorizationCallback(code, state))
gmail.open(email).use { mailbox ->
val inbox = mailbox.folders.special(SpecialUse.INBOX)
println("Inbox found: ${inbox != null}")
}
}Run it, open the printed URL and consent. Google redirects to localhost:8080, which fails to load because nothing is listening. That is expected: copy the code and state query parameters from the address bar into the terminal.
What just happened
beginAuthorizationcreated a one-timestateand PKCE verifier, saved them through yourAuthorizationSessionStoreand returned the Google URL.completeAuthorizationconsumed that state exactly once, exchanged the code, verified the account matchesGMAIL_ADDRESSand saved the tokens through yourTokenStore.openbuilt the mailbox from the stored tokens using silent refresh only, connected, and returned a liveMailbox.useclosed it again, even on failure or cancellation.
In a real application, steps 1 and 2 are two HTTP endpoints of your backend. Continue with Hosted authorization.