Email and Text from Rails
Tags: rails, beginners, ruby
Published: 2021-01-14 22:40:40

Hey Everyone!  

I've learned, and applied, a lot in the last few months, but there was a big glaring gap that I just hadn't addressed yet, and that was messaging users outside of my own app.  So, to finally get this addressed, I wanted to add functionality to my recipes app to send a shopping list to my users by email and/or text messaging.  

Email was pretty obvious, there are classes built into Rails called ActionMailers - these allow you to build email templates similar to how you would build a view in a Rails site, and then sends out that email.  Text messaging required a little searching, but ultimately there was a gem that was perfect for my purposes.  Here, I'll go through how to set up the emailing functionality in Rails, and then how that will translate into SMS capability using the [sms-easy](https://github.com/preston/sms-easy) gem.

This is broken down into 4 high-level steps:
1. [Configuring Rails](#ConfigureRails)
2. [Creating your Mailer](#CreateMailer)
3. [Setting up your workflow](#SetupWorkflow)
4. [Converting the Mailer to use an SMS address](#ConvertToSMS)

## Sending Emails

Like I said above, to send emails from Rails, there is a built in functionality called ActionMailers.

#### <a name="ConfigureRails">Configure Rails Environment</a>

The first step for using ActionMailers is to get them configured in our app.  First, we'll add some configuration to each of our environments.  We need to do three things here.
1. Tell Rails how we want to deliver our mailer.  In this case, using SMTP: `config.action_mailer.delivery_method = :smtp`.
2. Set up default URL options to say what URL the email will be sent from and how it will be sent ```
host = 'localhost:3000' // this needs to be set to your actual site url in your production environment
config.action_mailer.default_url_options = { :host => host, protocol: 'http' }
```

3. Configure the SMTP settings so Rails can log into the correct service, with the proper authorization, and actually send the email.  The below is for Gmail, but there are plenty of other options, such as Outlook, if you don't want to use Gmail just Google "Outlook SMTP Settings":
```
  config.action_mailer.smtp_settings = {
    :address              => "smtp.gmail.com",
    :port                 => 587,
    :user_name            => ENV["Email_Username"],
    :password             => ENV["Email_Password"],
    :authentication       => "plain",
    :enable_starttls_auto => true
  }
```

#### Setup the Gmail Account

Notice the lines in the above code that use `ENV[]`, these point to an environment variable.  After all, we don't want to have our email login info out there for the whole world to see.  So, the next step is to create these in our .env file (this requires the gem [dotenv-rails](https://www.rubydoc.info/gems/dotenv-rails/2.7.6).

Of course, to get the `:user_name` and `:password`, we'll need to create a Gmail account.  The important thing to keep in mind for this is that, once your account is created, you'll need to go to `Settings -> Security -> Signing in to Google` to set up 2-step verification, and then create an App Password to allow your app, a 3rd party app from Google's standpoint, to access your account.  This App Password will be the password you include in the smtp_settings above, not your personal password.

#### <a name="CreateMailer">Create your Mailer</a>

To create the Mailer, you could manually add the files needed, but Rails comes with a handy-dandy generator to do a lot of the setup for us.  Run `rails generate mailer shoplist_mailer` and Rails will create a mailer file `app/mailers/shoplist_mailer.rb` and a views folder `app/views/shoplist_mailer`, along with some testing files.

#### The Mailer

First, make sure that `application_mailer.rb` has the correct defaults set up:
```
class ApplicationMailer < ActionMailer::Base
  default from: "noreply.whats.cookin@gmail.com"
  layout 'mailer'
end
```

Then, within `app/mailers/shoplist_mailer.rb` we will add a method to create an email.  This method will receive the arguments from your controller as params, and it needs to convert them to instance variables to be passed into a view. It will then use ApplicationMailer's built in `#mail` method to pull the view (which we'll set up in the next step) as the template and pass in those instance variables. It should end up looking like this:
```
class ShoplistMailer < ApplicationMailer 
  def new_list_email
    @recipe_name = params[:recipe]
    @user = params[:user]
    @shoplist = params[:shoplist]
    mail(to: @user.email, subject: "What's Cookin' Shopping List")
  end
end
```

#### The View

Now that our Mailer is set up to find a view and pass it information, we need to actually create the view.  We're going to create 2 files under `app/views/shoplist_mailer`, the file names need to match the method name in your Mailer (in this case `new_list_email`), and they will be nearly identical except that one will be an html file and the other a text file.  What we're doing here is setting up our primary (html) view, and a backup (text) view in case there's an issue with sending html.  You shouldn't need to worry about any of the html setup as it should already be done for you in `app/views/layouts/mailer.html.erb`.  

This is your email template, so make it your own way, but here is mine as an example:

```
// app/views/shoplist_mailer/new_list_email.html.erb

<h3>Hello <%= @user.name %>,</h3>
<p>
  Here is your shopping list for <strong><%= @recipe_name %></strong>:
</p>
<ul><% @shoplist.each do |item| %>
  <li><%= item %></li>
<% end %></ul>

<p>Thank you for using <strong>What's Cookin'</strong>. Enjoy your meal!</p>
```

```
// app/views/shoplist_mailer/new_list_email.text.erb

Hello <%= @user.name %>,

Here is your shopping list for <%= @recipe_name %>:

<% @shoplist.each do |item| %>
  <%= item %>
<% end %>

Thank you for using What's Cookin'. Enjoy your meal!
```

And that's it, on to the next piece.

#### <a name="SetupWorkflow">Setup Workflow</a>

Okay, now that we have the email settings configured and our mailers created, all that's left to do is get your workflow set up.  How does it go from a user clicking a button on the page, to sending that email out? 

**Note that your use case may not be exactly the same as mine, so the steps may be a little different.  I've tried to note some areas to watch for this, but it's up to you to figure out your app's specific workflow.*

* Add a new Route
*(this step may not be necessary - for example, if you just need an auto-notification upon user signup it would NOT require a new route)*
```
resources :recipes  // for the basic Recipe model
post '/recipes/send_shoplist' => 'recipes#send_shoplist'  // specific route for sending a shopping list
```

* Add a new Action to Controller
*(this step may not be necessary - similar to the route, an auto-notification wouldn't require its own controller action)*
```
// app/controllers/recipes_controller.rb

def send_shoplist
  // controller code goes here
end
```

* Instantiate the Mailer from your Controller and pass in args - this is the step that actually tells Rails to send the email
```
// create args from params coming from fetch call
@shoplist = params[:shoplist]
@recipe = params[:recipe]

// pass args to Mailer and call delivery method
ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user).new_list_email.deliver_now
render json: "Shopping List Successfully Delivered".to_json
```

* Connect frontend:

**this entire step may not be necessary - if the user doesn't need to explicitly instruct the app to send them data (ex. auto-notification) there wouldn't necessarily need to be a dedicated call to your API - hence not needing a route and controller action*

Here, we need to create a function containing a POST request to the correct Controller action and we'll include whatever data we need for the email in the body (in this case, which recipe and what ingredients were chosen by user). It's also important to keep in mind that the backend will need to know which user to send the email to.  In my case, I could identify the `current_user` via my authorization flow, but you may need to pass a `user_id` as a param instead.  Once the function is created, we'll attach it as an eventListener somewhere on our page if it's designed as a manual user action.
```
// code doesn't include assigning recipe and list variables
function sendShopList() {
    fetch(`${RECIPES_URL}/send_shoplist`, {
        method: 'POST',
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": // dependent on auth strategy
        },
        mode: 'cors',
        credentials: "include",
        body: JSON.stringify({recipe: recipe, shoplist: list})
    })
    .then(resp => resp.json())
    .then(msg => alert(msg))
    .catch(function(error) {
        alert("Shopping List Could Not Be Delivered - Error Unknown")
        console.log(error)
    })
}

// Attach function to a button's eventListener
shopListButton.addEventListener("click", () => {sendShopList()})
```


## <a name="ConvertToSMS">Sending Text Messages (SMS)</a>

Now that we have our ActionMailers set up, let's add texting functionality as well.  We want our users to be able to choose whether the notification is sent to their email, phone, or both. The good news is that adding the texting functionality is almost effortless.  *Note that setting up fields to gather the needed data is outside the scope of this article, but is very important.*

#### How It Works

First thing's first, let's talk about how this actually works.  Like everything else in coding, there are multiple ways to go about texting users.  If you are building a commercial app, you may want to look at a service like Twilio to simplify things and make it fool-proof.  For our app, we're not concerned about being completely sure that 100% of users have a texting option, so we're going to implement a free method of texting instead.  

As a reminder, the gem we're going to use for this is called `sms-easy` so make sure that it's installed in your Gemfile.  This gem takes advantage of mobile carriers' email-to-text services.  Basically, if you send an email to the correct address, it will route to the user as an SMS message instead.  So the `sms-easy` gem essentially tracks these special addresses and performs a lookup and transformation based on the phone number and carrier.  As an aside, it also has functionality to directly send the message from the controller, but we don't want to use that, because we want the text to match the email template that our ActionMailers are already sending out.

So, we need to do 2 things:
1. Get the special email-to-text address.
2. Route our ActionMailer email to that special address.

Let's look at how it's done.

#### Implementation

First, we need to update our Mailer from before so that it's flexible enough to change the "to:" address based on passed in input.  All we need to do is modify one line:
```
// FROM
mail(to: @user.email, subject: "What's Cookin' Shopping List")

//TO
mail(to: params[:recipient], subject: "What's Cookin' Shopping List")
```

Once that's set, we can change the Mailer call in our Controller so that it provides that "to:" value as a parameter:
```
// FROM
ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user).new_list_email.deliver_now

// TO
ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: current_user.email).new_list_email.deliver_now
```

Now, we need to add a control flow to our Controller based on the user's contact preference so that the proper "to:" parameter gets passed.  You can certainly to an if...else statement for this, but this is a great use case for a case statement as well:
```
case current_user.contactPreference
when 'email'
  // Code to send Mailer via email
when 'text'
  // Code to send Mailer via text
when 'both'
  // Code to send Mailer via email
  // Code to send Mailer via text
else
  // Can't send option
end
```

Finally, let's fill in that code.  For the email case, we already have the code completed in the previous step, so let's throw it in and add a response message so our app knows everything worked out okay:
```
when 'email'
  ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: current_user.email).new_list_email.deliver_now
  render json: "Shopping List Successfully Delivered".to_json
```

For the text case, we need to determine what that special carrier address is and pass that address as the recipient parameter instead of the email.  This is where we'll make use of our `sms-easy` gem's `sms_address` method.  The method returns our special address, so let's assign to a variable (`recipient`) and then pass that into our Mailer call: 
```
when 'text'
  recipient = SMSEasy::Client.sms_address(current_user.phone, current_user.carrier)
  ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: recipient).new_list_email.deliver_now
  render json: "Shopping List Successfully Delivered".to_json
```

For the both case, we'll assign the recipient variable the same way, and then we'll simply include both of the Mailer calls.  The final case, of course, is when there is no match.  Normally, this is an error, so we could use `begin...raise...rescue` but for my implementation, I preferred to simplify by having my Controller send a message to the front-end and then simply do nothing.  All together, the modified #send_shoplist method looks like this:
```
def send_shoplist
  # send selected recipe-ingredients as shoplist
  @shoplist = params[:shoplist]
  @recipe = params[:recipe]

  # send shoplist via chosen contact preference
  case current_user.contactPreference
  when 'email'
    ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: current_user.email).new_list_email.deliver_now
    render json: "Shopping List Successfully Delivered".to_json
  when 'text'
    recipient = SMSEasy::Client.sms_address(current_user.phone, current_user.carrier)
    ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: recipient).new_list_email.deliver_now
    render json: "Shopping List Successfully Delivered".to_json
  when 'both'
    recipient = SMSEasy::Client.sms_address(current_user.phone, current_user.carrier)
    ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: current_user.email).new_list_email.deliver_now
    ShoplistMailer.with(recipe: @recipe, shoplist: @shoplist, user: current_user, recipient: recipient).new_list_email.deliver_now
    render json: "Shopping List Successfully Delivered".to_json
  else
    render json: "Shopping List Could Not Be Sent - No Valid Contact Preference Selected".to_json
  end
end
```

And that's it! Now our backend is set up so that it will email *and/or* text the user, depending on their contact preference selection.