_why day 2022

Today, we celebrate _why’s day with a Shoes (JRuby) based email client using the Nylas APIs to send, read, reply and delete emails.

_why day 2022

What is _why day?

On a day like today, 13 years ago, _why (also called why the lucky stiff) disappeared from the internet and why would that matter?

Simply, because _why was a visionary, an artist, and an awesome developer.

I started my Ruby journey around 1999 after I read the book Why’s (poignant) Guide to Ruby and hence became a Ruby aficionado and also a _why fanboy.

What did _why created?

But obviously, he created more than that…just to name a few:

  • Camping →  A tiny microframework inspired by Rails
  • Hpricot → An HTML parser
  • Shoes → An UI Toolkit to build web page like applications

So, in order to celebrate _why day, we’re going to build a very simple mailbox application using Shoes and of course, our Nylas APIs.

This mailbox application will allow us to read the first five emails of our email account, read the messages, reply to them, delete them or compose new messages.

Not Ruby, but JRuby

The original Shoes stopped being maintained some time ago and now it is called Shoes 4, but it runs on JRuby instead of regular Ruby.

As you may wonder, JRuby is a Ruby implementation running on the JVM (Java Virtual Machine).

If you have read the blog post How to Send Emails with Ruby Nylas SDK, then you most likely have rbenv installed already. Otherwise, please go ahead and read it.

Using Java

We need to use a specific version of the JVM, which is 9.0.4.0.11, so we can use the following code to see our installed version:

$ /usr/libexec/java_home -V

And then this code to choose the one we need:

$ export JAVA_HOME=`/usr/libexec/java_home -v 9.0`

This will change the version just for our current session.

Installing JRuby

Perform the following to get a list of all available versions:

$ rbenv install -l

And choose the one for JRuby which is, at the time of writing, is this:

jruby-9.3.6.0

After the installation, we just need to make it the default in our system by typing on the terminal:

$ rbenv global jruby-9.3.6.0

Installing additional Gems

Sadly, as we’re using JRuby, we’re not going to be able to install the Nylas gem…but, that doesn’t mean we can’t call the Nylas APIs from our application. So, everything is good, but we’re going to need a couple of extra gems to help us:

# To read .env files
$ gem install dotenv

# Makes http fun again
$ gem install httparty

# Shoes 4 GUI Toolkit
$ gem install shoes --pre

What will our application look like?

Let’s check some screenshots, so we know in advance exactly what we are going to build.

First, we will be greeted by the application and then, the first 5 emails will be displayed. We can either Refresh or Compose a new email.

JRuby Shoes Mail Box

When we open the application, it will present us with the first five emails. For that, we’re using Nylas Messages Endpoint.

Let’s send an email to ourselves. Here we can only go Back or Send the email.

Compose new email message

Once the email has been sent, we will be redirected to the main screen. In order to send the emails, we use the Nylas Send Endpoint.

Read email on client

Now, let’s reply to this message, and see how it looks on our email client.

Here we will need to press the Update button as we haven’t implemented any kind of auto-refresh logic. The Update button will simply fetch the first five emails again.

Read and unread emails

We received a new email and it’s getting displayed in a different color. We can open this email by clicking on its subject.

Read an email to reply

Here, we’re using a chained Regex. It will help us clean the content of the email, by getting rid of the HTML or CSS that might not be rendered correctly. 

Now, we have three options. We can go back, reply to the message or delete it.

Let’s reply first.

Reply to email message

To reply to a message, we use the Nylas Send Endpoint, just like with composing a new message but passing the reply_to_message_id field in the body.

Let’s make sure that it is successfully sent again.

Checking that reply was succesfull

Once we click refresh, we will see our new email as unread, and the one we answered as read.

For this, we call the Nylas Messages Endpoint and update the unread field in the body.

Now, the only thing left for us to do, it’s to delete our message. We need to read it first, and then click on the delete button. 

Read email again, to delete it

Now, the email is gone.

For that we call the Nylas Messages Endpoint and update the label_ids field in the body.

EMail message was deleted

Building our Shoes email application

Now that we have seen how it works, we can go ahead and look at the source code.

We’re going to call this file Shoes_Mail_Client.rb

# Import dependecies
require 'dotenv/load'
require 'httparty'
require 'date'

# This class will hold the header needed to authenticate us
class Headers
  def get_headers()
      return @headers = { 
        "Content-Type" => "application/json",
        "Authorization"  => "Bearer  " + ENV["ACCESS_TOKEN"],
      }
  end
end

# This class will get our name from our Nylas account
class Account
  def initialize()
    @headers = Headers.new()
    # We're calling the Account Endpoint
    @account = HTTParty.get('https://api.nylas.com/account',
                            :headers => @headers.get_headers())
  end
  
  def get_name()
    return @account["name"]
  end
end

# This class will get a list of Labels so that we can send email to the trash
class Label
  def initialize()
    @labelsDict = Hash.new
    @headers = Headers.new()
    # We're calling the Labels Endpoint
    @labels = HTTParty.get('https://api.nylas.com/labels',
                           :headers => @headers.get_headers())
    for @label in @labels
      @labelsDict[@label["name"]] = @label["id"]
    end
  end
  
  def get_trash()
    return @labelsDict["trash"]
  end
end

# This class will make sure our emails look nice and tidy
class Clean_Email
  def initialize(message)
    @body = messagegsub(/\n/," ").gsub(/<style>.+<\/style>/," ").
                                  gsub(/<("[^"]*"|'[^']*'|[^'">])*>/," ").
                                  gsub(/.email-content\{.+\}/," ").
                                  gsub(/&nbsp;/, " ").
                                  gsub(/.s/, " ").
                                  gsub(/^\s+|\s+$\/g/, "")
  end
  
  def get_clean()
    return @body
  end
end

# This class will update our email status, either "Read" or "Delete"
class Update_Email
  def initialize(message, type, label="")
    @headers = Headers.new()
    if type == "read"
      @body = {
        "unread" => false,
      }
    else
      @body = {
        "label_ids" => [label],
      }			
    end
    # We're calling the Messages Endpoint
    @updated_email = HTTParty.put('https://api.nylas.com/messages/' + message, 
                                  :headers => @headers.get_headers(),
                                  :body => @body.to_json)
  end
end

# This class will send our emails or reply them
class Send_Email
  def initialize(recipient_name, recipient_email, subject, body, reply_id=nil)
    @headers = Headers.new()
    @body = {
      "subject" => subject,
      "body" => body,
      "to" => [{
        "name" => recipient_name,
        "email" => recipient_email
      }],
      "reply_to_message_id" =>  reply_id
    }
  end
  
  def send()
    # We're calling the Send Endpoint
    @email = HTTParty.post('https://api.nylas.com/send',
                           :headers => @headers.get_headers(),
                           :body => @body.to_json)
  end
end

# This class will read our inbox and return the 5 most recent ones
class Emails
  def initialize()
    @headers = Headers.new()
  end
  
  def get_emails()
    # We're calling the Messages Endpoint
    @emails = HTTParty.get('https://api.nylas.com/messages?in=inbox&limit=5',
                           :headers => @headers.get_headers())
  end
  
  def return_emails()
    return @emails
  end
end

# This class will control the flow of our application
class Pages < Shoes
  url '/',			    	:index
  url '/read/(\d+)',		:read
  url '/compose',		:compose
  url '/reply/(\d+)',		:reply

  # Class variables that we can access from anywhere on the application
  @@email_detail = Net::HTTP::Get 
  @@trash_label = Net::HTTP::Get
  @@counter = 0

  # Our main view...this is our inbox
  def index
    background lightblue
    @@counter = 0
    @name = Account.new()
    if !@account_name
      @account_name = @name.get_name.split(" ").first
    end
    # Greet the user
    title "Welcome to your Inbox, " + @account_name + "!", align: "center"
    @emails = Emails.new()
    @labels = Label.new()
    # Create the Refresh and Compose buttons
    stack do
      background red
      flow do
        button "Refresh" do
          visit "/"
        end
        button "Compose" do
          visit "/compose"
        end				
      end	
    end
    stack do
      para ""
    end		
    @@trash_label = @labels.get_trash()
    @emails.get_emails()
    @@email_detail = @emails.return_emails()
    for @email in @@email_detail
      stack do
        @datetime = Time.at(@email["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}/)
        # Display emails: Subject, Sender, Date and Time. 
        # Also if unread, display it using a different color
        if @email["unread"] == true
          para " ", link(@email["subject"],
                         :click=>"/read/#{@@counter}"), " | " , 
                         @email["from"][0]["name"] , " | ", @date , " - ", 
                         @time, :size => 20, :stroke => darkblue
        else
          para " ", link(@email["subject"],
                         :click=>"/read/#{@@counter}"), " | " , 
                         @email["from"][0]["name"] , " | ", @date , " - ", 
                         @time, :size => 20
        end
        para ""
      end
        # Global counter to make sure we're opening the right email
        @@counter += 1
    end
  end

  # View to read an email
  def read(index)
    background lightblue	
    # Update email state so it goes from "unread" to "read"
    @update_email = Update_Email.new(@@email_detail[index.to_i]["id"],
                                     "read","")
    stack do
      background red
      flow do
        button "Back" do
          visit "/"
        end
        button "Reply" do
          visit "/reply/#{index}"
        end
        # When deleting an email, pass the "trash" label
        button "Delete" do
          @email = Update_Email.new(@@email_detail[index.to_i]["id"],
                                    "delete",@@trash_label)
          visit "/"
        end				
      end	
    end
    stack do
      background lavender
      flow do
        para "Date: ", :size => 25
        @datetime = Time.at(@@email_detail[index.to_i]["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}/)
        para @date[0] + " - " + @time[0], :size => 20
      end
    end		
    stack do
      background lightcyan
      flow do
        para "Sender: ", :size => 25
        para @@email_detail[index.to_i]["from"][0]["name"] + " / " + 
                @@email_detail[index.to_i]["from"][0]["email"], :size => 20
      end
    end	
    stack do
      background lightskyblue
      flow do
        para "Subject: ", :size => 25
        para @@email_detail[index.to_i]["subject"], :size => 20
      end
    end
    stack do
      # Call the Clean_Email in order to get rid of extra HTML
      @clean_email = Clean_Email.new(@@email_detail[index.to_i]["body"])
      @conversation = @clean_email.get_clean()		
        para "Body: ", :size => 25
        para ""
        para @conversation, :size => 20
    end
  end
  
  # View to compose an email
  def compose()
    @recipient_name = ""
    @recipient_email = ""
    @subject = ""
    @body = ""
    background lightblue
    stack do
      background red
      flow do
        button "Back" do
          visit "/"
        end
        button "Send" do
          # Call the Messages Endpoint to send the email
          @email = Send_Email.new(@recipient_name.text,
                                  @recipient_email.text,
                                  @subject.text,@body.text)
          @email.send()
          visit "/"
        end	
      end	
    end
    stack do
      background lightcyan
      flow do
        para "Recipient's Name: ", :size => 25
        @recipient_name = edit_line :width => 400
      end
    end
    stack do
      background lightcyan
      flow do
        para "Recipient's Email: ", :size => 25
        @recipient_email = edit_line :width => 400
      end
    end		
    stack do
      background lightskyblue
      flow do
        para "Subject: ", :size => 25
        @subject = edit_line :width => 600
      end
    end	
    stack do
      flow do
        para "Body: ", :size => 25
        @body = edit_box :width => 800, :height => 240
      end
    end			
  end
  
  # View to reply an email
  def reply(index)
    @recipient_name = ""
    @recipient_email = ""
    @subject = ""
    @body = ""
    background lightblue
    stack do
      background red
      flow do
        button "Back" do
          visit "/"
        end
        button "Send" do
          # Call the Messages Endpoint to send the email
          # and using the reply_to_message_id
          @email = Send_Email.new(@recipient_name,@recipient_email,
                                  @subject.text,@body.text,
                                  @@email_detail[index.to_i]["id"])
          @email.send()
          visit "/"
        end	
      end	
    end
    stack do
      background lightcyan
      flow do
        para "Recipient's Name: ", :size => 25
        para @@email_detail[index.to_i]["from"][0]["name"], :size => 20
        @recipient_name = @@email_detail[index.to_i]["from"][0]["name"]
      end
    end
    stack do
      background lightcyan
      flow do
        para "Recipient's Email: ", :size => 25
        para @@email_detail[index.to_i]["from"][0]["email"], :size => 20
        @recipient_email = @@email_detail[index.to_i]["from"][0]["email"]
      end
    end		
    stack do
      background lightskyblue
      flow do
        para "Subject: ", :size => 25
        @subject = edit_line :width => 600
        @subject.text = "Re: " + @@email_detail[index.to_i]["subject"]
      end
    end	
    stack do
      flow do
        # Body of reply message. 
        @clean_email = Clean_Email.new(@@email_detail[index.to_i]["body"])
        @conversation = @clean_email.get_clean()				
        @datetime = Time.at(@@email_detail[index.to_i]["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}/)
        para "Body: ", :size => 25
        @body = edit_box :width => 800, :height => 240
        @body.text = "\n\n\nOn " + @date[0] + " at " + @time[0] + "  " + 
                     @@email_detail[index.to_i]["from"][0]["email"]  + 
                     " wrote: \n\n\n" + @conversation[0]["conversation"]
      end
    end	
  end
  
end

# We call our Shoes application, specifying the name, width, height and if it's resizable or not
Shoes.app title: "Shoes Email Client", width: 1000, 
          height: 400, resizable: false

A little long, but it’s a complex application. And, I know what you’re thinking…Regex to sanitize HTML? Why don’t you use something like Nokogiri? Well, Shoes comes bundled with version 1.6.4.1 of Nokogiri, which happens to be incompatible with the JVM as it produces an Illegal reflective access operation.

Running our Shoes application

Now that we have our application ready, it’s time to run it. And we can do that from the terminal window:

$ shoes Shoes_Mail_Client.rb

And it will display our inbox.

Checking inbox again

If you want to check the source code, you can find it on our Github Samples as Shoes-Mail-Client.

What’s next

If you want to learn more about Shoes, read the book Nobody Knows Shoes and if you want to learn more about the Nylas APIs read our documentation.

Don’t miss the action, watch our LiveStream Coding with Nylas:

You May Also Like

Transactional Email APIs vs Contextual Email APIs
Best email tracker
Find the best email tracker and elevate your app’s email game
How to create and read Webhooks with PHP, Koyeb and Bruno

Subscribe for our updates

Please enter your email address and receive the latest updates.