This hopefully soon-to-be series was started quite a while ago when I was exploring new Rails features. Especially Turbo and ActionMailer. I finished the demo app and started writing articles while tidying up code. As it happens, I never finished and published the series. Almost two years passed and here you are, reading one of the first two parts that were finished before I abandoned the idea of having a blog. This hopefully changes now with more parts and different topics incoming!
After an anticlimactic ending to the first part where we just generated scaffold for our Messages after a long setup, let’s finally send an e-mail!
TLDR: You can find the code in this GitHub repository. Every part has a corresponding commit.
Sending an e-mail
We start with yet another generator, this time using Rails ActionMailer.
1
bin/rails g mailer Message send_message
This generates new MessageMailer
with just one method send_message
. Now let’s implement the sending. And by implement I mean lets write a test first!
Our e-mail must have its headers set (from
, to
etc.) and a proper body. So we test that.
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
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',
)
end
let(:mail) { MessageMailer.with(message: message).send_message }
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']
end
it 'renders the body' do
expect(mail.body.encoded).to match('my content')
end
end
end
In the spec, we created a Message entry using Factory that was generated for us. You can check the Factory in spec/factories/messages.rb
, but we don’t need to bother much with its content since we are defining all the fields manually to have control 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.rb
), you see two fails. So lets actually implement the mailer so the tests pass.
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
to the Mailer 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 %>
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!
This means the MessageMailer
should be correctly set and ready to send our e-mails. To actually do that, we need to call the mailer after Message was created. Also for easier testing, I created a new action in MessagesController
called send_email
. This way you can easily re-send every message on demand.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MessagesController < ApplicationController
before_action :set_message, only: %i[ send_email show edit update destroy ]
...
def create
@message = Message.new(message_params)
if @message.save
MessageMailer.send_message(@message.id).deliver_now
redirect_to @message, notice: "Message was successfully created."
else
render :new, status: :unprocessable_entity
end
end
# for testing purposes
def send_email
MessageMailer.send_message(@message.id).deliver_now
redirect_back fallback_location: messages_url, notice: 'Message was send.'
end
...
end
To be able to call this newly created action, we need to update our routes.
1
2
3
4
5
6
7
Rails.application.routes.draw do
resources :messages do
member do
post :send_email
end
end
end
And finally, to be able to use new send_email
action, you need to add a link to the template.
1
2
3
4
5
6
7
8
9
...
<% if action_name != "show" %>
<%= link_to "Show this message", message, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= button_to "Send again", send_email_message_path(message), method: :post, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<hr class="mt-6">
<% end %>
...
If you’d create a new Message right now, it’d seem like no e-mail was sent (its contents will be displayed in the server log). For development purposes, it’s a good idea to use letter_opener
gem which we already added to the Gemfile. Now we just need to tell Rails to actually use it. So go ahead and add those two lines to your development environment config. And don’t forget to restart Rails server after!
1
2
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
letter_opener
opens every e-mail in a new browser tab, so you can easily see and check what are you sending.
Go ahead. Send your first e-mail!
Now, that we have first noticeable outcome from our app, it’s time to write some tests for our new controller code.
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
require 'rails_helper'
RSpec.describe MessagesController, type: :controller do
describe 'POST /messages' do
it 'creates a new Message' do
message_params = attributes_for(:message)
expect {
post :create, params: { message: message_params }
}.to change { Message.count }.by(1)
end
it 'sends an e-mail' do
message_params = attributes_for(:message)
expect {
post :create, params: { message: message_params }
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
describe 'POST /messages/:id/send_email' do
it 'sends an e-mail' do
message = create(:message)
expect {
post :send_email, params: { id: message.id }
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
end
First, we test create
action which now does two things. First is to create a new database entry if provided with valid Message parameters. We use our factory to retrieve valid params using attributes_for
. Second thing to test is whether we actually send an e-mail after new Message is created.
If you want to be fancy, you can also throw in a test for send_email
action. The action is only for our testing purposes, but one more test never hurts.
In the next part, we add support for sending e-mails with attachments.
Comments powered by Disqus.