0%
August 4, 2024

Send Gmail in Kotlin

kotlin

springboot

Dependencies

dependencies {
    implementation("com.google.api-client:google-api-client:2.0.0")
    implementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1")
    implementation("com.google.apis:google-api-services-gmail:v1-rev20220404-2.0.0")
    implementation("javax.mail:mail:1.4.7")
}

OAuth Credential and Code Implementation

  • In the past we have studied Gmail API (not just sending email) from this post.

    • This time we assume that we have all got the credential.json in the same way, make sure that in the OAuth Consent Screen session we choose the scope correctly:

      • Filter the scope:

      • Choose the privilege we want:

    • And make sure that when we create a credential we choose Desktop app (then we don't need to provide the return_url which is not needed in backend application)

      • The credential.json must be obtained from the account that you want to send email. A company boss account has no permission to send email on behalf of his/her employee.

        If we plan to send email using 3 accounts (for different purpose), make sure to prepare 3 set of credential.json and StoredCredential (to be generated by the sdk once we try to send any email).

  • Let's translate the code in Gmail API Documentation into kotlin, then we get the following:

    1import com.google.api.client.auth.oauth2.Credential
    2import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp
    3import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver
    4import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
    5import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
    6import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
    7import com.google.api.client.googleapis.json.GoogleJsonResponseException
    8import com.google.api.client.http.javanet.NetHttpTransport
    9import com.google.api.client.json.gson.GsonFactory
    10import com.google.api.client.util.store.FileDataStoreFactory
    11import com.google.api.services.gmail.Gmail
    12import com.google.api.services.gmail.GmailScopes
    13import com.google.api.services.gmail.model.Message
    14import org.apache.commons.codec.binary.Base64
    15import org.springframework.beans.factory.annotation.Value
    16
    17import org.springframework.stereotype.Service
    18import java.io.*
    19import java.nio.file.Paths
    20import java.util.*
    21import javax.mail.Session
    22import javax.mail.internet.InternetAddress
    23import javax.mail.internet.MimeMessage
    24
    25
    26@Service
    27class GmailService(
    28    @Value("\${gmail.sender}") private val sender: String,
    29    @Value("\${gmail.credential-path}") private val credentialPath: String
    30) {
    31
    32
    33    private val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
    34    private val jsonFactory = GsonFactory.getDefaultInstance()
    35
    36    private val tokenPath = "tokens"
    37
    38    init {
    39        println("Sender: $sender")
    40        println("credentialPath: $credentialPath")
    41    }
    42
    43    @Throws(IOException::class)
    44    private fun getCredentials(httpTransport: NetHttpTransport, jsonFactory: GsonFactory): Credential {
    45        // Load client secrets.
    46        val inputStream: InputStream = GmailService::class.java.getResourceAsStream(credentialPath)
    47            ?: throw FileNotFoundException("Resource not found: $credentialPath")
    48        val clientSecrets =
    49            GoogleClientSecrets.load(jsonFactory, InputStreamReader(inputStream))
    50
    51        // Build flow and trigger user authorization request.
    52        val flow = GoogleAuthorizationCodeFlow.Builder(
    53            httpTransport, jsonFactory, clientSecrets, setOf(GmailScopes.GMAIL_SEND)
    54        )
    55            .setDataStoreFactory(FileDataStoreFactory(Paths.get(tokenPath).toFile()))
    56            // .setDataStoreFactory(FileDataStoreFactory(File(System.getProperty("user.dir"))))
    • Note that a request to login and get StoredCredential can only be applied when we actually use it (like sending an email).

    • To store this StoredCredential we must specify a writable directory (which cannot be resources folder).

    • Step 1. Therefore we choose user.dir which points to the current project root directory (where we execute the program), and execute the program to send email.

    • Step 2. A link will be shown in the terminal to ask for logging-in in a browser.

      On successful login, and on credential.json matched, a StoredCredential will be generated.

    • Step 3. We can move that StoredCredential to the resources directory, then we can write tokenPath (which is the directory that contains the StoredCredential token) in place of "user.dir".

    • Example. When we have 3 emails in an application and if we want to use noreply:

      then we set tokenPath to classpath:email/noreply_billieonsite.

    57          .setAccessType("offline")
    58          .build()
    59      val receiver = LocalServerReceiver.Builder().setPort(8888).build()
    60      val credential: Credential = AuthorizationCodeInstalledApp(flow, receiver).authorize("user")
    61      // returns an authorized Credential object.
    62      return credential
    63  }
    64
    65  fun sendEmail(subject: String, bodyText: String, toEmail: String): Message? {
    66      val service = Gmail.Builder(httpTransport, jsonFactory, getCredentials(httpTransport, jsonFactory))
    67          .setApplicationName("payment")
    68          .build()
    69
    70      val props = Properties()
    71      val session: Session = Session.getDefaultInstance(props, null)
    72      val email: MimeMessage = MimeMessage(session)
    73      email.setFrom(InternetAddress(sender))
    74      email.addRecipient(
    75          javax.mail.Message.RecipientType.TO,
    76          InternetAddress(toEmail)
    77      )
    78      email.subject = subject
    79      email.setText(bodyText)
    80
    81      val buffer = ByteArrayOutputStream()
    82      email.writeTo(buffer)
    83      val rawMessageBytes = buffer.toByteArray()
    84      val encodedEmail = Base64.encodeBase64URLSafeString(rawMessageBytes)
    85      var message = Message()
    86      message.setRaw(encodedEmail)
    87
    88      try {
    89          // Create send message
    90          message = service.users().messages().send("me", message).execute()
    91          println("Message id: " + message.id)
    92          println(message.toPrettyString())
    93          return message
    94      } catch (e: GoogleJsonResponseException) {
    95          val error = e.details
    96          if (error.code == 403) {
    97              println("Unable to create draft: " + e.details)
    98              throw Exception("403 not found")
    99          } else {
    100              throw e
    101          }
    102      }
    103  }
    104}