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
andStoredCredential
(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, aStoredCredential
will be generated. -
Step 3. We can move that
StoredCredential
to the resources directory, then we can writetokenPath
(which is the directory that contains theStoredCredential
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
toclasspath: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}
-