Using ActionMailer outside Rails

I normally use Ruby directly for sending e-mail from scripts that need to send out status, etc. I was recently looking at the ActionMailer code that we have in a Rails project and I felt that it is very complete and provides a lot of additional support, especially when working within the context of a larger system (Rails or not).

There are examples aplenty when you want to use ActionMailer from within a Rails controller but I wanted to use it from within Ruby scripts and systems that are not bolted onto a Rails application. Given that, I set out to identify the correct way to use ActionMailer outside of Rails.

There are a few things we need for this:

  • Set up the script and gems you need
  • Create a class and a method that enables the creation of the email
  • Configure ActionMailer
  • Template(s) for the email you want to send
  • Instantiate and make a request to send the e-mail

To actually be able to run something, we do need a bit of code, a bit of initialisation and a few other things. So, it’s not possible to go through completely in sequence but we will try.

If you are a Rails programmer, a lot of this will be quite obvious to you – the only bit is that you need to define a few things youself. If you’re a pure Ruby programmer, some of the explanations could be helpful for you to figure out how it all works together.

Just for reference, I am using Ruby 2.6.6 on Windows with Rails-related gems from Rails 6.1.0. I have tried this on older versions of Rails in the past and in general, do not expect issues. If you run into problems, just ask below in the comments and let’s see if we can figure it out.

Basic Setup

The main thing that we want to use is actionmailer – so, we need to add that to our Gemfile. If you don’t have a Gemfile in the directory yet, you can create a default one by using $ bundle init to get started.

Then add actionmailer to the Gemfile and do a bundle install or do both together by doing:

> bundle add actionmailer

Once this is done, you will see something like this:


❯ bundle add actionmailer
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Using concurrent-ruby 1.1.7
Using i18n 1.8.5
Using minitest 5.14.2
Fetching tzinfo 2.0.4
Installing tzinfo 2.0.4
Fetching zeitwerk 2.4.2
Installing zeitwerk 2.4.2
Fetching activesupport 6.1.0
Installing activesupport 6.1.0
Using builder 3.2.4
Using erubi 1.10.0
Using mini_portile2 2.4.0
Using nokogiri 1.10.10 (x64-mingw32)
Using rails-dom-testing 2.0.3
Using crass 1.0.6
Fetching loofah 2.8.0
Installing loofah 2.8.0
Using rails-html-sanitizer 1.3.0
Fetching actionview 6.1.0
Installing actionview 6.1.0
Using rack 2.2.3
Using rack-test 1.1.0
Fetching actionpack 6.1.0
Installing actionpack 6.1.0
Using globalid 0.4.2
Fetching activejob 6.1.0
Installing activejob 6.1.0
Using mini_mime 1.0.2
Using mail 2.7.1
Fetching actionmailer 6.1.0
Installing actionmailer 6.1.0
Using bundler 2.1.4

If you have errors at this stage, you will need to sort them out before you proceed.

The Class and the Method

The class that uses ActionMailer needs to be derived from ActionMailer::Base so that it has access to all the good things that it can do. So, the absolute minimum you need to do is:

1
2
3
4
5
require 'action_mailer'

# Our NotificationMailer class that will send email notifications
class NotificationMailer < ActionMailer::Base
end

Of course, this does nothing yet. The next thing you need is a method that prepares the data that will be put into the email that you intend to send. For simplicity, let’s add the following:

  • from: sender email
  • to: receiver email
  • subject: the subject for the email

For the body, we directly specify that we need to render a text email with the string returned from the block.

1
2
3
4
5
6
7
class Mailer < ActionMailer::Base
  def notification(from_email)
    mail(to: 'user@example.org', from: from_email, subject: "Test Email #1") do |format|
      format.text {"Simple test mail"}
    end
  end
end

The only thing left to be done for this super-simple script is to configure the server settings. I am keeping almost everything in a single script for simplicity, so we add this to the script.

1
2
3
4
5
6
7
8
9
10
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  :address        => "smtp.gmail.com",
  :port           => 587,
  :domain         => "gmail.com",
  :authentication => :plain,
  :user_name      => "your.user@gmail.com", # Your GMail user name
  :password       => "Y0urP@ssw$#d", # Your password
  :enable_starttls_auto => true
}

Also, you need to call the code in the class to run, so you need to add this also:

1
Mailer.notification('your.user@gmail.com').deliver_now

With all this in, we should be able to send our first email. You might have a problem with sending email through GMail even though everything looks OK. Take a look at this conversation to fix that.

Now that the basic plumbing is up, we can actually start to use some of the other things that ActionMailer lets us do.

At this point, our code probably looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'action_mailer'

# Our NotificationMailer class that will send email notifications
class NotificationMailer < ActionMailer::Base
  def notification(from_email)
    mail(to: 'user@example.org', from: from_email, subject: "Test Email #1") do |format|
      format.text {"Simple test mail"}
    end
  end
end

ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  :address        => "smtp.gmail.com",
  :port           => 587,
  :domain         => "gmail.com",
  :authentication => :plain,
  :user_name      => "your.user@gmail.com", # Your GMail user name
  :password       => "Y0urP@ssw$#d", # Your password
  :enable_starttls_auto => true
}

NotificationMailer.notification('your.user@gmail.com').deliver_now

Iteration 1 – More text

The first thing that you probably want to do is send more than just a simple string. The method at format.text accepts a block and will use whatever comes back from it. You could call another method in the same class, or even add more Ruby code directly into the block ensuring that the final value returned is a string.

So, you could add this in to the body and it would work fine – sending out an email of some of the keys present in the ENV on your Ruby.

1
2
3
4
5
6
7
8
      format.text {
       text_els = []
       ENV.each_with_index {|data, idx|
         text_els << "#{idx} | #{data[0]}"
         break if idx > 9
       }
       "This was extracted from your Environment\n" + text_els.join("\n")
      }

For example, this is what I received in the body of the email when I did that:

This was extracted from your Environment
0 | ALLUSERSPROFILE
1 | APPDATA
2 | CG_BOOST_ROOT
3 | ChocolateyInstall
4 | ChocolateyLastPathUpdate
5 | CommonProgramFiles
6 | CommonProgramFiles(x86)
7 | CommonProgramW6432
8 | COMPUTERNAME
9 | ComSpec
10 | DriverData

If we want we can stop here. We have done the following:

  • Configured ActionMailer with SMTP settings
  • Created a small NotificationMailer class that has a notification method
  • We can create any text we want and send it out

From here on, it is all refinements which is where the power and conventions of ActionMailer really come in to help.

Iteration 2 – Rendering HTML

We used format.text { } for rendering text into the body of the e-mail. In the same way, you can have format.html { } to render HTML to the body of the email. You can have either one or both in the same email and the handling will depend on how the e-mail client shows them.

For example, you can add this to the method body to send out an email that has both text and HTML body parts. For me, Outlook and Thunderbird both prefer to show only the HTML version when you view the email.

1
2
3
4
5
6
7
8
9
10
11
      format.text {
       text_els = []
       ENV.each_with_index {|data, idx|
         text_els << "#{idx} | #{data[0]}"
         break if idx > 9
       }
       "This was extracted from your Environment\n" + text_els.join("\n")
      }
      format.html {
         "<h1>Test Message</h1><p>This is an HTML message</p>"
      }

This is what you see in the email body

If you view the source of the email, you will see that both text and HTML parts are included.

----==_mimepart_5fe9d7bf3ed92_2f0814f64a083646
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

This was extracted from your Environment
0 | ALLUSERSPROFILE
1 | APPDATA
2 | CG_BOOST_ROOT
3 | ChocolateyInstall
4 | ChocolateyLastPathUpdate
5 | CommonProgramFiles
6 | CommonProgramFiles(x86)
7 | CommonProgramW6432
8 | COMPUTERNAME
9 | ComSpec
10 | DriverData
----==_mimepart_5fe9d7bf3ed92_2f0814f64a083646
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><h1>Test Message</h1><p>This is an HTML message</p>
----==_mimepart_5fe9d7bf3ed92_2f0814f64a083646--

We are now able to create and send an email that has both text and HTML parts. Let’s look at a few more things next.

Iteration 3 – Templates

Of course, we can keep adding stuff to the format.text and format.html parts but if you want to create a larger email with better layout and formatting, it’s not a great idea to cram it all into that function. As with the rest of Rails, ActionMailer lets you use templates for the emails that you want to send and allows you to render the text or HTML based on a template saved in a file.

The change in the method is very simple – you just change it to format.text and/ or format.html and Rails conventions take over.

1
2
3
4
5
6
  def notification(from_email)
    mail(to: "user@example.org", from: from_email, subject: "Test Message #1") do |format|
      format.text 
      format.html
    end
  end

Of course, if you are doing this outside of Rails and are doing it for the first time, it will fail since it won’t know where to find the templates. You might see an error that looks like this:

C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actionview-6.1.0/lib/action_view/path_set.rb:48:in `find':
 Missing template notification_mailer/notification with {:locale=>[:en], :formats=>[:text], :variants=>[],
  :handlers=>[:raw, :erb, :html, :builder, :ruby]}. Searched in: (ActionView::MissingTemplate)

It basically tells us that it did not find the teamplate called notification_mailer/notification and we need to fix that.

The convention that ActionMailer follows is:

  • You set ActionMailer::Base.view_paths to point to a directory
  • Under that, it looks for a folder matching the snake_case_name of the class, e.g. notification_mailer
  • In that folder, it looks for a file that has a name such as M.F.H where
    • M = name of the method in the class (e.g. notification)
    • F = format – in our case ‘text’ or ‘html’
    • H = handler/ templating engine, e.g. ERB

So, in our case it is looking for files such as:

  • notification_mailer/notification.text.erb
  • notification_mailer/notification.html.erb

We need to add these to the directory notication_mailer but we also need to tell ActionMailer where this directory is by setting ActionMailer::Base.view_paths

Add this to the code:

ActionMailer::Base.view_paths = File.expand_path('../views/', __FILE__)

Or maybe this if you create the notification_mailer directory in this folder (‘.’)

ActionMailer::Base.view_paths = '.' #File.expand_path('.', __FILE__)

Then, add the templates. If you’re using ERB, it’s very similar to Rails and allows you a lot of flexibility in mixing Ruby code with the text or HTML that is in the file.

For the text, a sample file for notification_mailer/notification.text.erb could be something like:

1
2
3
4
5
6
Hello! This is the system notification from the server.

This is the system status:
<%= @main_message %>

Message is generated at <%= Time.now %>

You will notice a few things here:

  • It’s just text with Ruby embedded
  • You can include Ruby code directly such as <%= Time.now %>
  • You can also include instance variables that are created in the method that invokes this template – we use an instance variable called @main_message in the code which is expected to be created by the method (notification in the NotificationMailer class)

To match this, we changed the notification method to have Line 2 with the definition of @main_message

1
2
3
4
5
6
7
8
  def notification(from_email)
    @main_message = 'Everything is working fine.'
    mail(to: "user@example.org", from: from_email, subject: "Test Message #1") do |format|
      format.text 
      format.html
    end
  end
end

Similarly, a corresponding HTML template might be something like the one below.

<h1>System Status</h1>

<p>Hello! This is the system notification from the server.</p>

<p>This is the system status:<br />
<%= @main_message %></p>

<hr />
<p style='font:small; color: #aaa'>Message is generated at <%= Time.now %></p>

That will set it all up! This is how the emails show on my computer if only the text part is sent or if both HTML and text parts are sent.

ActionMailer does all the wiring to make this work – it discovers the templates that are available, it renders them, it packages them into a proper valid email, and it sends out the email for you.

Refinement 1 – Render this, not that

In a structured project, you will likely have a proper place for all the mailer templates and might have an organisation that makes sense. In a ‘small’ script that runs by itself, you might decide that you want everything in the same folder without creating elaborate sub-folders for each class and its methods. In that case, you can directly point ActionMailer to the specific template and not worry about setting ActionMailer::Base.view_paths – of course, you lose out on the automatic discovery and setup to some extent but for completeness, you should know that it is possible.

Imagine that the template email.html.erb is in the same folder as the Ruby file. You can simply change the format.text line by passing it the file to use as the template.

format.html { render './email' }

That is all it takes. Since you are rendering ‘html’, it will expect to find the file as email.html.handler (or in our case, email.html.erb) and will determine that you want to use ERB for the template and process it.

Refinement 2 – default values

You can set default values for the sender, recipient, etc. as you like. For example, you can have something like this within the class which will serve as the defaults for the to, from, subject.

1
2
3
4
5
6
7
8
9
10
11
12
13
class NotificationMailer < ActionMailer::Base
  default to: 'user@example.org'
  default from: 'your.user@gmail.com'
  default subject: 'Status is fine'

  def notification
    @main_message = 'Everything is working fine.'
    mail do |format|
      format.text 
      format.html { render './email' }
    end
  end
end

Some final notes

ActionMailer is very powerful and can allow you to really get on top of the emails you send from a large, complex system. You would have seen that our example always uses NotificationMailer.notification('your.user@gmail.com').deliver_now which instructs ActionMailer to send the mail right now rather than queuing it to be run by a background job. I don’t yet have a need to use that in our system and did not get deeper into how that would be set up correctly outside Rails.

There is still more that could be discussed about ActionMailer and how it does things, e.g. attachments, images, etc. but in this post, I have focused only on getting the email to go – including formats and templates. Maybe, there is scope for a Part 2? Let me know.

Acknowledgement

The post at https://excid3.com/blog/using-actionmailer-without-rails was helpful when I started.

These are the base references if you want to read more:

  • https://api.rubyonrails.org/classes/ActionMailer/Base.html
  • https://guides.rubyonrails.org/action_mailer_basics.html

As always, this is for me to be able to remember how to do it but if it helps someone, that’s great! Also, if you have some comments, please add below so that I can reflect changes to the code.

comments powered by Disqus