Grouping email threads with Java and Nylas

Grouping email threads with Java and Nylas

10 min read
Tags:

We can use our email client to search through email threads, however, they will come with a lot of extra noise (email responses, long signatures and so on). Gladly, we can use Nylas and Java to build an application to focus on what’s important in our email threads. Getting only the conversations of each thread.

Is your system ready to group email threads?

If you already have the Nylas Java SDK installed and your Java environment is configured, then continue along with the blog.

Otherwise, I recommend reading the post How to Send Emails with the Nylas Java SDK where the basic setup is explained.

What are we going to talk about?

What does our application look like?

Before we jump into the code, let’s see how our application actually works. We will have a single input field accepting an email address to get all the related email threads and messages included in those threads:

Email Threads application

We will list all email threads related to the address we used, as long as they have at least two messages.

The email threads are presented in an accordion, and when we open one, we will get the emails in a sentence with the contact image and with the noise removed. No emails, phone numbers or reply texts.

Email Threads displayed using Java

As we can see, they are both simple and nice.

Creating the project

Our project is going to be called EmailThreading, and it will have a main class called EmailThreading. We need to change the default pom.xml file and create a Handlebars template called main.

We want to use a web framework, and a very light and fast option is SparkJava.

Creating the pom.xml

We’re going to include some useful libraries:

  • spark-core → The Spark Web Framework
  • nylas-java-sdk → The Nylas Java SDK
  • dotenv-java → To enable using .env files
  • spark-template-handlebars → To allow handlebars on Spark projects
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>EmailThreading</groupId>
   <artifactId>EmailThreading</artifactId>
   <version>1.0-SNAPSHOT</version>

   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <maven.compiler.source>17</maven.compiler.source>
       <maven.compiler.target>17</maven.compiler.target>
   </properties>

   <dependencies>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-simple</artifactId>
           <version>1.7.25</version>
       </dependency>
       <dependency>
           <groupId>com.sparkjava</groupId>
           <artifactId>spark-core</artifactId>
           <version>2.9.4</version>
       </dependency>
       <dependency>
           <groupId>com.sparkjava</groupId>
           <artifactId>spark-template-mustache</artifactId>
           <version>2.7.1</version>
       </dependency>
       <dependency>
           <groupId>io.github.cdimascio</groupId>
           <artifactId>dotenv-java</artifactId>
           <version>2.2.4</version>
       </dependency>
       <dependency>
           <groupId>com.nylas.sdk</groupId>
           <artifactId>nylas-java-sdk</artifactId>
           <version>1.18.0</version>
       </dependency>
       <dependency>
           <!-- jsoup HTML parser library @ https://jsoup.org/ -->
           <groupId>org.jsoup</groupId>
           <artifactId>jsoup</artifactId>
           <version>1.15.3</version>
       </dependency>
       <dependency>
           <groupId>com.sparkjava</groupId>
           <artifactId>spark-template-handlebars</artifactId>
           <version>2.7.1</version>
       </dependency>
   </dependencies>

</project>

Creating the main class

Here’s the source code of our EmailThreading.java class:

// Import Java Utilities
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import okhttp3.ResponseBody;
import java.nio.file.Paths;
import java.nio.file.Files;

// Import Spark and Handlebars libraries
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import spark.ModelAndView;
import static spark.Spark.*;
import spark.template.handlebars.HandlebarsTemplateEngine;

//Import Nylas Packages
import com.nylas.*;
import com.nylas.Thread;

//Import DotEnv to handle .env files
import io.github.cdimascio.dotenv.Dotenv;

public class EmailThreading {

   static RemoteCollection<Contact> get_contact(Contacts _contact, String email) throws RequestFailedException, IOException {
       RemoteCollection<Contact> contact_list = _contact.list(new ContactQuery().email(email));
       return contact_list;
   }

   static void download_contact_picture(NylasAccount account, String id) throws RequestFailedException, IOException{
       try (ResponseBody picResponse = account.contacts().downloadProfilePicture(id)) {
           Files.copy(picResponse.byteStream(), 
                      Paths.get("src/main/resources/public/images/" + id +".png"));
       }catch (Exception e){
           System.out.println("Image was already downloaded");
       }
   }

   public static void main(String[] args) {
       staticFiles.location("/public");
       // Load the .env file
       Dotenv dotenv = Dotenv.load();
       // Create the client object
       NylasClient client = new NylasClient();
       // Connect it to Nylas using the Access Token from the .env file
       NylasAccount account = client.account(dotenv.get("ACCESS_TOKEN"));
       // Get access to messages
       Messages messages = account.messages();
       // Get access to contacts
       Contacts contacts = account.contacts();
       // Default path when we load our web application

       // Hashmap to send parameters to our handlebars view
       Map map = new HashMap();
       map.put("search", "");

       get("/", (request, response) ->
               // Create a model to pass information to the handlebars template
               // Call the handlebars template
               new ModelAndView(map, "main.hbs"),
               new HandlebarsTemplateEngine());

       // When we submit the form, we're posting data
       post("/", (request, response) -> {
           // Get parameter from form
           String search = request.queryParams("search");
           // Search all threads related to the email address
           Threads threads = account.threads();
           List<Thread> thread = threads.list(new ThreadQuery().
                   in("inbox").from(search)).fetchAll();
           if (search.equals("")) {
               String halt_msg = "<html>\n" +
                       "<head>\n" +
                       "    <script src=\"https://cdn.tailwindcss.com\"></script>\n" +
                       "    <title>Nylas' Email Threading</title>\n" +
                       "</head>\n" +
                       "<body>\n" +
                       "<div class=\"bg-red-300 border-green-600 border-b 
                                             p-4 m-4 rounded w-2/5 grid place-items-center\">\n" +
                       "<p class=\"font-semibold\">You must specify all fields</p>\n" +
                       "</div>\n" +
                       "</body>\n" +
                       "</html>";
               halt(halt_msg);
           }

           // This ArrayList will hold all the threads with their
           // accompanying information
           ArrayList<ArrayList<ArrayList<String>>> _threads = new 
           ArrayList<ArrayList<ArrayList<String>>>();

           // Look for threads with more than 1 message
           for (Thread msg_thread : thread) {
               // Auxiliary variables
               ArrayList<ArrayList<String>> _thread = new ArrayList<ArrayList<String>>();
               ArrayList<String> _messages = new ArrayList<String>();
               ArrayList<String> aux_messages = new ArrayList<String>();
               ArrayList<String> _pictures = new ArrayList<String>();
               ArrayList<String> _names = new ArrayList<String>();

               // Only add threads with two messages or more
               if (msg_thread.getMessageIds().size() > 1) {
                   // Get the subject of the first email
                   aux_messages.add(msg_thread.getSubject());
                   _thread.add(aux_messages);
                   // Loop through all messages contained in the thread
                   for (String message_ids : msg_thread.getMessageIds()) {
                       // Get information from the message
                       Message message = messages.get(message_ids);
                       // Try to get the contact information
                       RemoteCollection<Contact> contact = get_contact(contacts,                                                                            
                                                           message.getFrom().get(0).getEmail());
                       if (contact != null && !contact.fetchAll().get(0).getId().isEmpty()) {
                           // If the contact is available, downloads its profile picture
                           download_contact_picture(account, contact.fetchAll().get(0).getId());
                       }
                       // Remove extra information from the message, like appended
                       //  message, email and phone number
                       String parsed_message = message.getBody();
                       parsed_message = parsed_message.replaceAll("\\\\n", "\n\n");
                       parsed_message = Jsoup.clean(parsed_message, Safelist.basic());
                       // Phone number
                       String pattern_key = "\\d{3}[- .]\\d{3}[- .]\\d{4}";
                       parsed_message = parsed_message.replaceAll(pattern_key, "");
                       // Email
                       pattern_key = "(?i)[A-Z0-9+_.\\-]+@[A-Z0-9.\\-]+";
                       parsed_message = parsed_message.replaceAll(pattern_key, "");
                       // Replied message history
                       pattern_key = "(?s)(\\bOn.*\\b)(?!.*\\1).+";
                       parsed_message = parsed_message.replaceAll(pattern_key, "");
                       // Twitter handler
                       pattern_key = "(?i)<span>twitter:.+";
                       parsed_message = parsed_message.replaceAll(pattern_key, "");
                       _messages.add(parsed_message);
                       // Convert date to something readable
                       LocalDateTime ldt = LocalDateTime.ofInstant(message.getDate(), 
                                                                   ZoneOffset.UTC);
                       String date = ldt.getYear() + "-" + ldt.getMonthValue() + "-" + 
                                            ldt.getDayOfMonth();
                       String time = ldt.getHour() + ":" + ldt.getMinute() + ":" + ldt.getSecond();
                       // If there's no contact
                       if (contact == null || contact.fetchAll().get(0).getId().isEmpty()) {
                           _pictures.add("NotFound.png");
                           _names.add("Not Found" + " on" + date + " at " + time);
                       } else {
                           // If there's a contact, pass picture information,
                           // name and date and time of message
                           _pictures.add(contact.fetchAll().get(0).getId() + ".png");
                           _names.add(contact.fetchAll().get(0).getGivenName() + " " +
                                   contact.fetchAll().get(0).getSurname() + " on " +
                                   date + " at " + time);
                       }

                   }
               // Add ArrayLists to main thread arraylist
               // and then add them all
               _thread.add(_messages);
               _thread.add(_pictures);
               _thread.add(_names);
               _threads.add(_thread);
               }
           }

           // Hashmap to send parameters to our handlebars view
           Map thread_map = new HashMap();
           // We're passing the same _threads ArrayList twice
           // as we need the copy for processing
           thread_map.put("threads", _threads);
           thread_map.put("inner_threads", _threads);

           // Call the handlebars template
           return new ModelAndView(thread_map, "main.hbs");
       }, new HandlebarsTemplateEngine());
   }
}

Inside the resources folder, we need to create a templates folder and inside we need to create a file called main.hsb:

<html>
<head>
    <!-- Call the TailwindCSS and Flowbite libraries -->
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/flowbite.min.css" />
    <title>Nylas' Email Threading</title>
</head>
<body>
<div class="grid bg-green-300 border-green-600 border-b p-4 m-4 rounded place-items-center">
    <p class="text-6xl text-center">Email Threading</p>
    <br>
    <form method="post">
        <div class="flex bg-blue-300 border-blue-600 border-b p-4 m-4 rounded place-items-center">
            <input type="text" name="search" value="" size="50"></input>&nbsp;&nbsp;
            <button type="submit" class="block bg-blue-500 hover:bg-blue-700 text-white text-lg mx-auto py-2 px-4 rounded-full">Search</button>
        </div>
    </form>
    <div id="accordion-collapse" data-accordion="collapse">
        <!-- Loop through each thread -->
        {{#each threads}}
        <h2 id="accordion-collapse-heading-{{@index}}">
            <button type="button" class="flex items-center justify-between w-full p-5 font-medium
                    text-left text-gray-500 border border-b-0 border-gray-200
                    focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-800
                    dark:border-gray-700 dark:text-gray-400 hover:bg-gray-100
                    dark:hover:bg-gray-800"
                    data-accordion-target="#accordion-collapse-body-{{@index}}"
                    aria-expanded="false" aria-controls="accordion-collapse-body-{{@index}}">
                <!-- Title of the thread -->
                <span>{{this.[0].[0]}}</span>
                <svg data-accordion-icon class="w-6 h-6 shrink-0" fill="currentColor"
                     viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd"
                          d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414        
                              1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd">
                    </path>
                </svg>
            </button>
        </h2>
        <div id="accordion-collapse-body-{{@index}}" class="hidden" aria-labelledby="accordion-collapse-heading-{{@index}}">
            <div class="p-5 font-light border border-b-0 border-gray-200 dark:border-gray-700">
                <div class="grid "grid-rows-{{this.[1].length}}" grid-flow-col gap-4">
                    {{#each this.[1]}}
                        <div class="col-span-2 ...">
                            {{#with (lookup ../this.[2] @index)}}
                                <img class="mx-auto" src="images/{{this}}"><b>
                            {{/with}}
                            {{#with (lookup ../this.[3] @index)}}
                                <p class="text-center">{{this}}</p></b><br>
                            {{/with}}
                            {{{this}}}
                        </div>
                    <br>
                    {{/each}}
                </div>
            </div>
        </div>
        {{/each}}
    </div>
</div>
<script src="https://unpkg.com/[email protected]/dist/flowbite.js"></script>
</body>
</html>

To store our images, on the resources folder we need to create a folder called public and inside, a folder called images.

And that’s it. We’re ready to roll.

Running our Email Grouping application

In order to run our application, we need to compile it by typing the following on the terminal window:

$ mvn package
Compile package using MVN

To actually run the application, we need to type the following:

$ mvn exec:java -Dexec.mainClass="EmailThreading"
Running our Email Threading application

Our application will be running on port 4567 of localhost, so we just need to open our favourite browser and go to the following address:

http://localhost:4567

If you want to learn about our Email APIs, please go to our documentation Email API Overview.

Related resources

How to Send Emails Using an API

Key Takeaways This post will provide a complete walkthrough for integrating an email API focused…

How to build a CRM in 3 sprints with Nylas

What is a CRM? CRM stands for Customer Relationship Management, and it’s basically a way…

How to create an appointment scheduler in your React app

Learn how to create an appointment scheduler in your React app using Nylas Scheduler. Streamline bookings and UX with step-by-step guidance.