Outlook / Microsoft 365
kotlin
implementation("dev.reapermaga.mailkt:outlook:0.1.0")The Outlook module uses MSAL4J as a confidential client for Exchange Online IMAP and SMTP.
Register the app in Microsoft Entra
- In the Entra admin center, go to App registrations → New registration.
- Choose the supported account types. Multi-tenant plus personal accounts matches the default
commonauthority. - Add a Web platform redirect URI for each exact callback route.
- Under Certificates & secrets, create a client secret for the backend.
- Under API permissions, add the delegated Office 365 Exchange Online permissions
IMAP.AccessAsUser.Alland, if you send mail,SMTP.Send.
Tenant settings
IMAP and authenticated SMTP must be enabled for the mailbox in Exchange Online. Organizations often disable SMTP AUTH by default.
Configure
kotlin
fun outlookClient(tokens: InMemoryTokenStore, sessions: InMemoryAuthorizationSessionStore): Outlook {
val config = OutlookConfig(
clientId = System.getenv("OUTLOOK_CLIENT_ID"),
clientSecret = System.getenv("OUTLOOK_CLIENT_SECRET"), // held by the backend only
allowedRedirectUris = setOf(URI("https://app.example.com/oauth/outlook/callback")),
enableSending = true, // adds the SMTP.Send scope
)
return Outlook(config, tokens, sessions)
}| Property | Default | Description |
|---|---|---|
clientId | Application (client) ID. | |
clientSecret | Client secret, held by the backend only. | |
allowedRedirectUris | Exact callback URIs registered in Entra. | |
authority | https://login.microsoftonline.com/common/ | Use a tenant-specific authority to restrict sign-in to one organization. Must be HTTPS. |
enableSending | false | Adds the SMTP.Send scope and the SMTP endpoint. Without it mailbox.outbox is null. |
Requested scopes are https://outlook.office.com/IMAP.AccessAsUser.All, plus https://outlook.office.com/SMTP.Send when sending is enabled. MSAL4J adds offline_access for refresh tokens.
Authorize and open
kotlin
suspend fun outlookAuthorizeAndOpen(outlook: Outlook, email: String, code: String, state: String): Mailbox {
// 1. The backend creates the browser URL for the frontend.
val request = outlook.beginAuthorization(MailAddress(email), URI("https://app.example.com/oauth/outlook/callback"))
println("Send the browser to ${request.authorizationUrl}")
// 2. The provider redirects to the backend callback, which completes the flow.
outlook.completeAuthorization(AuthorizationCallback(code = code, state = state))
// 3. Open the mailbox using the stored tokens.
return outlook.open(MailAddress(email))
}In a real backend, the three steps are split across the begin endpoint and the callback route exactly as for Gmail.
Endpoints
| Protocol | Host | Port | Security |
|---|---|---|---|
| IMAP | outlook.office365.com | 993 | Implicit TLS, XOAUTH2 |
| SMTP | smtp.office365.com | 587 | STARTTLS, XOAUTH2 |
Exchange Online stores a copy of sent mail itself, so send(saveToSent = true) never appends a duplicate to Sent.