Gmail
kotlin
implementation("dev.reapermaga.mailkt:gmail:0.1.0")Register the OAuth client
- Open APIs & Services → Credentials in the Google Cloud console.
- Create an OAuth client ID of type Web application.
- Add every exact callback URI your backend uses, per environment, under Authorized redirect URIs.
- Configure the OAuth consent screen. The full-mailbox scope
https://mail.google.com/is a restricted scope: test users work immediately, public apps need Google's verification.
MailKT requests the scopes https://mail.google.com/ openid email, which cover IMAP, SMTP and identity verification.
Configure
kotlin
fun gmailClient(tokens: InMemoryTokenStore, sessions: InMemoryAuthorizationSessionStore): Gmail {
val config = GmailConfig(
clientId = System.getenv("GMAIL_CLIENT_ID"),
clientSecret = System.getenv("GMAIL_CLIENT_SECRET"),
// Exact backend callback URIs registered with Google for this environment.
allowedRedirectUris = setOf(
URI("https://app.example.com/oauth/gmail/callback"),
URI("http://localhost:8080/oauth/gmail/callback"), // local development
),
)
return Gmail(config, tokens, sessions)
}| Property | Description |
|---|---|
clientId | OAuth client ID. |
clientSecret | OAuth client secret. Keep it on the backend. |
allowedRedirectUris | Exact callback URIs registered with Google. beginAuthorization rejects any other. |
Begin
Return the URL to your frontend, which navigates the browser to it:
kotlin
/** Backend endpoint: returns the URL the frontend navigates the browser to. */
suspend fun beginGmailAuthorization(gmail: Gmail, email: String): String {
val request = gmail.beginAuthorization(
expectedEmail = MailAddress(email),
redirectUri = URI("https://app.example.com/oauth/gmail/callback"),
)
return request.authorizationUrl.toString()
}Complete and open
The route registered as redirect URI hands the query parameters to MailKT:
kotlin
/** Backend route registered as the redirect URI: hand the query parameters to MailKT. */
suspend fun gmailCallback(gmail: Gmail, code: String?, state: String?, error: String?): Mailbox {
val id = gmail.completeAuthorization(AuthorizationCallback(code, state, error))
println("Authorized mailbox of provider ${id.provider}")
return gmail.open(MailAddress(requireNotNull(System.getenv("GMAIL_ADDRESS"))), MailboxOptions())
}completeAuthorization returns the MailboxId it stored tokens for. Open the mailbox now or later; open only needs stored tokens.
Endpoints
| Protocol | Host | Port | Security |
|---|---|---|---|
| IMAP | imap.gmail.com | 993 | Implicit TLS, XOAUTH2 |
| SMTP | smtp.gmail.com | 465 | Implicit TLS, XOAUTH2 |
Gmail's SMTP service stores a copy of sent mail itself, so send(saveToSent = true) never appends a duplicate to Sent.
Gmail labels are folders
Over IMAP, Gmail labels appear as folders and [Gmail]/All Mail contains every message. The same message can therefore show up in several folders; conversations merge such copies.