Home Simple e-mail service with Rails - Part 2: Sending first e-mail
Post
Cancel

Simple e-mail service with Rails - Part 2: Sending first e-mail

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.

This post is licensed under CC BY 4.0 by the author.
Trending Tags
Contents

Simple e-mail service with Rails - Part 1: Setup

Simple e-mail service with Rails - Part 3: Adding attachments

Comments powered by Disqus.

Trending Tags