DEV Community

Cover image for Grouping Email Threads with Ruby and Nylas
Alvaro (Blag) Tejada Galindo for Nylas

Posted on • Originally published at nylas.com

Grouping Email Threads with Ruby and Nylas

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

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

Email Threading Header

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

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

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, 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:

# 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
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

We’re calling both the TailwindCSS and Flowbite libraries to handle CSS.

We need to create then the file main.erb:

<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>
Enter fullscreen mode Exit fullscreen mode

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

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 as well Threads and Messages.

Top comments (0)