Welcome to Part 7 of this series of guides on how to integrate Gmail with Angular and Spring Boot.

As you may recall, I left you with a bit of tease in the last guide. I won't leave you hanging any longer.

Plus, I'll give you some insight about email formatting that will come in useful as you continue with this integration.

Or you can just view the code on GitHub.

The choice is yours.

At This Point in the Journey

Thus far in the series, you have:

  • Created an OAuth2 client ID and secret on Google Cloud Console
  • Enabled the Gmail API via Google Cloud Console
  • Set the necessary properties in your Spring Boot application.properties file 
  • Set up a DataStore implementation with its associated factory
  • Set up a refresh listener
  • Created a utility class that handles the Google authorization code flow
  • Set up proper Gmail consent with the Google Cloud Platform
  • Created a controller that handles a request to get the Google authorization code flow URL
  • Used Postman to get the Google authorization code URL
  • Used that URL to authorize your application to access your Gmail inbox
  • Updated the controller so it handles a token request
  • Updated the utility class so it creates the credential from a token
  • Persisted that credential
  • Used Postman to retrieve the token

And if you haven't done those things, I suggest you check out Part 1 and work forward from there.

What Lies Ahead

In this part of the series, you will:

  • Understand why you don't need the access token you just got back
  • Learn about MIME and the data structure of email messages

And then you can go back to playing Parcheesi.

You're Just Not That Into the Token

When we last parted company, you used Postman to request the access token from the email service. You got back something that looked like this:

ba49.a0AfdddddddddddddddddddddddddddddddddddddddddddddV9_syXVnkdu_Pu8c50phEbC8sSZF4JFd768uogZJ9ZJL

I told you at the time that you don't need that token.

And I'm still telling you that.

Why not? Because you wrote code that persisted the credential.

And remember: the credential includes the access token and the refresh token.

Take a look at this line from the getCredentialFromCode() method in GoogleOauthUtil:

credential = acf.createAndStoreCredential(response, id);

The code invokes the createAndStoreCredential() method from the GoogleAuthorizationCodeFlow object.

As you can tell from the name of that method, it does two things:

  • It creates the credential
  • It stores the credential

And it stores the credential in whatever persistent store you're using. For the purposes of this series (and this ecosystem), I've opted for MongoDB.

So let's take a look at what a User object, retrieved from the MongoDB instance, looks like before that createAndStoreCredential() method gets called:

{
   "_id":"ObjectId(""6014081e221e1b534a8aa432"")",
   "firstName":"Milton",
   "lastName":"Jones",
   "street1":"123 Main St.",
   "city":"Detroit",
   "state":"MI",
   "zip":"36555",
   "email":"milton@xyzmail.com",
   "phoneNumber":"474-555-1212",
   "authorityNames":[
      "CAREYDEVELOPMENT_CRM_USER"
   ],
   "username":"milton",
   "country":"United States",
   "password":"$2afffffffffffffffhVC"
}

And this is what that same document in MongoDB looks like after that method gets called:

{
   "_id":"ObjectId(""6014081e221e1b534a8aa432"")",
   "firstName":"Milton",
   "lastName":"Jones",
   "street1":"123 Main St.",
   "city":"Detroit",
   "state":"MI",
   "zip":"36555",
   "email":"milton@xyzmail.com",
   "phoneNumber":"474-555-1212",
   "authorityNames":[
      "CAREYDEVELOPMENT_CRM_USER"
   ],
   "username":"milton",
   "country":"United States",
   "password":"$2a$fffffffffffffffhVC",
   "googleApi":{
      "storedCredential":BinData(0,
      "rO0ABXNyADJjb20uZ29vZ2xlLmFgggggggggggV3X0RtcWc="")"
   }
}

Take note of the googleApi object in the updated document. The storedCredential property holds the user's credential.

So here's what happened when the code invoked the createAndStoreCredential() method:

  • The Google API created the Credential object from the TokenResponse object and the user ID
  • The Google API converted the Credential object to a StoredCredential object
  • The Google API used the DataStore object you created way back in Part 2 of this series to persist the StoredCredential object

The end result is what you see above. That storedCredential property is a serialized version of the StoredCredential object.

That's why it's stored as BinData. It's a byte array because the whole object got serialized.

And that googleApi property is reflected on the Java side as a type of GoogleApi

That's a plain old Java object with only one field. It's a byte array unsurprisingly called storedCredential.

Now... when the user accesses an endpoint that requires Gmail integration, the microservice will retrieve that user's OAuth2 credential from the database. Then, the application will use that credential to gain access to the user's inbox.

End of story.

So why return the token at all?

Because you might be doing things differently.

Maybe you want the user to have access for only an hour. You don't want to do any refreshing.

In that case, the user will just use that token until it expires.

It's up to you. Feel free to use it (or not use it) as you see fit.

You might also want to return the refresh token and do the refreshing manually.

Again, it's up to you.

It's MIME Time

Now that you've got the OAuth2 credential thing straightened out, it's time to access the user's inbox. But before you can do that, you need to know what you're getting into.

When you access the user's inbox, you're going to (eventually) get a List of Message objects.

You need to know what those Message objects look like so you can parse them properly.

For starters: understand that the whole point of the Message class is that it specifies how to interpret the JSON object that gets sent back and forth when you're working with the Gmail API.

So the good news is that everything is in JSON.

The bad news is that the JSON objects can get a bit complicated at times.

Let's take a look at a sample message:

{
   "historyId":15767,
   "id":"1787ed38",
   "internalDate":1616928398000,
   "labelIds":[
      "UNREAD",
      "CATEGORY_UPDATES",
      "INBOX"
   ],
   "payload":{
      "body":{
         "data":"SSBzbyBkbyBkaWcgb24gbXkgc3RhY2pdGhvdXQgd3JpdHRlbiBwZXJtaXNzaW9uIGZyb20gU2V0dGxlLCBMTEMuDQoNCg0KUE8gQm94IDEwNTYNCkdvbGQgQmVhY2ggT3JlZ29uIDk3NDQ0DQpVU0ENCg0KVG8gdW5zdWJzY3JpYmUgb3IgY2hhbmdlIHN1YnNjcmliZXIgb3B0aW9ucywgdmlzaXQ6DQpodHRwczovL3d3dy5hd2ViZXIuY29tL3ovci8_VEF3c3JDd2N0S3pzREd5c0hKd010RWEwTEp3TTdCd3M3Qnc9DQoNCg==",
         "size":2671
      },
      "filename":"",
      "headers":[
         {
            "name":"Delivered-To",
            "value":"gmail-address@gmail.com"
         },
         {
            "name":"Received",
            "value":"by 2002:a0c:85a2:0:0:0:0:0 with SMTP id o31csp4009336qva;        Sun, 28 Mar 2021 03:47:12 -0700 (PDT)"
         },
         {
            "name":"X-Google-Smtp-Source",
            "value":"ABdhPJyuf0HgoHIC5C8s0"
         },
         {
            "name":"X-Received",
            "value":"by 2002:ac8:431e:: with SMTP id z30mr18433830qtm.216.1616928432426;        Sun, 28 Mar 2021 03:47:12 -0700 (PDT)"
         },
         {
            "name":"ARC-Seal",
            "value":"i=1; a=rsa-shaqRsTU4Yws7eFWKNvXK/EqfIgh3vuEL3vk         GyWw=="
         },
         {
            "name":"ARC-Message-Signature",
            "value":"i=1; a=rsa-sha2        LUqg=="
         },
         {
            "name":"ARC-Authentication-Results",
            "value":"i=1; mx.google.com;     from=.com"
         },
         {
            "name":"Return-Path",
            "value":"<TAwsrBw=@smtp-coi-g07-019.aweber.com>"
         },
         {
            "name":"Received",
            "value":"from smtp-coi-g07-Sun, 28 Mar 2021 03:47:12 -0700 (PDT)"
         },
         {
            "name":"Received-SPF",
            "value":"pass (google.com: domain of tawsresignates 204.194.223.19 as permitted sender) client-ip=x.x.x.x;"
         },
         {
            "name":"Authentication-Results",
            "value":"mx.google.com;       dkim=pass header.i=.com"
         },
         {
            "name":"DKIM-Signature",
            "value":"v=1; a=rsa-sha256; c=relaxeXjpaZGqFQvEuRtediosLHwbkgz7VJ\t GvDGAe8ueX5iQ=="
         },
         {
            "name":"Date",
            "value":"Sun, 28 Mar 2021 10:46:38 +0000"
         },
         {
            "name":"Message-ID",
            "value":"<13.BD.E50606@momentum-coi-mta2.prod.aweberint.com>"
         },
         {
            "name":"Content-Transfer-Encoding",
            "value":"quoted-printable"
         },
         {
            "name":"MIME-Version",
            "value":"1.0"
         },
         {
            "name":"Content-Type",
            "value":"text/plain; charset=\"utf-8\""
         },
         {
            "name":"From",
            "value":"Joe Blow <joe@myhost.com>"
         },
         {
            "name":"X-AWMessage",
            "value":"32a335c9425"
         },
         {
            "name":"To",
            "value":"gmail-address@gmail.com"
         },
         {
            "name":"X_Id",
            "value":"204548:03-07-2021-10-39-36"
         },
         {
            "name":"Require-Recipient-Valid-Since",
            "value":"gmail-address@gmail.com; Sun, 31 Jan 2016 15:10:34 +0000"
         },
         {
            "name":"Feedback-ID",
            "value":"A2129-36:AWeber"
         },
         {
            "name":"X-Subscription",
            "value":"Subscribed on 01/31/2016, via Webform, by x.x.x.x, from http://p3/"
         },
         {
            "name":"Subject",
            "value":"NO BS advice straight from the king of NO BS"
         },
         {
            "name":"List-Unsubscribe",
            "value":"<https://www.aweber.com/z/r/?TAwsrCwcBws7Bw=>"
         },
         {
            "name":"List-Unsubscribe-Post",
            "value":"List-Unsubscribe=One-Click"
         },
         {
            "name":"X-Mailer",
            "value":"AWeber Composer 2.48.2"
         },
         {
            "name":"Sender",
            "value":"Joe Blow <>"
         }
      ],
      "mimeType":"text/plain",
      "partId":""
   },
   "sizeEstimate":7628,
   "snippet":"This is my snippet",
   "threadId":"1787d38"
}

Okay the first thing to note is I did quite a bit of surgery on that JSON object to protect names and IP addresses.

But it's more than enough to give you an idea of what you'll be dealing with.

There's another important point to keep in mind as you look at that object: it reflects an email sent in plain text.

That might not seem like an important point, but it is. You'll parse plain text emails differently than you'll parse HTML emails.

And how do I know that the email above is in plain text? Take a look at the mimeType property towards the end. That's how I know.

Another thing to note: the snippet is in plain text but the body is Base64 encoded.

And while the snippet is just off the root of the JSON object, the body is buried deeply in the data property of the body object that belongs to the payload object.

That data property, by the way, holds a MIME message.

If you're not familiar with MIME, it stands for Multipurpose Internet Mail Extensions. It's the standard that extends email message formatting so they can support attachments.

I won't get into attachments this early in the series. But it's important to understand MIME if you want to properly handle email messages.

That's because sometimes you'll process emails that include both plain text and HTML versions of the same message.

Depending on your business requirements, you might opt for showing the user just the plain text version of the email.

In other situations, you might need to show the user a plain text email if that's all that exists but show the user an HTML version if that one exists as well.

Speaking of that, let's take a look at a Message object that includes both plain text and HTML versions of the body:

{
   "historyId":1591,
   "id":"17803",
   "internalDate":1616886150000,
   "labelIds":[
      "UNREAD",
      "CATEGORY_UPDATES",
      "INBOX"
   ],
   "payload":{
      "body":{
         "size":0
      },
      "filename":"",
      "headers":[
         {
            "name":"Delivered-To",
            "value":"gmail-address@gmail.com"
         },
         {
            "name":"Received",
            "value":"by 2002:       Sat, 27 Mar 2021 16:05:49 -0700 (PDT)"
         },
         {
            "name":"X-Google-Smtp-Source",
            "value":"ABdhPJy7vuibIVw"
         },
         {
            "name":"X-Received",
            "value":"by 2002:6349221;        Sat, 27 Mar 2021 16:05:49 -0700 (PDT)"
         },
         {
            "name":"ARC-Seal",
            "value":"i=1; a=r92         6cKQ=="
         },
         {
            "name":"ARC-Message-Signature",
            "value":"i=1; a=rFh         RKdQ=="
         },
         {
            "name":"ARC-Authentication-Results",
            "value":"i=1; mx..rnchq.com"
         },
         {
            "name":"Return-Path",
            "value":"<bounce-16_HTML-607169-31039@bounce.m>"
         },
         {
            "name":"Received",
            "value":"from ef193.mta.exac;        Sat, 27 Mar 2021 16:05:49 -0700 (PDT)"
         },
         {
            "name":"Received-SPF",
            "value":"pass (google.com: d.32.193;"
         },
         {
            "name":"Authentication-Results",
            "value":"mx.google.com;       q.com"
         },
         {
            "name":"DKIM-Signature",
            "value":"v=1; a=rsYLWzexHq8="
         },
         {
            "name":"Received",
            "value":"by ef19q.com>)"
         },
         {
            "name":"From",
            "value":"Joe Blow <contact@com>"
         },
         {
            "name":"To",
            "value":"<gmail-address@gmail.com>"
         },
         {
            "name":"Subject",
            "value":"Will you be there?"
         },
         {
            "name":"Date",
            "value":"Sat, 27 Mar 2021 17:02:30 -0600"
         },
         {
            "name":"List-Unsubscribe",
            "value":"<mailto:leave-f.com>"
         },
         {
            "name":"MIME-Version",
            "value":"1.0"
         },
         {
            "name":"Reply-To",
            "value":"Joe Blow <>"
         },
         {
            "name":"List-ID",
            "value":"<109local>"
         },
         {
            "name":"X-CSA-Complaints",
            "value":"c"
         },
         {
            "name":"X-SFMC-Stack",
            "value":"1"
         },
         {
            "name":"x-job",
            "value":"109185"
         },
         {
            "name":"Message-ID",
            "value":"<ae2xt.local>"
         },
         {
            "name":"Feedback-ID",
            "value":"1096ktgcld"
         },
         {
            "name":"Content-Type",
            "value":"multipart/alternative; boundary=\"9YW2N=_?:\""
         }
      ],
      "mimeType":"multipart/alternative",
      "partId":"",
      "parts":[
         {
            "body":{
               "data":"DQogIA0KIA0KDQogDQogDQogJnp3bmo7ICZEwZjk1OTIyZTg1Mjc4NzcwZWYwOTg4MWQwYjZlMWQ5NDNiZWUzMWQgDQpVcGRhdGUgUHJvZmlsZSAgDQoNCg0KIA0KIA0KDQoNCiAgDQogDQoNCg==",
               "size":10042
            },
            "filename":"",
            "headers":[
               {
                  "name":"Content-Type",
                  "value":"text/plain; charset=\"utf-8\""
               },
               {
                  "name":"Content-Transfer-Encoding",
                  "value":"quoted-printable"
               }
            ],
            "mimeType":"text/plain",
            "partId":"0"
         },
         {
            "body":{
               "data":"PCFET0NUWVBFIEhUTUwgUFVCTElDICIKICA8L2JvZHk-DQo8L2h0bWw-DQo=",
               "size":33242
            },
            "filename":"",
            "headers":[
               {
                  "name":"Content-Type",
                  "value":"text/html; charset=\"utf-8\""
               },
               {
                  "name":"Content-Transfer-Encoding",
                  "value":"quoted-printable"
               }
            ],
            "mimeType":"text/html",
            "partId":"1"
         }
      ]
   },
   "sizeEstimate":50821,
   "snippet":"‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌",
   "threadId":"17875ef1403"
}

Once again, I did quite a bit of surgery so the results aren't exact. What's important here is that you understand the formatting.

First of all, take a look at the mimeType property in the payload object. You'll see that it's set to multipart/alternative.

What does that mean? Exactly what it sounds like.

That type means that each part is just another version of the same content.

In this case, there's an HTML version of the content as well as a plain text version of the content.

And that's where it gets confusing. If you look at the parts array, you'll see that there are two parts. Each part has its own partId and its own mimeType.

So there are three mimeType properties in the JSON document. One for the whole message, and one for each message part.

Yep. That's how it works.

And with that I think we'd better call time on this guide.

Wrapping It Up

Now you understand more about how and why the user's credential got persisted. You also learned about the format of the Message object you'll need to parse.

And you know something about MIME. You're making good progress here.

Stay tuned for future guides. They're in the works.

Have fun!

Photo by Karolina Grabowska from Pexels