Mailboxer Docs - kickserv/mailboxer GitHub Wiki

Table of Contents

  1. Overview
  2. How Mailboxer is used in the Kickserv App
  3. Tables and Relationships
  4. Postmark
  5. How to run locally
  6. Gem Maintenance
  7. Helpful Links

Overview

Mailboxer is a Rails gem that creates a private messaging system within an application. It supports the use of conversations with two or more participants, sending notifications to conversation recipients and mailing a messageable model. Mailboxer derived from the outdated rails gem acts_as_messageable.

The Mailboxer gem hasn't been consistently maintained since 2018 and Kickserv forked the repo in order to continue using it within the Kickserv app while upgrading from Rails 5 to Rails 6. Kickserv is now responsible for maintaining the Kickserv Mailboxer repo and this wiki is intended to provide documentation on how Mailboxer works and how it is used with the Kickserv web application.

How Mailboxer is used within the Kickserv App

Mailboxer allows Kickserv account employees to easily communicate with customers via email. An employee can send estimates and/or invoices to a customer, and when a customer responds back, it will display the messages in the conversation for the employee. An employee can also initiate a conversation by composing and sending a message to a customer through the new message modal on the /:account_slug/inbox page by clicking on the Compose button or sending a message through the Messages tab on the job show page.

An example of how a conversation is created on the backend is when an employee sends an outgoing email through Kickserv to a customer, it goes through the inbox/message#create controller action and instantiates a CreateMessageForm object. The CreateMessageForm is where the logic occurs for creating a conversation. If this is the first message in a conversation, it will trigger the .submit! method for the form which will create the conversation, the message, the receipts (When a receipt is created, it triggers a callback to create a participant, more on this is listed in the section below), and lastly it creates a conversationable. If a conversation already exists for the form, it is going to trigger the .reply_to_conversation! method for the form, which will create the message, the receipts and if any new recipients have been added to the conversation.

Within the Kickserv app, the Mailboxer method acts_as_messageable (found here in the repo) is added to models to create the necessary relationships. These models include Account, Contact, Customer, and Employee. This creates a has many relationship between the models and messages as well as receipts. These Kickserv models also include a has many relationship between Conversationable and mailboxer_conversations. This method converts the model into messageable allowing them to interchange messages and receive notifications. More information on these models and relationships is listed below.

Tables and Relationships

Screen Shot 2022-09-14 at 12 45 15 PM

In the diagram above, senders, participants, receivers, and unsubscribers can be one of the following Kickserv models: account, contact, customer, or an employee.

  1. conversationables (Conversationable - Kickserv Model)

Kickserv has a conversationables table which is not part of the Mailboxer Gem. The purpose of this table is to be a joins table between a mailboxer_conversation (foreign key conversation_id) and another conversationable (foreign key conversationable_id) which is a polymorphic relationship. The polymorphic conversationable defines the conversationable_type and the conversationable_id for the conversationable. For example, a conversationable that has a Job as a conversationable_type will have the conversationable_id pointing to that job (conversationable_type can also include a customer or a request). The conversationable also belongs to an account, and the account has many mailboxer_conversations through the conversationable.

  • A conversationable belongs to a mailboxer_conversation, a conversationable (polymorphic), and an account
  • No validations
  1. mailboxer_participants (Mailboxer::Participant - Kickserv Model)

Kickserv also has a mailboxer_participants table which is a joins table between participants (i.e. account, contact, customer, or an employee) and conversations (mailboxer_conversations). For example, an employee has many conversations through mailboxer_participants and a conversation has many participants through mailboxer_participants. This class can be found here.

  • Belongs to a conversation and a participant (polymorphic)
  • No validations
  1. mailboxer_conversations (Mailboxer::Conversation - Mailboxer Model)
  • This table has many opt_outs, messages, and receipts through messages
  • validations only for subject
    • presence: true
    • length which is default 255, this can be changed in the mailboxer config file
Conversation Originator (originator)

When a conversation is created, it also defines the conversation originator. The originator is essentially the person who started the conversation, as in the person who sent the original message for the conversation. The original message is the first message created in the conversation. originator is a method defined in the Mailboxer::Conversation model and the original_message is also defined there as well.

The Originator is very important on the Kickserv side because conversations without an originator can cause failures. If a sender gets deleted from the database, we need to make sure we destroy their messages in the conversations where that sender is the originator, otherwise we will receive a nilClass error in the API. This error will cause users to not be able to view their inbox in their Kickserv account. This occurred when contacts were added as an acts_as_messageable model.

The main takeaway for this is that we need to make sure that a conversation always has an originator to ensure Kickserv users can always view their inboxes.

  1. mailboxer_receipts (Mailboxer::Receipt - Mailboxer Model)
  • A joins table which belongs to a notification (i.e. a message), a receiver which is polymorphic (i.e. account, contact, customer, or an employee), and a message (Kickserv does not use this field and it is not required).
  • validates only the presence of a receiver
  1. mailboxer_notifications (Mailboxer::Notification - Mailboxer Model)
  • A table which has many receipts and is also a joins table between a sender (not required) and a notified object (kickserv does not use the notified_object_type or notified_object_id).
  • validates length for subject - default 255
  • validates presence and length for body - default 32_000
  1. mailboxer_conversation_opt_outs (Mailboxer::Conversation::OptOut - Mailboxer Model)
  • Kickserv does not use this table.

Mailboxer Builders

In Ruby, builders offer a creational design pattern which allows for constructing complex objects step by step. Builders are especially useful when you need to create an object with lots of possible configuration options. Mailboxer builders handle the logic of Mailboxer model/object creation in the Messageable model. All the builders inherit from the Mailboxer::BaseBuilder. Mailboxer builders include the following:

  1. Mailboxer::ConversationBuilder
  2. Mailboxer::MessageBuilder
  3. Mailboxer::NotificationBuilder
  4. Mailboxer::ReceiptBuilder

Mailboxer Messageable Model

This module can be found in the lib directory of the Mailboxer repo. It includes the acts_as_messageable method which gives Kickserv the Mailboxer relationships needed to send and receive messages with Mailboxer. Several important methods are included in this module, such as reply which is used in the CreateContactForm. In the CreateContactForm, when the reply_to_user! method is called (inbound) or reply_to_conversation! (outbound), it calls the Mailboxer Messageable method reply.

The reply method builds the message with the Mailboxer::MessageBuilder and delivers the message with the deliver method (delvier is a Mailboxer::Message instance method). The deliver method then creates the receipts, which are created as unread (is_read) set to false for all recipients. The sender of the message has a receipt created with is_read set to true. deliver then calls the Mailboxer::MailDispatcher which is in charge of sending the message with only the recipient receipts (receiver_receipts, it will not send a message to the sender) with the method default_send_email which takes a receipt as an argument. The default_send_email method calls deliver_now or deliver based on whether the mail object responds to the deliver_now method.

In Summary

  1. reply is called in the CreateMessageForm with the conversation, recipients, message body, subject, and sanitize text false as arguments.
  2. This creates a message object
  3. deliver is called on the message which creates the receipts
  4. Mailboxer::MailDispatcher is called with the receiver_receipts to send the email to the conversation recipients

Developer Note - The method reply is not recommended. It is favored for either reply_to_sender, reply_to_all and reply_to_conversation instead. A great example of this is in the CreateContactForm here in the reply_to_conversation! method. In this method, the following code is run:

    if should_untrash && @mailbox.is_trashed?(conversation)
      @mailbox.receipts_for(conversation).untrash
      @mailbox.receipts_for(conversation).mark_as_not_deleted
    end

The reply_to_conversation method in the Mailboxer Messageable module runs the same code block then calls on reply. This is an example of how we could dry up some of the code on the Kickserv side. More about the functionality of this code block can be found below in the receipts section below.

Mailbox

Mailbox is a Mailboxer model that filters a user's conversations based on certain criteria. This allows users to see unread messages, archived messages, and sent messages in their inbox. The main methods for mailbox are the following:

  1. inbox
  • Optional options argument can be passed through
  • By default, this method filters mailboxer_receipts by maiboxer_type: 'inbox'
  1. sentbox
  • Optional options argument can be passed through
  • By default, this method filters mailboxer_receipts by maiboxer_type: 'sentbox'
  1. trash
  • Optional options argument can be passed through
  • By default, this method filters mailboxer_receipts by maiboxer_type: 'trash'

An example of how Mailbox is used in the Kickserv application is in an employee's inbox. The inbox can be filtered based on unread messages (inbox where conversations have not been read yet), archived conversations (trashed), sent conversations (sendbox), requests (inbox but specific for work requests created by customers on the request form), and defaults to the users inbox mailbox.

ConversationInterpreter

This Kickserv service was intended to be in charge of assigning new participants to a conversation in the method assign_new_participants. The CreateMessageForm instantiates a ConversationInterpreter when the method reply_to_conversation! is called. reply_to_conversation! is called when a conversation already exists. A ConversationInterpreter is instantiated with the following:

  • Conversation
  • Account
  • An optional Conversationable (default is nil)

After the ConversationInterpreter is instantiated, nothing further is done with this object in the CreateMessageForm, as in no further methods are called on it after it is created.

Receipts

Mailboxer Receipts contain a lot of information about messages and the conversation. As mentioned above, receipts are created with is_read false for conversation recipients and is_read true for the message sender. Receipts also determine if a conversation has been the mailbox type (i.e. sentbox, inbox, or trash).

For receipts marked as trash for a conversation, if an outbound message is sent to out that contains receipts that have been marked as trash (trashed => true), they will be untrashed and set to false for the trashed column.

Recipients

Recipients for a conversation in the Kickserv app are typically the customer and all of the accounts employees that have the receives_message_notifications set to true. A contact can also be added as a recipient for a conversation. An example of this is when an estimate or invoice is sent to a customer and includes a CC'd email. If that email replies back to the message, it will create a new contact for that customer and add the contact to the recipient list for the customer.

Postmark

Kickserv uses Postmark to facilitate and ensure a high rate of delivery for emails between customers and account employees. This is setup through the rails postmark gem. If developing locally with Mailboxer and Postmark, please see the Postmark Wiki.

Postmark Code

Inbound messages come through the /app/postmark/inbound POST route. The inbound action calls the build_and_send_message! method from the PostmarkConcern. The PostmarkConcern facilitates all of the logic with the inbound message. When an outbound message is sent out, it includes a replyto hash for the conversationable type (for instance, if the conversationable_type is for a Job, the has will include { job: job.id }). This hash is then dehashed in the PostmarkConcern to look up the conversationable_type. If the conversationable_type is found, it will build and add the message (build_and_add_message) through the CreateMessageForm.

How to run locally

First, clone the repo

  git clone https://github.com/kickserv/mailboxer.git

Then run

   cd mailboxer
   bundle install

From here, if you would like to add pry for debugugging, go to the mailboxer.gemspec file and add s.add_development_dependency('pry', '~> 0.13.1'). Run bundle install again. You can also point the Kickserv repo to your local Mailboxer repo for development by adding the following: Go to the Kickserv gemfile and find the Mailboxer gem, Remove github: and add path: to the repo locally.

Config file

The config file can be used to customize a few features within Mailboxer. Most importantly here, it is where we tell Mailboxer which mailer to use within the Kickserv app. There are two mailers to config here, the notification mailer, which is set to use the Mailboxer::CustomNotificationMailer, and the message mailer, which uses the Mailboxer::CustomMessageMailer. This is also where the subject length and the message body length are defined which are used for validations. Nothing should be changed in this file unless there is a very good reason for doing so, this section of the documentation is purely for information purposes only.

Gem Maintenance

The Kickserv Mailboxer repo is set to run on Rails 6 and above. The gem needs to have a few changes to be fully up to date with Rails 6. The current methods that will need to be changed to avoid breaking changes are the methods containing update_attributes. These methods are located in the Mailboxer::Receipt model and the spec/models/notification_spec file as well. Fortunately, none of the Mailboxer methods used in the Kickserv app contain breaking changes, and all class and module names follow traditional camelcase style (i.e. no class or module names have a section with all caps, such as APIClass, which would cause errors in rails 6.1 and above).

Currently, there is 85.34% test coverage for Mailboxer. Here is a brief todo list for the next steps to upgrade Mailboxer completely to Rails 6:

  1. Get the test suite up and running
    • Create a spec/dummy/app/assets/config/manifest.js file containing the following
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
  • Add the following to spec/spec_helper.rb line 23
  migrations_paths = File.expand_path('../dummy/db/migrate', __FILE__)
  schema_migration = ActiveRecord::Base.connection.schema_migration
  ActiveRecord::MigrationContext.new(migrations_paths, schema_migration).migrate
  • This will now allow RSpec to run the full test suite
  1. Fix failing specs
    • All specs fail due to the deprecated method update_attributes method as mentioned above. This method can be updated with update

NOTE Any changes done on the Mailboxer side will require rigorous QA testing on the Kickserv side. Since everything is stable now, it is probably best to consider updating Mailboxer when Kickserv upgrades to Rails 7.

Dependencies (Gems)

Most gems used within Mailboxer are used for testing. Here is the Link to gemspec for reference.

Gem Name Purpose Description Current Version Gem Docs Additional Info
rails Development Rails >= 6 link
carrierwave Development Upload files from Ruby applications >= 0.5.8 (2.2.2) link
rspec-rails Testing Allows use of Rspec testing library 3.0 Link
rspec-its Testing Use the its method to generate a nested example group with a single example that specifies the expected value of an attribute 1.1 Link
rspec-collection_matchers Testing Lets you express expected outcomes on collections of an object in an example 1.1 Link
appraisal Testing against multiple versions of your dependencies 1.0.0 Link Helpful Video
shoulda-matchers Testing Allows use of should methods for testing rails validations and relationships 2.0 Link
factory_girl Testing Allows use of should methods for testing rails validations and relationships 2.6.0 Link This gem is Deprecated Upgrading to factory Bot
forgery Testing Used to create fake testing data 0.3.6 link
capybara Testing Used to test user interface of applications >= 0.3.9 (3.35.3) link

NOTE - Most all of these gems, especially rails, have many of their own dependencies.

Helpful Links

Mailboxer gem

Kickserv Mailboxer

mailboxer extension folder

⚠️ **GitHub.com Fallback** ⚠️