Skip to content

Gmail ​

kotlin
implementation("dev.reapermaga.mailkt:gmail:0.1.0")

Register the OAuth client ​

  1. Open APIs & Services → Credentials in the Google Cloud console.
  2. Create an OAuth client ID of type Web application.
  3. Add every exact callback URI your backend uses, per environment, under Authorized redirect URIs.
  4. 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)
}
PropertyDescription
clientIdOAuth client ID.
clientSecretOAuth client secret. Keep it on the backend.
allowedRedirectUrisExact 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 ​

ProtocolHostPortSecurity
IMAPimap.gmail.com993Implicit TLS, XOAUTH2
SMTPsmtp.gmail.com465Implicit 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.

Released under the MIT License.