Key Takeaways
- Mailboxer is a versatile Rails gem that allows any model to send and receive messages, making it ideal for building messaging systems within social networks.
- The demonstration app utilizes Devise for user authentication and Bootstrap along with the Chosen jQuery plugin for crafting a user-friendly interface.
- Key functionalities include creating conversations, managing message folders (inbox, sentbox, trash), and setting up email notifications for new messages.
- Users can manage their avatars using Gravatar, enhancing the visual appeal and personalization of the messaging interface.
- The system allows for marking conversations as read, restoring deleted conversations from the trash, and sending messages with the option to add attachments.
- The guide provides detailed setup instructions and code snippets, ensuring even those new to Rails or Mailboxer can implement the features effectively.
Recently I’ve written a series of articles about crafting chat applications: Mini-Chat with Rails, Realtime Mini-Chat with Rails and Faye, and Mini-chat with Rails and Server-Sent Events. These posts have gained some popularity among readers and I’ve received a lot of feedback in the comments, as well as by email. This is great and, once again, I wanted to thank you guys for sharing your thoughts.
One of the readers mentioned Mailboxer, a nice solution to implement messaging systems. So, I decided to research it and share the results with you.
Mailboxer is a Rails gem that is a part of the social_stream framework for building social networks. It is a generic messaging system that allows any model to act “messageable”, equipping it with some versatile methods. With Mailboxer, you can create conversations with one or more recipients (messages are organized into folders – sentbox, inbox, trash) and send notifications via email. It is even possible to send messages between different models and add attachments! The only drawback is the lack of documentation, so I hope this post will be useful.
We are going to discuss a demonstration app that:
- Uses basic authentication with Devise
- Allows users to manage avatars with Gravatar
- Integrates Mailboxer
- Creates a GUI for starting new conversations, as well as replying to the existing ones (using Bootstrap’s styles and the Chosen jQuery plugin)
- Displays folders and allows easily switching between them
- Allows marking conversations as read, trashing conversations, and restoring them. It’ll clear the trash bin too.
- Sets up email notifications
Rails 4 will be used for this demo, but nearly the same solution can be implemented with Rails 3.2 (version 3.1 is not supported by Mailboxer anymore).
The source code is available on GitHub.
The working demo can be found at sitepoint-mailboxer.herokuapp.com.
Preparing the App
Suppose we have to create a private messaging system for internal use where co-workers can discuss different topics. This system should allow users to create conversations with an unlimited number of recipients, provide a notification system, and allow the deletion of outdated conversations.
Let’s call it Synergy and start off by creating a new Rails app without the default testing suite:
$ rails new Synergy -T
Drop these gems into your Gemfile (I am going to stick with Bootstrap, but you can use any other CSS framework, employ your own design, or skip prettifying the website completely):
Gemfile
[...]
gem 'bootstrap-sass'
gem 'bootstrap-will_paginate'
gem 'will_paginate'
[...]
Run
$ bundle install
and drop in Bootstrap’s files if you wish to follow along:
application.css.scss
@import 'bootstrap';
@import 'bootstrap/theme';
Next tweak the layout a bit:
layouts/application.html.erb
[...]
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
<div class="page-header">
<h1><%= yield :page_header %></h1>
</div>
<%= yield %>
</div>
[...]
Let’s also add a helper method to render the page header easily:
application_helper.rb
[...]
def page_header(text)
content_for(:page_header) { text.to_s }
end
[...]
Authentication
Before implementing messaging functionality, we need a model that will message. Create the User
model:
$ rails g model User name:string
$ rake db:migrate
You can use any type of authentication, but I like Devise. Devise’s basic set up is very simple and there are plenty of docs to help you further customize things.
Drop a new gem:
Gemfile
[...]
gem 'devise'
[...]
and install it:
$ bundle install
Now we can take advantage of Devise’s code generator to do some work for us:
$ rails generate devise:install
Be sure to read the post-install message to complete some additional steps. Specifically, you will need to tweak the config.action_mailer.default_url_options
setting for development and production, as it will be used to send emails to the users (to help them restore forgotten passwords, for example). Please note that email won’t be sent in development unless you set config.action_mailer.perform_deliveries = true
in environments/development.rb.
Here are some examples on how to configure ActionMailer.
When you are ready, run the following command to create the User
model with Devise:
$ rails generate devise User
$ rake db:migrate
You may want to check the generated migration before applying it to add some more fields to your table (to enable Confirmable or Lockable modules). You will also need then to tweak the model, accordingly.
Lastly, run
$ rails generate devise:views
to copy Devise’s views directly into your project so they can be modified it a bit. Users will be able to change their names, so add a new field to the registration form:
views/devise/registrations/new.html.erb
[...]
<%= f.label :name %>
<%= f.text_field :name %>
[...]
Drop the same code to the views/devise/registrations/edit.html.erb (or refactor it into a partial) so users can provide their name when registering and, later, edit it.
This is Rails 4, so strong_params
are in play. Permit the new :name
parameter to be passed:
application_controller.rb
[...]
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :name
devise_parameter_sanitizer.for(:account_update) << :name
end
The devise_controller?
method is provided by Devise. Here, we simply permit the :name
attribute for account creation and editing. If you forget to do this, users won’t be able to set their names.
At this point, you may also style the views a bit. I will not cover this step, as it’s not too hard and will highly depend on your setup (whether you are using Bootstrap or not). If you’ve decided to use Bootstrap, then flash messages generated by Devise won’t be styled. To fix this, use the SASS @extend
method, like this:
application.css.scss
[...]
.alert-notice {
@extend .alert-success;
}
.alert-alert {
@extend .alert-warning;
}
Integrating Mailboxer
Great, at last we are ready to proceed with the main task – integrating and setting up Mailboxer.
First of all, add the new gem:
Gemfile
[...]
gem "mailboxer"
[...]
and install it:
$ bundle install
Generate and apply all the required migrations and create an initializer file:
$ rails g mailboxer:install
$ rake db:migrate
Take a look at config/initializers/mailboxer.rb to see which options you can modify. For now, let’s leave this file as is – later we will set up sending email notifications.
Our model requires a small change to equip it with Mailboxer functionality:
models/user.rb
[...]
acts_as_messageable
[...]
Displaying Conversations
As suggested in this small guide on creating a GUI for Mailboxer, the optimal way is to create two controllers: one for messages and one for conversations. Individual messages are grouped into conversations. Later, you’ll see that conversations can be different types.
Begin by creating a new controller for conversations:
conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
before_action :get_mailbox
def index
@conversations = @mailbox.inbox.paginate(page: params[:page], per_page: 10)
end
private
def get_mailbox
@mailbox ||= current_user.mailbox
end
end
Every user has its own mailbox which, in turn, is divided into the inbox, sentbox, and trash. Currently we are working only with the inbox.
The authenticate_user!
method is part of Devise. We only want authenticated users to access our app, so it’s set as the before_action
. If a user is not authenticated, she will be redirected to the sign in page.
As you see, I am also using the paginate
method provided by will_paginate
. The styling for pagination is provided by bootstrap-will_paginate
.
Add some routes (the other controller methods will be added soon):
config/routes.rb
[...]
resources :conversations, only: [:index, :show, :destroy]
[...]
Now the view:
views/conversations/index.html.erb
<% page_header "Your Conversations" %>
<ul class="list-group">
<%= render partial: 'conversations/conversation', collection: @conversations %>
</ul>
<%= will_paginate %>
page_header
is our helper method created earlier. will_paginate
displays pagination controls (only if there are more than one page). We could write it as will_paginate @conversations
but the gem is clever enough to understand what we want to paginate in this case (convention over configuration!).
We have to specify the partial
argument for render
because @conversations
is an instance of Mailboxer::Conversation::ActiveRecord_Relation
and therefore Rails will look for the _conversation
partial located inside the mailboxer/conversations directory by default.
Now the actual partial:
views/conversations/_conversation.html.erb
<li class="list-group-item clearfix">
<%= link_to conversation.subject, conversation_path(conversation) %>
</li>
Each conversation has a subject and a handful of messages which will be rendered on the show page. The .clearfix
CSS class will be required soon.
Add a menu to the layout:
layouts/application.html.erb
[...]
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Synergy', root_path, class: 'navbar-brand' %>
</div>
<ul class="nav navbar-nav">
<% if user_signed_in? %>
<li><%= link_to 'Edit Profile', edit_user_registration_path %></li>
<li><%= link_to 'Your Conversations', conversations_path %></li>
<li><%= link_to 'Log Out', destroy_user_session_path, method: :delete %></li>
<% else %>
<li><%= link_to 'Log In', new_user_session_path %></li>
<% end %>
</ul>
</div>
</nav>
</body>
[...]
The user_signed_in?
method, as well as most of the routes, are provided by Devise.
Next is the show action:
conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
before_action :get_mailbox
before_action :get_conversation, except: [:index]
def show
end
private
def get_conversation
@conversation ||= @mailbox.conversations.find(params[:id])
end
end
I’ve added a new before_action
and tweaked the existing one.
You probably know that the find
method raises an exception when no record is found. This is what we want, but the exception should be rescued. To keep things simple let’s use the rescue_from
method:
application_controller.rb
[...]
rescue_from ActiveRecord::RecordNotFound do
flash[:warning] = 'Resource not found.'
redirect_back_or root_path
end
def redirect_back_or(path)
redirect_to request.referer || path
end
[...]
We are simply redirecting the user back with a warning message. If the referer
field is not set (for example if a user has installed an add-on to clear this field), they are redirected to the root_path
.
Now the view:
views/conversations/show.html.erb
<% page_header "Conversation" %>
<div class="panel panel-default">
<div class="panel-heading"><%= @conversation.subject %></div>
<div class="panel-body">
<div class="messages">
<% @conversation.receipts_for(current_user).each do |receipt| %>
<% message = receipt.message %>
<%= message.sender.name %>
says at <%= message.created_at.strftime("%-d %B %Y, %H:%M:%S") %>
<%= message.body %>
<% end %>
</div>
</div>
</div>
We are rendering each message, showing the sender’s name, date of creation, and the body. Let’s style the .messages
container a bit so that it does not become too tall:
application.css.scss
[...]
.messages {
max-height: 400px;
overflow-y: auto;
margin-bottom: 1em;
margin-top: 1em;
}
Nice, some basic views are present, however, we still lack important bits of the app:
- User should know whom is he chatting with
- Users need to be able to start new conversations
- Users should be able to respond to conversations
- Sentbox and trash should be displayed on the conversations page
- Users should be able to mark conversations as read
Displaying User Avatars
While this is not related to Mailboxer, I thought that showing avatars would make our app look prettier. However, allowing users to upload their avatars directly into the app would be overkill, so let’s use Gravatar and gravatarimagetag to integrate it with Rails.
Drop a new gem into the Gemfile:
Gemfile
[...]
gem 'gravatar_image_tag'
[...]
and run
$ bundle install
Also, add a helper method to easily render avatars:
application_helper.rb
[...]
def gravatar_for(user, size = 30, title = user.name)
image_tag gravatar_image_url(user.email, size: size), title: title, class: 'img-rounded'
end
[...]
Create a separate partial to render avatars of a conversation’s participants (except for the current user):
views/conversations/_participants.html.erb
<% conversation.participants.each do |participant| %>
<% unless participant == current_user %>
<%= gravatar_for participant %>
<% end %>
<% end %>
Modify the following views:
views/conversations/show.html.erb
<% page_header "Conversation" %>
<p>Chatting with
<%= render 'conversations/participants', conversation: @conversation %>
</p>
<div class="panel panel-default">
<div class="panel-heading"><%= @conversation.subject %></div>
<div class="panel-body">
<div class="messages">
<% @conversation.receipts_for(current_user).each do |receipt| %>
<div class="media">
<% message = receipt.message %>
<div class="media-left">
<%= gravatar_for message.sender, 45, message.sender.name %>
</div>
<div class="media-body">
<h6 class="media-heading"><%= message.sender.name %>
says at <%= message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></h6>
<%= message.body %>
</div>
</div>
<% end %>
</div>
</div>
</div>
views/conversations/_conversation.html.erb
<li class="list-group-item clearfix">
[...]
<p><%= render 'conversations/participants', conversation: conversation %></p>
</li>
While we are here, display the last message of the conversation and its creation date:
views/conversations/_conversation.html.erb
<li class="list-group-item clearfix">
[...]
<p><%= render 'conversations/participant', conversation: conversation %></p>
<p><%= conversation.last_message.body %>
<small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></span>)</small></p>
</li>
We’ve finished with avatars. It’s high time we allow users to start conversations.
Creating Conversations
Creating a conversation actually means creating a new message while providing a subject (this is optional though). This means that a new controller will be needed:
messages_controller.rb
class MessagesController < ApplicationController
before_action :authenticate_user!
def new
end
def create
recipients = User.where(id: params['recipients'])
conversation = current_user.send_message(recipients, params[:message][:body], params[:message][:subject]).conversation
flash[:success] = "Message has been sent!"
redirect_to conversation_path(conversation)
end
end
In the create action, find an array of users (stored in the params['recipients']
) and utilize Mailboxer’s send_message
method, passing in the recipients, body, and the subject. Later, we will enable email notifications so users know when a new message is received.
Now the view:
views/messages/new.html.erb
<% page_header "Start Conversation" %>
<%= form_tag messages_path, method: :post do %>
<div class="form-group">
<%= label_tag 'message[subject]', 'Subject' %>
<%= text_field_tag 'message[subject]', nil, class: 'form-control', required: true %>
</div>
<div class="form-group">
<%= label_tag 'message[body]', 'Message' %>
<%= text_area_tag 'message[body]', nil, cols: 3, class: 'form-control', required: true %>
</div>
<div class="form-group">
<%= label_tag 'recipients', 'Choose recipients' %>
<%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control' %>
</div>
<%= submit_tag 'Send', class: 'btn btn-primary' %>
<% end %>
recipients_options
is a helper method that we need to create:
messages_helper.rb
module MessagesHelper
def recipients_options
s = ''
User.all.each do |user|
s << "<option value='#{user.id}'>#{user.name}</option>"
end
s.html_safe
end
end
Don’t forget about the routes:
config/routes.rb
[...]
resources :messages, only: [:new, :create]
[...]
Let’s present a “Start conversation” link on the conversations#index page:
views/conversations/index.html.erb
<% page_header "Your Conversations" %>
<p><%= link_to 'Start conversation', new_message_path, class: 'btn btn-lg btn-primary' %></p>
[...]
Technically, everything is ready to post your first message. You can either have a nice chat with yourself or register another account to emulate situation with two users.
However, selecting recipients is not very convenient. Currently, a basic select field is rendered so, if there are many users, finding someone in the list can be a tedious task. We can enhance this field with some superpowers using Chosen, a jQuery plugin that makes selects more user-friendly. There is a chosen-rails gem that makes integrating this plugin into Rails app easier.
Add this gem to your Gemfile:
Gemfile
[...]
gem 'chosen-rails'
[...]
I also had to specify versions for sass-rails
and coffee-rails
, as I was getting errors related to the application.css.scss file, which is a known bug):
Gemfile
[...]
gem 'chosen-rails'
gem 'sass-rails', '~> 4.0.5'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-turbolinks'
[...]
Also, I am using the jquery-turbolinks gem to bring back the default jQuery page load event when using Turbolinks.
Don’t forget to run
$ bundle install
and add Chosen to application.js and application.css.scss:
javascripts/application.js
[...]
//= require jquery.turbolinks
//= require chosen-jquery
[...]
stylesheets/application.css.scss
[...]
@import 'chosen';
[...]
Now let’s add the class .chosen-it
to our select tag:
views/messages/new.html.erb
[...]
<div class="form-group">
<%= label_tag 'recipients', 'Choose recipients' %>
<%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control chosen-it' %>
</div>
[...]
and equip all elements of this class with Chosen’s magic:
javascripts/messages.coffee
jQuery ->
$('.chosen-it').chosen()
javascripts/application.js
[...]
//= require messages
[...]
Now reload the server, navigate to conversations/new, and behold the new shiny select tag. It is much more convenient to use, isn’t it?
We could go further and display avatars along with users’ names inside the select tag. There is an Image-Select extension for Chosen. Just hook up ImageSelect.jquery.js and ImageSelect.css files into your project and require them in application.js and application.css.scss, respectively. Then, modify the helper method a bit:
messages_helper.rb
module MessagesHelper
def recipients_options
s = ''
User.all.each do |user|
s << "<option value='#{user.id}' data-img-src='#{gravatar_image_url(user.email, size: 50)}'>#{user.name}</option>"
end
s.html_safe
end
end
Then reload the server and check the results. Very cool!
Replying to a Conversation
Now, users can create conversations, but there is no option to reply! To fix this, we need another form and a controller method, as well as a new route:
views/conversations/show.html.erb
[...]
<%= form_tag reply_conversation_path(@conversation), method: :post do %>
<div class="form-group">
<%= text_area_tag 'body', nil, cols: 3, class: 'form-control', placeholder: 'Type something...', required: true %>
</div>
<%= submit_tag "Send Message", class: 'btn btn-primary' %>
<% end %>
You can allow users to add a subject as well by adding another text field. Consider that homework. :)
conversations_controller.rb
[...]
def reply
current_user.reply_to_conversation(@conversation, params[:body])
flash[:success] = 'Reply sent'
redirect_to conversation_path(@conversation)
end
[...]
The Mailboxer method reply_to_conversation
makes this a snap. It accepts a conversation to reply to, a message body, an optional subject, and a handful of other arguments. Note that, if the conversation was moved to trash (which we will handle shortly) it will be restored by default. Take a look at the source code for more info.
Now the route:
config/routes.rb
[...]
resources :conversations, only: [:index, :show, :destroy] do
member do
post :reply
end
end
[...]
Very nice, out the basic chat system is up and running!
Implementing Sentbox and Trash
Currently, we are showing only the user’s inbox. However, it’s a good idea to display the sentbox and trash folders, as well.
Probably the easiest way to mark which folder to render is using a GET parameter, so let’s tweak the controller accordingly:
conversations_controller.rb
[...]
before_action :get_box, only: [:index]
def index
if @box.eql? "inbox"
@conversations = @mailbox.inbox
elsif @box.eql? "sent"
@conversations = @mailbox.sentbox
else
@conversations = @mailbox.trash
end
@conversations = @conversations.paginate(page: params[:page], per_page: 10)
end
private
def get_box
if params[:box].blank? or !["inbox","sent","trash"].include?(params[:box])
params[:box] = 'inbox'
end
@box = params[:box]
end
[...]
The new private get_box
method is introduced to fetch the requested folder.
On to the view. If you are using Bootstrap, I suggest using vertical navigational pills to render the list of folders. Also, the current folder should be highlighted. Create a helper method for this:
conversations_helper.rb
module ConversationsHelper
def mailbox_section(title, current_box, opts = {})
opts[:class] = opts.fetch(:class, '')
opts[:class] += ' active' if title.downcase == current_box
content_tag :li, link_to(title.capitalize, conversations_path(box: title.downcase)), opts
end
end
This method takes the title of the link (which is also used to specify a GET parameter), the currently opened folder, and a hash with options passed directly to the content_tag
method. Then, check if the opts
hash already has a class
key. If not, set it to an empty string and then append an active
class if this is the current box.
Change the view:
views/conversations/index.html.erb
<% page_header "Your Conversations" %>
<div class="row">
<div class="col-sm-3">
<ul class="nav nav-pills nav-stacked">
<%= mailbox_section 'inbox', @box %>
<%= mailbox_section 'sent', @box %>
<%= mailbox_section 'trash', @box %>
</ul>
</div>
<div class="col-sm-9">
<ul class="list-group">
<%= render partial: 'conversations/conversation', collection: @conversations %>
</ul>
<%= will_paginate %>
</div>
</div>
The next step is adding an “Add to trash” button for each conversation that is not yet trashed. For trashed conversations, the “Restore” button should be displayed.
views/conversations/_conversation.html.erb
<li class="list-group-item clearfix">
<%= link_to conversation.subject, conversation_path(conversation) %>
<div class="btn-group-vertical pull-right">
<% if conversation.is_trashed?(current_user) %>
<%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %>
<% else %>
<%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete,
data: {confirm: 'Are you sure?'} %>
<% end %>
<% end %>
</div>
<p><%= render 'conversations/participant', conversation: conversation %></p>
<p><%= conversation.last_message.body %>
<small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></span>)</small></p>
</li>
The corresponding methods:
conversations_controller.rb
[...]
before_action :authenticate_user!
before_action :get_mailbox
before_action :get_conversation, except: [:index]
before_action :get_box, only: [:index]
[...]
def destroy
@conversation.move_to_trash(current_user)
flash[:success] = 'The conversation was moved to trash.'
redirect_to conversations_path
end
def restore
@conversation.untrash(current_user)
flash[:success] = 'The conversation was restored.'
redirect_to conversations_path
end
[...]
Once again I’ve tweaked before actions a bit so that they take place only when needed. move_to_trash
and untrash
are the two methods presented by Mailboxer and they are pretty self-explanatory.
Now the routes:
config/routes.rb
[...]
resources :conversations, only: [:index, :show, :destroy] do
member do
post :restore
end
end
[...]
How about an “Empty trash” button. Easy:
views/conversations/index.html.erb
[...]
<div class="col-sm-9">
<% if @box == 'trash' %>
<p><%= link_to 'Empty trash', empty_trash_conversations_path, class: 'btn btn-danger', method: :delete,
data: {confirm: 'Are you sure?'} %></p>
<% end %>
<ul class="list-group">
<%= render partial: 'conversations/conversation', collection: @conversations %>
</ul>
<%= will_paginate %>
</div>
[...]
The corresponding method:
conversations_controller.rb
before_action :get_conversation, except: [:index, :empty_trash]
[...]
def empty_trash
@mailbox.trash.each do |conversation|
conversation.receipts_for(current_user).update_all(deleted: true)
end
flash[:success] = 'Your trash was cleaned!'
redirect_to conversations_path
end
[...]
and the route:
config/routes.rb
resources :conversations, only: [:index, :show, :destroy] do
collection do
delete :empty_trash
end
end
Marking Conversation as Read
Let’s allow user to mark conversations as read. To implement it, we will need yet another method, route, and button:
views/conversations/_conversation.html.erb
[...]
<div class="btn-group-vertical pull-right">
<% if conversation.is_trashed?(current_user) %>
<%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %>
<% else %>
<%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete,
data: {confirm: 'Are you sure?'} %>
<% if conversation.is_unread?(current_user) %>
<%= link_to 'Mark as read', mark_as_read_conversation_path(conversation),
class: 'btn btn-xs btn-info', method: :post %>
<% end %>
<% end %>
</div>
[...]
The is_unread?
method is used here (a user has to be specified). There is another method is_read?
that does the opposite.
conversations_controller.rb
[...]
def mark_as_read
@conversation.mark_as_read(current_user)
flash[:success] = 'The conversation was marked as read.'
redirect_to conversations_path
end
[...]
Lastly, the route:
config/routes.rb
[...]
resources :conversations, only: [:index, :show, :destroy] do
member do
post :mark_as_read
end
end
[...]
Brilliant! You can also tweak the show action so that the conversation is marked as read when it is opened…more homework…
E-mail Notifications
Remember, Mailboxer can send email notifications each time the user receives a message. This feature is enabled in the initializer:
config/initializers/mailboxer.rb
Mailboxer.setup do |config|
#Configures if you application uses or not email sending for Notifications and Messages
config.uses_emails = true
#Configures the default from for emails sent for Messages and Notifications
config.default_from = "no-reply@mailboxer.com"
#Configures the methods needed by mailboxer
config.email_method = :mailboxer_email
config.name_method = :name
[...]
end
The config.email_method
and config.name_method
tell Mailboxer how to get the email and name, respectively. name
is already present for our User
model, however there is no mailboxer_email
. You could try to change this value to just email
as this method was added by Devise, but that would result in an error because Mailboxer passes an argument to it which contains the received message. The choices are either redefine this method or create a new one. I will stick with the second option:
user.rb
[...]
def mailboxer_email(object)
email
end
[...]
Email notifications are now enabled (make sure to set up ActionMailer as described earlier. Also, don’t forget that by default mail will not be sent in development. And yes, I’ve disabled this functionality in the demo app.)
Conclusion
Whew. That was quite a lot to discuss, wasn’t it? We looked at the basic features of Mailboxer, including messages, different types of conversations, managing them, and setting up email notifications. We’ve also integrated Devise into the app and taken advantage of Gravatar to make things look a bit prettier.
I hope this post was useful for you. By the way, you may be interested in this page on the Mailboxer wiki and this sample app presenting the basic features of Mailboxer.
As always, your feedback is welcome. If you want me to cover a specific topic, please don’t hesitate to ask. Happy coding!
UPDATE: 2015/03/29
I’ve received a lot of feedback and questions from the readers – it is really great to know that my articles are useful. There is a question that was asked several times – “How can I add a button to send a message to the specific user”? I believe this is a pretty common feature and decided to add it as an update to the article. This can be done pretty easily. The specified user should be automatically chosen from a dropdown list on the “Start conversation” page. I think that the best way to provide the user is by using a GET parameter. So modify theMessagesController
like this:
messages_controller.rb
def new
@chosen_recipient = User.find_by(id: params[:to].to_i) if params[:to]
end
Now the @chosen_recipient
either contains a user record or a nil
value.
The view:
views/messages/new.html.erb
<div class="form-group">
<%= label_tag 'recipients', 'Choose recipients' %>
<%= select_tag 'recipients', recipients_options(@chosen_recipient), multiple: true, class: 'form-control chosen-it' %>
</div>
We just pass the @chosen_recipient
to the helper method.
messages_helper.rb
def recipients_options(chosen_recipient = nil)
s = ''
User.all.each do |user|
s << "<option value='#{user.id}' data-img-src='#{gravatar_image_url(user.email, size: 50)}' #{'selected' if user == chosen_recipient}>#{user.name}</option>"
end
s.html_safe
end
Here is an updated version of the recipients_options
helper method. Just set the selected
attribute for an option if the user is equal to the selected one.
Basically, that’s it. To demonstrate how this works, add a separate page with a list of users and a “Send message” button next to each one.
config/routes.rb
resources :users, only: [:index]
users_controller.rb
class UsersController < ApplicationController
def index
@users = User.order('created_at DESC').paginate(page: params[:page], per_page: 30)
end
end
views/users/index.html.erb
<% page_header "Users" %>
<%= will_paginate %>
<ul>
<% @users.each do |user| %>
<li>
<strong><%= user.name %></strong>
<% unless current_user == user %>
<%= link_to 'Send message', new_message_path(to: user.id), class: 'btn btn-default btn-sm' %>
<% end %>
</li>
<% end %>
</ul>
<%= will_paginate %>
There you have it. Keep the feedback coming!
Frequently Asked Questions about Messaging in Rails with Mailboxer
How do I install Mailboxer in Rails?
To install Mailboxer in Rails, you first need to add the gem to your Gemfile. You can do this by opening your Gemfile and adding the line gem 'mailboxer'
. After adding the gem, run bundle install
in your terminal to install it. Once the gem is installed, you need to run the generator with rails g mailboxer:install
and then migrate your database with rake db:migrate
. This will create the necessary tables and associations in your database for Mailboxer to function.
How do I send a message with Mailboxer?
Sending a message with Mailboxer is straightforward. You first need to have two users, the sender and the receiver. You can send a message from the sender to the receiver with the following code: sender.send_message(receiver, "Body of the message", "Subject")
. This will create a new message from the sender to the receiver with the specified body and subject.
How do I retrieve messages with Mailboxer?
To retrieve messages with Mailboxer, you can use the mailbox
method on a user. This will return a Mailbox
object, which has methods for retrieving different types of messages. For example, to get all the received messages for a user, you can use user.mailbox.inbox
. To get all the sent messages, you can use user.mailbox.sentbox
. To get all the unread messages, you can use user.mailbox.inbox.unread
.
How do I mark a message as read with Mailboxer?
To mark a message as read with Mailboxer, you can use the mark_as_read
method on a message. This method takes a user as an argument and marks the message as read for that user. For example, to mark a message as read for a user, you can use message.mark_as_read(user)
.
How do I reply to a message with Mailboxer?
To reply to a message with Mailboxer, you can use the reply
method on a message. This method takes two arguments: the user who is replying and the body of the reply. For example, to reply to a message, you can use message.reply(user, "Body of the reply")
. This will create a new message that is a reply to the original message.
How do I customize the Mailboxer views?
Mailboxer comes with default views for displaying messages, but you can customize these views to fit your application’s needs. To do this, you can run the command rails g mailboxer:views
in your terminal. This will copy the default views to your application’s views directory, where you can customize them.
How do I configure Mailboxer?
Mailboxer can be configured by creating an initializer file in your application’s config/initializers
directory. In this file, you can set various options for Mailboxer, such as the default from address for emails, whether or not to use delayed job for sending emails, and more. For example, to set the default from address, you can add the line Mailboxer.default_from = "no-reply@myapp.com"
to your initializer file.
How do I add attachments to messages with Mailboxer?
Mailboxer supports adding attachments to messages. To do this, you can use the add_attachment
method on a message. This method takes a file as an argument and adds it as an attachment to the message. For example, to add an attachment to a message, you can use message.add_attachment(file)
.
How do I delete messages with Mailboxer?
To delete messages with Mailboxer, you can use the move_to_trash
method on a message. This method takes a user as an argument and moves the message to the user’s trash. For example, to move a message to the trash for a user, you can use message.move_to_trash(user)
.
How do I search for messages with Mailboxer?
Mailboxer does not provide built-in support for searching messages, but you can easily add this functionality with a gem like Ransack. With Ransack, you can add a search form to your views and then use the ransack
method in your controller to search your messages. For example, to search your received messages, you can use user.mailbox.inbox.ransack(params[:q])
.
Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.