In this multi-part tutorial, I’ll try to show off some of the cool new parts of Rails by building a simple app to send and receive e-mails. As trivial as it sounds, we’ll be using many parts of Rails showing how easy is to build non-trivial apps just with a usage of generators and a few lines of custom code.
So lets start off with generating new Rails app!
Setup
1
rails new hotmailing --database=postgresql --skip-jbuilder --skip-test --css=tailwind
As you can see, we’ll be using PostgreSQL as our databse and Tailwind as a CSS framework. The generator also automatically includes and configures hotwire-rails
which we find usage of in the later parts!
1
2
bin/rails db:create
bin/rails server
Now create development and test databases and run a server to see if everything works properly before we start adding any code. By opening localhost:3000
in your browser, you should see a nice new Rails welcome page.
Let’s add some usefull gems to our Gemfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
group :development, :test do
gem 'pry-rails'
gem 'capybara'
gem 'capybara-screenshot'
gem 'database_cleaner'
gem 'factory_bot_rails'
gem 'faker'
gem 'rspec-rails'
gem 'webdrivers', '~> 5.0', require: false
gem 'rails-controller-testing'
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
gem 'annotate', '~> 3.2'
gem 'better_errors', '~> 2.9'
gem 'letter_opener', '~> 1.7'
end
Install and configure gems we added
1
2
bundle install
bin/rails generate rspec:install
Update rails_helper.rb
to include FactoryBot syntax.
1
2
3
4
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
...
We’ll be using Rails generators a bunch, so let’s add some config to make our life easier. Create a new initializer in a config/initializers/generators.rb
. This way we skip generating a bunch of useles files we won’t need in the scope of this tutorial.
1
2
3
4
5
6
7
8
Rails.application.config.generators do |g|
g.assets false
g.helper false
g.request_specs false
g.view_specs false
g.controller_specs false
g.routing_specs false
end
Since we plan to use Rails ActionText with its neat editor, let’s install some more.
1
bin/rails action_text:install
For ActionText to work properly with images, we need to install image processing library. As in image_processing Readme, we could use ImageMagick or Vips. The later is new default for Rails, but I had problems while deploying on Heroku. We need to tell Rails to use imagemagick
instead by updating application.rb
config file.
1
2
3
4
5
6
7
8
9
...
module Hotmailing
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.active_storage.variant_processor = :mini_magick
...
Phew! We should be done with the setup for now. Let’s check if everything still works by running migrations, restarting Rails server and opening locahost:3000
again.
1
2
bin/rails db:create
bin/rails server
All fine? Now let’s actually build something!
Messages management
Lets generate our Message
s.
Message model will represent an e-mail in our application. So it should have all the e-mail properties. Instead of a plaintext content, we added content:rich_text
to make use of ActionText.
1
bin/rails g scaffold Message subject from to cc bcc content:rich_text
It’s generally a good idea to add database based validations. So open your newly generated migration file (db/migrate/xxxx_create_messages.rb
) and update it a bit to make from
and to
columns required.
1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateMessages < ActiveRecord::Migration[7.0]
def change
create_table :messages do |t|
t.string :subject
t.string :from, null: false
t.string :to, null: false
t.string :cc
t.string :bcc
t.timestamps
end
end
end
Now we should tell Rails to validate columns on client side too.
1
2
3
4
5
6
class Message < ApplicationRecord
has_rich_text :content
validates :from, presence: true
validates :to, presence: true
end
Run migrations again and open localhost:3000/messages
. You should newly generated scaffold for our messages. If the page has no CSS styles applied to it, you need to run TailwindCSS watcher as a separate server to build our CSS files.
1
bin/rails tailwindss:watch
Now you can create a new Message. So lets create one!
Don’t bother too much with attachments for now. ActionText Trix editor can attach files already, but actually sending the files is a whole different beast we tame in the next parts!
Perfect. We can create Messages, now we need to be able to actually send an e-mail.
Sending an e-mail
We start with yet another generator.
1
bin/rails g mailer Message send_message
This generates new MessageMailer
with just one method send_message
. Now lets implement the sending.
We start off with writing a test. Our e-mail needs to have it’s properties set (from
, to
etc.) and a proper body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require "rails_helper"
RSpec.describe MessageMailer, type: :mailer do
describe 'Sending a message' do
let(:message) do
create(
:message,
subject: 'Test subject',
from: 'adam@tmck.cz',
to: 'test@example.com',
cc: 'cc@example.com',
bcc: 'bcc@example.com',
content: 'my content',
reply_to: create(:message, message_id: '4db58d0dac4ffa7f0431ea8230acfc1e6f42f40d@hey.com'),
)
end
let(:mail) { MessageMailer.send_message(message.id) }
it 'renders the headers' do
expect(mail.subject).to eq 'Test subject'
expect(mail.from).to eq ['adam@tmck.cz']
expect(mail.to).to eq ['test@example.com']
expect(mail.cc).to eq ['cc@example.com']
expect(mail.bcc).to eq ['bcc@example.com']
expect(mail.header['In-Reply-To']).not_to be_nil
end
it 'renders the body' do
expect(mail.body.encoded).to match('my content')
end
end
end
In the test, we created a Message entry using Factory that was generated for us. You can check contents of the Factory in spec/factories/messages.rb
, but we don’t need to bother much with its content since we are defining all the filelds manually to properly have controll over the values we need to check in the actual test.
If you run this test (bundle exec rspec
to run the whole test suite or bundle exec rspec spec/mailers/message_spec
), you see two fails. So lets actually implement the mailer to send our Message.
1
2
3
4
5
6
7
class MessageMailer < ApplicationMailer
def send_message(message_id)
@message = Message.find(message_id)
mail subject: @message.subject, from: @message.from, to: @message.to, cc: @message.cc, bcc: @message.bcc
end
end
We need to pass a message_id
to the Mailer, find the coresponding message and set all the e-mail headers before sending it. If you run the test file now, first of the tests checking the e-mail headers should be passing now!
All that’s left is setting the correct body of the e-mail. For that, we just update the proper template files.
1
<%= @message.content %>
1
<%= @message.content %>
It’s a good idea to also update text part of the e-mail to render plain text version of the content.
1
<%= @message.content.to_plain_text %>
If you run the test file now, the results should be all nice and green!
N