Grouping Email Threads With Python and Nylas

Use the Nylas Email API and Python to group email threads into a single view, and easily access complete conversations within your app.

Grouping Email Threads With Python and Nylas

Getting your emails in a single-view thread makes it easier to get the whole picture in a quick glance. Thanks to the Nylas Python APIs, we can make email threading work for you.

Is your system ready to group email threads?

If you already have the Nylas Python SDK installed and your virtual 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 Python SDK where the basic setup is clearly 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 works. We will have a single field where we can input an email address to get all the related email threads and messages included in those threads.

Email Threads application

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

Email Threads main screen

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 the noise removed. No emails, phone numbers or reply texts.

Email Threads displayed using Python

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

Installing the Flask package

As we want to create a Python web application, our best option is to use Flask, one of the most popular Micro Frameworks in the Python world. We’re going to add an additional package as well:

$ pip3 install flask
$ pip3 install beautifulsoup4

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 templates and other called static.

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

# Load your env variables
from dotenv import load_dotenv

# Import your dependencies
from flask import Flask,render_template,request,redirect,url_for
import re
import os
from nylas import APIClient
from bs4 import BeautifulSoup
import datetime
from datetime import date

# Create the app
app = Flask(__name__)

# Initialize your Nylas API client
nylas = APIClient(

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

# Download the contact picture if it's not stored already
def download_contact_picture(nylas, id):
	if id != None:
		contact = nylas.contacts.get(id)
		picture = contact.get_picture()
		file_name = "static/" +  id + ".png"
		file_ = open(file_name, 'wb')

# This the landing page
@app.route("/", methods=['GET','POST'])
def index():
# We're using a GET, display landing page
	if request.method == 'GET':
		 return render_template('main.html')
# Get parameters from form
		search = request.form["search"]
	    # Search all threads related to the email address
		threads = nylas.threads.where(from_= search, in_= 'inbox')
		_threads = []
		# Loop through all the threads
		for thread in threads:
			_thread = []
			_messages = []
			_pictures = []
			_names = []
			# Look for threads with more than 1 message
			if len(thread.message_ids) > 1:
				# Get the subject of the first email
				# Loop through all messages contained in the thread
				for message in thread["message_ids"]:
					# Get information from the message
					message = nylas.messages.get(message)
					# Try to get the contact information	
					contact = get_contact(nylas, message["from"][0]["email"])
					if contact != None and contact != "":
						# If the contact is available, downloads its profile picture
					# Remove extra information from the message like appended 
					#  message, email and phone number
					soup = BeautifulSoup(message["body"],features="html.parser")
					regex = r"(\bOn.*\b)(?!.*\1)"
					result = re.sub(regex, "", str(soup), 0, re.MULTILINE)
					regex = r"[a-z0-9._-]+@[a-z0-9._-]+\.[a-z]{2,3}\b"
					result = re.sub(regex, "", result, 0, re.MULTILINE)
					regex = r"(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}"
					result = re.sub(regex, "", result, 0, re.MULTILINE)
					regex = r"twitter:.+"
					result = re.sub(regex, "", result, 0, re.MULTILINE)
					soup = BeautifulSoup(result, "html.parser")
					for data in soup(['style', 'script']):
						# Remove tags
					result = '<br>'.join(soup.stripped_strings)	
					# Convert date to something readable
					date = datetime.datetime.fromtimestamp(message["date"]).strftime('%Y-%m-%d')
					time = datetime.datetime.fromtimestamp(message["date"]).strftime('%H:%M:%S')
					if contact == None or contact == "":
						_names.append("Not Found" + " on " + date + " at " + time)
						# If there's a contact, pass picture information, 
						# name and date and time of message
						_pictures.append(contact["id"] + ".png")
						_names.append(contact["given_name"] + 
						" " + contact["surname"] + " on " + date + 
                                                " at " + time)
		return render_template('main.html', threads = _threads)

# Run our application  
if __name__ == "__main__":

Inside the templates folder, we need to create two different files, let’s start with base.html:

<!DOCTYPE html>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<!-- Call the TailwindCSS and Flowbite libraries -->	
	<script src=""></script>
	<link rel="stylesheet" href="" />	
	<title>Email Threadding</title>
{% block content %}  
{% endblock %}	
<script src=""></script>

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

We need to create then the file main.html:

{% extends 'base.html' %}

{% block content %}
<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="/">
<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>
<!-- Do we have any threads? -->
	<div id="accordion-collapse" data-accordion="collapse">
	<!-- Counter to generate accordion elements -->
	{% set counter = namespace(value=1) %}
	<!-- Loop through each thread -->
	{% for thread in threads %}
		<!-- Define values for the accordion elements -->  
		{% set heading = "accordion-collapse-heading-" + counter.value | string() %}
		{% set body = "accordion-collapse-body-" + counter.value | string()  %}
		{% set _body = "#accordion-collapse-body-" + counter.value | string()  %}
		<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-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="">
                                   <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"   
		<div id={{ body }} class="hidden" aria-labelledby={{ heading }}>
		<div class="p-5 font-light border border-b-0 border-gray-200 
		<!-- Get size of thread array -->
		{% set count = thread[1] | length %}
		<!-- Define amount of elements on the grid -->
		{% set count_str = "grid-rows-" + count | string()  %}
		<div class="grid {{ count_str }} grid-flow-col gap-4">
		<!-- Counter to access array elements -->
		{% set _counter = namespace(index=0) %}
		<!-- Loop through each email -->
		{% for message in thread[1] %}
			<div class="col-span-2 ...">
			<!-- Display image and date/time of email -->
			<img class="mx-auto" src="static/{{ thread[2][_counter.index] }}"><b>
			<p class="text-center">{{ thread[3][_counter.index] }}</p></b><br>
			<!-- Display the email message -->
			<p>{{ message | safe }}</p>
		{% set _counter.index = _counter.index + 1 %}
		{% endfor %}
		{% set counter.value = counter.value + 1 %}
	{% endfor %}
{% endblock %}

If you wonder about the static 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

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

$ python3

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


If you want to learn more about our Email APIs, please go to our documentation Email API Overview as well Threads and Messages.

You May Also Like

hero banner
How to Build an Email Responder with Generative AI
How to create and read Google Webhooks using Kotlin
AI solutions
Unleashing intelligence: How Nylas APIs power AI-driven solutions

Subscribe for our updates

Please enter your email address and receive the latest updates.