DEV Community

Cover image for Kotlin & Gmail API - listing emails
vaclavhodek for Localazy

Posted on • Originally published at localazy.com

Kotlin & Gmail API - listing emails

The idea

Let's demonstrate the basic functionality on a useful idea. I have a folder/label in Gmail with a lot of emails. I want to list all email addresses - all senders.

Everything I need to do is a small console app to go through all emails with the given label and extracting the From header. If it contains the email address in format Name <email@address.com>, extract only the email address.

Let's dive into it!

Get credentials

Before we start, you need to create a new project in Google Cloud Console.

Under the new project, navigate to API & Services and enable Gmail API in Library.

In Credentials, click the + CREATE CREDENTIALS, select OAuth client ID and setup it like this:

OAuth client ID setup

Click Save and download the credentials for your newly created ID:

OAuth client ID setup

Save the downloaded file as credentials.json.

Kotlin project

Create a new Kotlin project with Gradle and add Google's dependencies:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib"
    implementation 'com.google.api-client:google-api-client:1.23.0'
    implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
    implementation 'com.google.apis:google-api-services-gmail:v1-rev83-1.23.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1'
}
Enter fullscreen mode Exit fullscreen mode

Authorization scopes

As we only want to go through labels, messages (we need just headers - metadata), we need a small and safe set of scopes:

private val SCOPES = setOf(
        GmailScopes.GMAIL_LABELS,
        GmailScopes.GMAIL_READONLY,
        GmailScopes.GMAIL_METADATA
)
Enter fullscreen mode Exit fullscreen mode

Beware that if you provide the GmailScopes.GMAIL_METADATA, you are not able to access the whole message. You have to omit it if you want to get the message body.

Authorize with Gmail

Fortunately, Google libraries come with everything we may need including the server for receiving the authorization request. The whole implementation is as simple as:

private fun getCredentials(httpTransport: NetHttpTransport): Credential? {
    val inputStream = File("credentials.json").inputStream()
    val clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, InputStreamReader(inputStream))
    val flow = GoogleAuthorizationCodeFlow.Builder(httpTransport, JSON_FACTORY, clientSecrets, SCOPES)
            .setDataStoreFactory(FileDataStoreFactory(File(TOKENS_DIRECTORY_PATH)))
            .setAccessType("offline")
            .build()
    val receiver = LocalServerReceiver.Builder().setPort(8888).build()
    return AuthorizationCodeInstalledApp(flow, receiver).authorize("user")
}
Enter fullscreen mode Exit fullscreen mode

This code above outputs the request for authorization to the console:

Please open the following address in your browser:
  https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=...
Enter fullscreen mode Exit fullscreen mode

Click the link, authorize the app, and the authorization token is received and stored in TOKENS_DIRECTORY_PATH. You only need to do this for the first time. Next time, the stored token is used.

Build client & get labels

We can now use the getCredentials() function above to build an authorized client, list all labels, and find the required one identified by labelName.

// Build a new authorized API client service.
val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
val service = Gmail.Builder(httpTransport, JSON_FACTORY, getCredentials(httpTransport))
                .setApplicationName(APPLICATION_NAME)
                .build()

// Find the requested label
val user = "me"
val labelList = service.users().labels().list(user).execute()
val label = labelList.labels
        .find { it.name == labelName } ?: error("Label `$labelName` is unknown.")

Enter fullscreen mode Exit fullscreen mode

List all emails

For listing all email messages, let's use a few of Kotlin's goodies - tailrec extension function with lambda as the last parameter.

We need to invoke the list request repeatedly until the nextPageToken is null, and doing so with tailrec is safer.

For each message, we invoke the process lambda to perform an actual operation.

private tailrec fun Gmail.processMessages(
    user: String,
    label: Label,
    nextPageToken: String? = null,
    process: (Message) -> Unit
) {

    val messages = users().messages().list(user).apply {
        labelIds = listOf(label.id)
        pageToken = nextPageToken
        includeSpamTrash = true
    }.execute()

    messages.messages.forEach { message ->
        process(message)
    }

    if (messages.nextPageToken != null) {
        processMessages(user, label, messages.nextPageToken, process)
    }

}
Enter fullscreen mode Exit fullscreen mode

Process message

The code for listing emails above returns only id and threadId for each of the messages, so we need to fetch message details, extract From header, and eventually process it.

To speed up the process, let's use Kotlin's coroutines to perform the message fetching in parallel. First, introduce a custom dispatcher, so we can limit the number of threads.

private val MAX_FETCH_THREADS = Runtime.getRuntime().availableProcessors()

val executors = Executors.newFixedThreadPool(MAX_FETCH_THREADS)

val dispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executors.execute(block)
    }
}
Enter fullscreen mode Exit fullscreen mode

For extracting the email address from the Name <email@address.com> format, the code is simple:

private fun String.parseAddress(): String {
    return if (contains("<")) {
        substringAfter("<").substringBefore(">")
    } else {
        this
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can put things together. Of course, you should introduce some logic for catching exceptions, etc.

private fun Gmail.processFroms(
        user: String,
        label: Label,
        process: (String) -> Unit
) {
    runBlocking(dispatcher) {
        processMessages(user, label) { m ->
            launch {
                val message = users().messages().get(user, m.id).apply { format = "METADATA" }.execute()
                message.payload.headers.find { it.name == "From" }?.let { from ->
                    process(from.value.parseAddress())
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Result

With all the code above, we can unique list of all senders like this:

val senders = mutableSetOf<String>()
service.processFroms(user, label) {
    senders += it
}

senders.forEach(::println)
Enter fullscreen mode Exit fullscreen mode

Source code

The complete source code is available on Github.


Originally published on Localazy - the best developer-friendly solution for localization of web, desktop and mobile apps.

Top comments (1)

Collapse
 
jaredrummler profile image
jaredrummler

Nice little project. Thank you for sharing.