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

Now it's time to create a reader.

As in: an email reader.

In other words: show your users individual email messages in all their HTML glory. Or plain text glory. Whatever.

And when it's all over, they'll see email messages that look like this:

 

Yes, that's one of my own emails. I ordered a suckling pig. Don't judge.

Read on for a bit and I'll show you how to display email messages so they look just like that.

Or you can go straight to the code on GitHub.

Just remember: the source code I cover here is part of the Carey Development CRM application I've been working through for some time now. 

To Date

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
  • Learned why you didn't actually need to retrieve the token
  • Learned about the structure of the Message object
  • Learned about MIME
  • Created an Email type that gets sent back to the client
  • Wrote code that converts a Message object to an Email object
  • Instantiated the Gmail service
  • Used the Gmail service to retrieve just the messages you want to retrieve
  • Used the Gmail service to extracted full Message objects from very lightweight objects
  • Added a new endpoint to listen for inbox requests
  • Used that endpoint to send back the user's most recent emails
  • Learned how to disallow your application from accessing your inbox
  • Created a new component on the Angular side that shows emails from the user's inbox
  • Created a new service on the Angular side that retrieves emails from the Spring Boot microservice
  • Optimized the payload response from the microservice
  • Displayed HTML reference characters as they should be displayed
  • Got rid of whitespace that isn't whitespace
  • Designed a user-friendly, responsive inbox

And if you haven't done those things, Part 1 is waiting for you.

A Hot Date

In this guide, you will:

  • Create a new component to display individual email messages
  • Display either the HTML or plain text versions of emails your users receive
  • Learn about the security risks associated with full HTML display of emails

And then you can go back to skeet shooting.

Service Oriented

First, add a new method to email.service.ts.

  fetchMessageById(id: string): Observable<Email> {
    let url = `${baseUrl}/email/message/${id}`;
    console.log("Fetch message URL is " + url);

    return this.http.get<Email>(url);
  }

That accomplishes the very simple task of retrieving a single email message from the downstream microservice. 

Recall that the inbox component, at this time, just retrieves lightweight versions of the first 20 email messages in the user's inbox. The lightweights include data like the subject, snippet, date sent, and a few other items.

But they don't include the body of the email messages.

So if the user wants to read an email message, he or she will have to click on it from the inbox. That action will open a new screen (i.e., a new component) that will display the email body.

But before it can display the body, it must first retrieve the body from the service. That's what the code above does.

I haven't covered the new code required in the downstream microservice. But if you've been following along, you can probably handle it yourself.

If not, feel free to take a look at GmailController and GmailService in ecosystem-email-service.

A New Component!

As promised in a previous section, you'll create a new component. So go to the root of the Angular source in your operating system or use the Terminal window in Microsoft Visual Studio to create the message component as follows:

ng g c features/user/email/message

That's going to create a few new files in your source tree. You'll need to update them to display individual email messages.

Start by editing message.component.ts. Make it look like this:

@Component({
  selector: 'app-message',
  templateUrl: './message.component.html',
  styleUrls: ['./message.component.css']
})
export class MessageComponent implements OnInit {

  email: Email;
  loading: boolean = true;

  constructor(private emailService: EmailService, private route: ActivatedRoute,
    private alertService: AlertService) { }

  ngOnInit(): void {
    this.fetchEmail();
  }

  private fetchEmail() {
    let email$ = this.route.queryParamMap.pipe(
      switchMap((params: ParamMap) =>
        this.emailService.fetchMessageById(params.get('id')))
    );

    email$.subscribe(
      (email: Email) => this.handleEmailResponse(email),
      err => this.handleError(err)
    );
  }

  private handleEmailResponse(email: Email) {
    //console.log(email);
    this.email = email;
    this.loading = false;
  }

  private handleError(err: Error) {
    console.error(err);
    this.alertService.error("Problem - please contact support!");
    this.loading = false;
  }
}

That's a pattern you've seen countless times if you've followed along with this series.

The email property stores the email once it's retrieved from the service.

The loading property is a boolean that indicates whether the application is in the process of retrieving the message (true) or not (false). The UI will display a "loading" spinner as long as loading is set to true.

The fetchEmail() message starts by grabbing the id parameter from the URL. That parameter represents the ID of the email message to retrieve.

Then, the method invodes the aforementioned fetchMessageById() method from EmailService to get an Observable containing the full email message.

Finally, that method subscribes to the Observable and hands off execution to handleEmailResponse(). Unless there's an error, in which case execution goes to handleError().

And that's pretty much the extent of of the class. At this time, anyway.

Just make sure you add the new component and it's associated route to UserModule.

export const routes = [
...
  {
    path: 'email/message',
    component: MessageComponent,
    data: {
      breadcrumb: 'Read Email'
    }
  },
...

Mucking About With Markup

Next, edit message.component.html:

<div fxFlex fxLayout="column" fxLayoutGap="0px">
  <div style="margin-bottom:20px">
    <alert></alert>
  </div>
  <div class="absolute-center" *ngIf="loading">
    <mat-spinner [diameter]="80"></mat-spinner>
  </div>
  <div *ngIf="!loading">
    <mat-card>
      <mat-card-title>{{email.subject}}</mat-card-title>
      <mat-card-subtitle>{{email.from}}</mat-card-subtitle>
      <mat-card-content *ngIf="email.html" [innerHTML]="email.html | noSanitize"></mat-card-content>
      <mat-card-content *ngIf="!email.html" [innerText]="email.plainText"></mat-card-content>
    </mat-card>
  </div>
</div>

Once again, that HTML follows a pattern.

You'll see towards the top that the code sets apart space for the alert box. I've covered how to implement alert messages in a previous guide.

That <mat-spinner> element only appears if the loading boolean is set to true. You can see that in the *ngIf directive in the enclosing <div> element.

I covered using a spinner element in a previous guide as well.

The HTML above uses an Angular Material card to display the email message.

And here's the really cool news: the code uses the default design specs to print out the subject, from attribute, and body. No custom CSS needed (yet).

I think most of that is self-explanatory based on the element names. But let me take a moment to explain why there are two (2) <mat-card-content> elements.

Recall from previous guides that the Gmail API will give you both the plain text and HTML versions of email messages (if both are available). 

The first <mat-card-content> displays the HTML version of the body if it exists. That *ngIf directive checks for its existence.

The second <mat-card-content> displays the plain text body if the HTML version doesn't exist.

I've explained the [innerHTML] and [innerText] directives in previous guides.

But what about that noSanitize thing? What's that all about?

The answer to that comes with a warning.

And Now a Word of Caution

Part of the solution I've implemented in the source code I checked in to GitHub, but have not explained here, displays the entire HTML message body retrieved from the Gmail API.

It does that with the aid of a custom Pipe. Take a look at that noSanitize you see in the first <mat-card-content> element in the code above.

That comes from a custom Pipe I created.

But why did I need to create the custom PIpe?

Because Angular by default strips out elements from HTML that could be used to create cross-site scripting (XSS) threats.

That means you must consult a data security professional before implementing the method I use to display Gmail HTML message bodies.

Let me repeat that: get in touch with a data security professional before you use the custom Pipe to display full HTML messages sent back from Gmail.

It's not worth the risk.

Anyhoo, if you want to learn how to display the whole HTML message once your data security consultant gives you the green light, feel free to read my guide on using a custom pipe to get around Angular's sanitizing process.

Pick & Click

Finally, you've got to add some code so users can click on email messages from the inbox. When they click, they'll go to the new screen that displays the individual message.

So go back to inbox.component.ts. Add this method:

  viewMessage(id: string) {
    let route = '/user/email/message';
    this.router.navigate([route], { queryParams: { id: id } });
  }

That's going to take in a message ID as a parameter. Then, it will route the user to the component you just created (via /user/email/message).

But that queryParams object is significant. It adds an id request parameter to the URL. The value of that id parameter is unsurprisingly set to the value of the id string passed in to the method.

The message component, as you may recall, will read that id parameter and use that to retrieve the Gmail message by ID.

One more thing: for the code above, make sure you add Router to your constructor. 

After you're done with the class, update inbox.component.html as follows:

        <ng-container matColumnDef="from">
          <tr><th mat-header-cell *matHeaderCellDef mat-sort-header> From </th></tr>
          <tr><td mat-cell *matCellDef="let row" (click)="viewMessage(row.id)" class="from-cell"> {{emailService.getDisplayableFrom(row.from)}} </td><tr>
        </ng-container>

        <ng-container matColumnDef="subject">
          <tr><th mat-header-cell *matHeaderCellDef mat-sort-header> Subject </th></tr>
          <tr><td mat-cell *matCellDef="let row" (click)="viewMessage(row.id)" class="subject-cell" [innerHTML]="getSubjectSnippetDisplay(row)"></td></tr>
        </ng-container>

        <ng-container matColumnDef="date">
          <tr><th mat-header-cell *matHeaderCellDef mat-sort-header> Date </th></tr>
          <tr><td mat-cell *matCellDef="let row" (click)="viewMessage(row.id)" class="date-cell"> {{dateService.getCustomDateDisplay(row.date, 'MMM d')}} </td></tr>
        </ng-container>

Note the (click) event handler in all three of those blocks. When the user clicks on any one of those cells (from, subject, or date), the application will route the user to the message component. That's where it will display the email message.

Now you might be wondering: why not make the whole row clickable instead of individual cells?

Answer: because the first row is a checkbox. You want users to be able to click it without navigating somewhere else.

And now add some CSS so the user sees a pointer when hovering over a row in the table:

.email-row:hover {
  cursor: pointer;
}

Wrapping It Up

That's it. That's all you need to do to display individual Gmail messages.

Now it's up to you talk to a data security consultant and apply what you've learned here to your own applications.

Have fun!

Photo by Thought Catalog from Pexels