Grouping Email Threads with Ruby and Nylas
Tags: ruby
Published: 2022-12-09 11:56:52

Looking for emails is straightforward, however, trying to get them all in a single place with less noise can be a difficult task. Thanks to the Nylas Ruby API, we can make email threading possible.

If you want to learn more about Email Threads, read the blog post [Stay Threaded: How to Manage and Control Email Threads](https://www.nylas.com/blog/how-to-make-email-threading-less-chaotic/)

## Is your system ready to group email threads?

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

Otherwise, I would recommend that you read the post [How to Send Emails with the Nylas Ruby SDK](https://www.nylas.com/blog/how-to-send-emails-with-the-nylas-ruby-sdk/) where the basic setup is explained.

## What are we going to talk about?

- [What does our application look like?](#what-are-we-going-to-talk-about)
- [Installing the Sinatra package](#installing-the-sinatra-package)
- [Running our Email Threading application](#running-our-email-threading-application)

## 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 the email address to get all the related email threads and messages included in those threads:

![Email Threading Header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vbgk9ub0y0u0fna12w5y.png)

We’re going to list all email threads related to the address we used, as long as they have at least two messages:

![Email Threading Threads](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wl7mgt2n6cp1uuegdgy4.png)

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

![Email Threading Conversation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ca49xrpfh1fjlq7z2q0k.png)

As we can see, both simple and nice.

## Installing the Sinatra package

To create a Ruby web application, our best option is to use [Sinatra](http://sinatrarb.com), one of the most popular Micro Frameworks in the Ruby world. We might need to install some additional gems:

`$ gem install sinatra
$ gem install puma
$ gem install nokogiri`

Once installed, we’re ready to go:

First, we’re going to create a folder called EmailThreading, and inside we’re going to create two folders, one called views and other called public.

Let’s create a file called **EmailThreading.rb** in the **EmailThreading** folder, and add the following code:

```ruby
# Import your dependencies
require 'dotenv/load'
require 'nylas'
require 'sinatra'
require 'nokogiri'
require 'date'

# Initialize your Nylas API client
nylas = Nylas::API.new(
    app_id: ENV["CLIENT_ID"],
    app_secret: ENV["CLIENT_SECRET"],
    access_token: ENV["ACCESS_TOKEN"]
)

# Use the Nokogiri gem to clean up the email response
def clean_content(raw_html)
	html = raw_html.encode('UTF-8', invalid: :replace, undef: :replace, 
                         replace: '', universal_newline: true).gsub(/\P{ASCII}/, 
                         '')
	parser = Nokogiri::HTML(html, nil, Encoding::UTF_8.to_s)
	parser.xpath('//script')&.remove
	parser.xpath('//style')&.remove
	parser.xpath('//text()').map(&:text).join('<br> ')
end

# Get the contact associated to the email address
def get_contact(nylas, email)
	contact =  nylas.contacts.where(email: email)
	if contact[0] != nil
		return contact[0]
	end
end

# Download the contact picture if it's not stored already
def download_contact_picture(nylas, id)
    if id != nil
        contact = nylas.contacts.find(id)
        picture = contact.picture
        file_name = id + ".png"
        File.open("public/" + file_name,"wb") do |f|
            f.write File.open(picture, 'rb') {|file| file.read }
        end
    end
end

# When calling the application for the first time
get '/' do
    _threads = []
    # Call the page
    erb :main, :layout => :layout, :locals => {:threads => _threads}
end

# When asking for the email threading
post '/search' do
    # Get parameter from form
    search = params[:search]
    # Search all threads related to the email address	
    threads = nylas.threads.where(from: search,in: 'inbox')	
	
    _threads = []

    # Loop through all the threads	
    threads.each{ |thread|
        _thread = []
        _messages = []
        _pictures = []
        _names = []
        # Look for threads with more than 1 message		
        if thread.message_ids.length() > 1
            # Get the subject of the first email		
            _thread.push(thread.subject)
            # Loop through all messages contained in the thread
            thread.message_ids.each{ |message|
                # Get information from the message			
                message = nylas.messages.find(message)
                # Try to get the contact information				
                contact = get_contact(nylas, message.from[0].email)
                if contact != nil and contact != ""
                    # If the contact is available, downloads its profile picture
                    download_contact_picture(nylas, contact.id)
                end
                # Remove extra information from the message, like appended 
                #  message, email and phone number
                _messages.push(clean_content(message.body).
                    gsub(/(\bOn.*\b)(?!.*\1)/,"").
                    gsub(/[a-z0-9._-]+@[a-z0-9._-]+\.[a-z]{2,3}\b/i,"").
                    gsub(/(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/,"").
                    gsub(/twitter:.+/i,""))
                    # Convert date to something readable
                    datetime = Time.at(message.date).to_datetime
                    date = datetime.to_s.scan(/\d{4}-\d{2}-\d{2}/)
                    time = datetime.to_s.scan(/\d{2}:\d{2}:\d{2}/)
                    if contact == nil or contact == ""
                        _pictures.push("NotFound.png")
                        _names.push("Not Found" + " on " + date[0] + " at " + 
                                    time[0])
                    else
                        # If there's a contact, pass picture information, 
                        # name and date and time of message
                        _pictures.push(contact.id + ".png")
                        _names.push(contact.given_name + " " + contact.surname + 
                                    " on " + date[0] + " at " + time[0])
                    end
            }
            _thread.push(_messages)
            _thread.push(_pictures)
            _thread.push(_names)
            _threads.push(_thread)
        end
    }
	
    # Call the page and display threads	
    erb :main, :layout => :layout, :locals => {:threads => _threads}
end
```

Inside the **views** folder, we need to create two different files, let’s start with **layout.erb**:

```html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Call the TailwindCSS and Flowbite libraries -->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/flowbite@1.5.3/dist/flowbite.min.css" />
<title>Nylas’ Email Threading</title>
<body>
<%= yield %>
<script src="https://unpkg.com/flowbite@1.5.3/dist/flowbite.js"></script>
</body>
</html>
```

We’re calling both the [TailwindCSS](https://tailwindcss.com) and [Flowbite](https://flowbite.com) libraries to handle CSS.

We need to create then the file **main.erb**:

```html
<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>
<!-- Create the form-->
<form method = "post" action="search">
<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>
<!-- Do we have any threads? -->
<% if threads != [] %>
<div id="accordion-collapse" data-accordion="collapse">
<!-- Counter to generate accordion elements -->
  <% counter = 1 %>
<!-- Loop through each thread -->  
  <% threads.each do |thread| %>
<!-- Define values for the accordion elements -->  
	<% heading = "accordion-collapse-heading-" + counter.to_s  %>
	<% body = "accordion-collapse-body-" + counter.to_s  %>
	<% _body = "#accordion-collapse-body-" + counter.to_s  %>
  <h2 id=<%= heading %> >
    <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=<%= _body %> 
                                 aria-expanded="false" aria-controls=<%= body %>>
<!-- Title of the thread -->
      <span><%= thread[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=<%= body %> class="hidden" aria-labelledby=<%= heading %>>
  <div class="p-5 font-light border border-b-0 border-gray-200 dark:border-gray-700">
  <!-- Get size of thread array -->
  <% count = thread[1].length() %>
  <!-- Define amount of elements on the grid -->	
  <% count_str = "grid-rows-" + count.to_s %>
  <div class="grid <%= count_str %> grid-flow-col gap-4">
  <!-- Counter to access array elements -->	
    <% counter = 0 %>
    <!-- Loop through each email -->		
    <% thread[1].each do |message| %>
      <div class="col-span-2 ...">
      <!-- Display image and date/time of email -->				
	<img class="mx-auto" src="<%= thread[2][counter] %>"><b>
        <p class="text-center"><%= thread[3][counter] %></p></b><br>
        <!-- Display the email message -->
	<%= message %>
      </div>
	<% counter = counter + 1 %>
        <% end %>
	</div>
	</div> 
  </div>	
  <% end %>
</div>
<% end %>
</div>
```

If you wonder about the public folder, it will only hold the contact profile picture, so there’s nothing we need to do there.

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

## Running our Email Threading application

In order to run our application, we just need to type the following on the terminal window:

`$ ruby EmailThreading.rb`

![Running the application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4pevhs6117kyqe0sir5x.png)

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 more about our Email APIs, please go to our documentation [Email API Overview](https://developer.nylas.com/docs/connectivity/email/) as well [Threads](https://developer.nylas.com/docs/api/#tag--Threads) and [Messages](https://developer.nylas.com/docs/api/#tag--Messages).