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

Simple e-mail service with Rails - Part 1: Sending a first e-mail

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 Messages.

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

New Message form

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!

Created Message

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

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