In this lab, we’re going to create a contact form that visitors to our web site can complete. When they submit the contact form, we’ll both store it in the database and generate an email to ourselves.
1. Creating the contact form
a. Scaffolding the contact model
If we only wanted to send an email when the contact form was filled in, we wouldn’t really need to use an Active Record model and save it to the database. But it’s actually easier to use all the scaffolding and other support that Active Record provides, and it is handy to have the messages stored in the database in case any of them are lost in the email maze.
So let’s start by creating a scaffold for contacts:
ruby script/generate scaffold contact name:string email:string message:text subject:string
As in the previous labs, this will generate the model, controller, views, and test files.
Now run the migration, as in previous labs, by migrating the database to the current version (rake db:migrate
if you’re working from the command line.)
Let’s add some simple validations, since we don’t want any contact messages that don’t have all the fields filled in. Add this line to the file models/contact.rb:
validates_presence_of :name, :email, :subject, :message
You’d probably want more sophisticated validations in the real world, but this is a start.
b. Hooking up the contact form
First delete the annoying layout file the scaffold generates (views/layouts/contact.html.erb), so the scaffolded views will use our standard layout.
We already have a contact button in the navigation bar, but it is pointing to one of our initial static pages, and now we want it to point to the new contact form. So find the link to “Contact” in views/layouts/application.html.erb, and change it to:
<li><%= link_to 'Contact', {:controller => 'contacts', :action => 'new'}, :id => 'contact_nav' %></li>
Or, to use the path shortcut that map.resources provides:
<li><%= link_to 'Contact', new_contact_path, :id => 'contact_nav' %></li>
These both do the same thing. The first is more explicit, the second is the “modern” Rails way of doing things.
Now you should be able to click on the Contact button in the nav bar and see the new contact form. Go ahead and create a couple of messages so we can see them in a minute in the admin interface.
Since we’ve hijacked a form that was meant to be part of the admin interface and are using it for a user-facing form, we need to change some of the linkages. Delete this line from the end of views/contacts/new.html.erb, since we don’t want visitors to try to get to the list of all messages:
<%= link_to 'Back', contacts_path %>
Finally, when a contact form is successfully saved, we don’t want to redirect to the normal “show” action, as the scaffolded pages are set up to do. Instead, let’s redirect to the home page. Open the file controllers/contacts_controller.rb, and find this section of code in the create action:
if @contact.save flash[:notice] = 'Contact was successfully created.' format.html { redirect_to(@contact) }
Let’s provide a more appropriate flash message, and change the redirect, so the code now reads as follows:
if @contact.save flash[:notice] = 'Thanks for your message. It has been delivered.' format.html { redirect_to :controller => 'static', :action => 'home' }
c. Setting up the contact admin
We don’t want site visitors being able to view the list of contact messages, even if they guess the URL for that page, so we need to add authentication to the contacts controller. But we do need the “new” action in this controller to be accessible to visitors, as well as the “create” action that is invoked by the form when it is submitted. So add the following line to the start of controllers/contacts_controller.rb:
before_filter :login_required, :except => [:new, :create]
We also want to be able to easily access the list of contacts from our admin page, so add this line to the file views/static/admin.html.erb:
<li><%= link_to 'Contacts Admin', contacts_path %></li>
Now you can click the Admin button to access the admin page (you’ll have to log in), and then click the Contacts Admin link to see the list of messages that have been submitted.
2. Sending the messages via email
Now we need to create the linkage to the mail system, so new contact messages will be sent to us via email as well as added to the database.
a. Generating the mailer
We’ve used the scaffold and migration generators in previous labs. There’s another generator for mailers, which is what we’re going to use here. Enter the following command in your terminal, at the root of your app:
ruby script/generate mailer contact_mailer contact_form
(This generator actually works from within NetBeans, so you can use its GUI version of you prefer.)
This creates a mailer model, called contact_mailer and its associated view, which we’ve called contact_form. You can enter multiple views here, for example if you had different kinds of contact forms and wanted to format each message differently.
b. Configuring the mailer model
Once you’ve run the generator, you’ll find a file contact_mailer.rb in your models folder. Open that file, and this is what you should find there:
def contact_form(sent_at = Time.now) @subject = 'ContactMailer#contact_form' @body = {} @recipients = '' @from = '' @sent_on = sent_at @headers = {} end
Note that this method is in a class that inherits from ActionMailer::Base, rather than ActiveRecord::Base like all your other models, so it has a different set of characteristics and capabilities.
We need to fill in some of the blanks in this method. First, we need to pass to this method the parameters from our form, so modify the first line to read:
def contact_form(name, email, subject, message, sent_at = Time.now)
Note that the one parameter the default method has is sent_at = Time.now
. When a parameter has an assignment to a value like this, it makes it optional to actually pass that parameter. So this is set up so we can pass a sent_at value if we want, but if we don’t, the current time will be used (which is just fine in most cases).
Now we need to use these parameters appropriately in the body of the method. First, the subject:
@subject = subject
Simple enough, we’re just taking the subject from the variable that we passed in as a parameter, and assigning it to the instance variable that will be passed to the view. Mailer views are special, so this subject text will automatically become the actual subject line of the email message.
Now for the body, which is a little more obscure. This line should read:
@body = {:name => name, :message => message}
Mailers are odd, and this is one of the odd bits of syntax. The @body instance variable is set to a hash, and each key in the hash is turned into an instance variable of that name. So we’re creating two instance variables, for use in the body of the message. You might think that we should be able to just add lines of text to explicitly set the name and message instance variables, just as you would in a controller, but remember this is a mailer model, not a controller. The only variables that will be available to our mailer view for explicit use are the ones we define in this body hash. All the other variables are used to create the message headers.
Now we need to set the recipient:
@recipients = CONTACT_RECIPIENT
We’ve chosen here to set it to a constant, rather than a literal value, because we don’t want a specific email address, which may change in time, deep in our code files. Add this line in your config/environments/development.rb file:
CONTACT_RECIPIENT = 'yourname@yourdomain.com'
Another benefit of this approach is that you can set a different address in your production.rb environment file. For example, in development, you’ll probably want contact messages to go to the developer, but in production, they should go to an administrative or sales person.
Note that if you want messages to go to more than one person, you can provide an array of email addresses:
CONTACT_RECIPIENT = ['email1@domain.com', 'email2@domain.com', 'email3@domain.com']
And all of these recipients will be included on the “to” line. You can also set “cc” recipients by adding “@cc = COPY_RECIPIENT” in the model.
Finally, we need to set the “from” address. That’s been passed in as a parameter from our form, so this line should simply read:
@from = email
That’s it for the mailer model. All it is really doing it taking parameters that we’ll pass to it from the contact form (when we get to modifying the controller), puts those parameters into the right places for the mailer view, and sets the recipient address. Now on to the view.
c. Creating the mailer view
You’ll find an empty (well, with a couple lines of useless boilerplate) view in views/contact_mailer/contact_form.erb. This is the view that will get invoked when we use the mailer model to request delivery of a message.
The instance variables passed to this view are those we defined in the “@body” variable in the model. Now we just use those however we want, much like in a regular view, but remember that we’re generating a plain-text email here, so there’s no HTML code required. Here’s a simple view:
Email from your web site From: <%= @name %> Message: <%= @message %>
c. Configuring how mail is sent
We’re almost ready to generate email, but first we need to tell Rails how it supposed to access the mail system. You can configure it to use an SMTP server, or you can tell it to use sendmail, which is the more common approach if you’re on a unix-type system. The details can be a little tricky, but you should be able to get help from your hosting company or system administrator. Here’s the configuration that works on the Joyent server that you have:
ActionMailer::Base.delivery_method = :sendmail ActionMailer::Base.sendmail_settings = { :location => '/opt/csw/sbin/sendmail', :arguments => '-t' } ActionMailer::Base.default_charset = 'utf-8'
We’re providing two parameters to make sendmail work. The first is the path to the executable file, which is specific to the way the Joyent servers are set up. The second, which you’d probably want for any system configuration, is the -t
parameter, which tells sendmail to extract the recipient addresses from the message headers.
In the sample app, we put this in a new file, called mail.rb, which we put in config/initializers. You might want to to put it in environments/production.rb instead, so you could then put a different configuration in environments/development.rb. For example, here’s the configuration for sending email via SMTP over a Comcast connection:
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { :address => 'smtp.comcast.net', :port => 25, :domain => 'comcast.net' } ActionMailer::Base.default_charset = 'utf-8'
SMTP servers are picky about what mail they will accept, so you’ll have to be on a Comcast internet connection for the above settings to work. If your SMTP server requires authentication, you may need to add login and password settings to the smtp_settings hash.
If you’re having trouble getting your mail to go out in development mode, you may want to change this line in your development environment:
config.action_mailer.raise_delivery_errors = false
And set it to true
instead. If you don’t do that, deliver failures are silently ignored in development mode.
d. Generating the email from the contact form
With all this setup behind us, actually delivering the mail is easy. Open the file controllers/contacts_controller.rb, and add this line immediately after “if @contact.save” (we only want to deliver the mail if the save was successful, indicating that the validations passed):
ContactMailer.deliver_contact_form(@contact.name, @contact.email, @contact.subject, @contact.message)
We’re invoking the contact_form method in the ContactMailer class. This is the class we edited at the start of this lab. We pass to that method the data from the form; the order of these parameters is determined by the order in which we listed them in the method definition.
There’s just one strange thing here: the method we’re invoking is “deliver_contact_form”, not “contact_form”. Rails creates this method name for us, and we have to use it. Just one of those oddities to get used to. (This odd syntax exists to support another option: you can call the create_contact_form method, which will produce the mail object, ready to be sent, but won’t actually send it.)
You can now try creating a contact message, and it should be sent. Depending on what system you’re running on, and how your mail delivery is configured, it may not actually go out, but you can look at the Rails log, and it will show the email that was generated, even if it couldn’t contact a mail system to actually send it.
3. Blocking spam
As an example of integrating a third-party web service, we’ll use the Defensio service to check our contact messages to see if they are spam.
a. Installing the plugin
As with most web services, there’s a Rails plugin available that simplifies integrating the Defensio service. Install it as follows:
ruby script/plugin install http://code.macournoyer.com/svn/plugins/defensio/
b. Setting up the contact model to use Defensio
We need to tell our comment model to use the Defensio plugin. The basic invocation, which you should paste into the file models/contact.rb, is:
acts_as_defensio_comment :fields => {:content => :message, :author => :name, :author_email => :email, :title => :subject }
This tells Defensio that the field with the content we want to check is the message, and also points to the other relevant fields.
Because it is designed for use with a blog, there’s a little trick we need to use to make Defensio work in our situation, in which there is no parent article to a message. Add the following code to the contact.rb. model:
class StubArticle attr_accessor :created_at def initialize(date = Time.now) @created_at = date end # stubbing acts_as_defensio_article methods def self.defensio_fields(field) field end end def article StubArticle.new(self.created_at) end
What we’ve done here is to create a phony article, with no contents, that will act as the parent article that the Defensio plugin expects to find. We created a class, StubArticle, to provide the behaviors expected of an Article object, and then we defined an article method to create an instance of that class.
c. Using Defensio in the contacts controller
Now that this is in place, all we need to do to check for spam is to modify the section of code following if @contact.save
in the create method of the contacts_controller to test the spam? method of the contact object, which is provided by the Defensio plugin, to see if the message is believed to be spam:
if @contact.save if @contact.spam? flash[:notice] = 'Your message is Under Review.' else ContactMailer.deliver_contact_form(@contact.name, @contact.email, @contact.subject, @contact.message) flash[:notice] = 'Thanks for your message. It has been delivered.' end format.html { redirect_to :controller => 'static', :action => 'home' } else ...
If the save is successful, the we test to see if the message has been rated as spam. If so, we don’t generate an email, and we display an appropriate message. If not, we proceed as before.
d. Setting up the Defensio API key
To use the Defensio service, you need to sign up for an API key. For testing, you can use ours. If you’re going to use the service, sign up for your own key at http://defensio.com//, as ours will be deactivated in a month or two.
The Defensio plugin looks for a file called defensio.yml in the config folder. The plugin installs a template version of this file, but the API keys in that version won’t work. So open that file and paste the following information into it, overwriting what’s there:
development: api_key: 47dcc80bc580d10b3c2b3eb4ed2841b1 owner_url: http://lab.railsquickstart.com/ test: api_key: 47dcc80bc580d10b3c2b3eb4ed2841b1 owner_url: http://lab.railsquickstart.com/ production: api_key: 47dcc80bc580d10b3c2b3eb4ed2841b1 owner_url: http://lab.railsquickstart.com/
e. Adding the Defensio columns to the contact model
Defensio needs to store a few things in the model for each contact: a boolean value that indicates whether or not a message is spam, a “spaminess” rating that gives the estimated likelihood that a comment is spam, and a “signature” field that Defensio uses to identify the message on future calls to the service.
To add these fields to your content model, create a new migration, called something like AddDefensioFields, and put the following in the self.up block in the new migration file:
add_column :contacts, :spam, :boolean, :default => false add_column :contacts, :spaminess, :float add_column :contacts, :signature, :string
And to keep things tidy, add the following to the self.down block in the migration file:
remove_column :contacts, :spam remove_column :contacts, :spaminess remove_column :contacts, :signature
Now run the migration, and restart your server so the plugin can initialize itself. You should now be able to submit contact messages just as before. You may not notice anything different yet, but when the contact message is saved, it is also sent to the Defensio service, and the service responds with the spam flag and the spamminess value, which are stored in the database. All this happens automatically because of the acts_as_defensio_comment
statement that we added to the contact model.
If you get any error messages at this point, check all the Defensio-related code.
f. Adding Defensio support to the contact admin pages
The last piece of the puzzle is displaying the values created by Defensio in the contact admin list, and providing a couple links to send information to Defensio.
Open the file views/contacts/index.html.erb, and add these two lines at the end of the list of headers (the last of which is “Subject” until you make this addition):
<th>Spam?</th> <th>Spam Score</th>
Now we need to add the columns that display this information. In the block that is displaying the information for each contact, add these two lines after the one that displays contact.subject:
<td><%=h contact.spam? ? "TRUE" : "FALSE" %></td> <td><%=h contact.spaminess %></td>
The first of these displays TRUE or FALSE depending on the state of the spam attribute. The second displays the “spaminess” value.
Finally, all the follow lines just before the </tr>
element, after the link to the Destroy action:
<td><%= link_to 'Report Spam', {:action => :report_as_spam, :id => contact} %></td> <td><%= link_to 'Report Ham', {:action => :report_as_ham, :id => contact} %></td>
The first link allows us to flag a message as spam; the second allows us to flag it as “ham” (i.e., not spam).
g. Adding the actions
Just one more step. We need to add the actions to the contacts controller to deal with reporting of messages as spam or ham from the admin page we just modified. Add these two methods to the end of controllers/contacts_controller.rb:
def report_as_spam @contact = Contact.find(params[:id]) @contact.report_as_spam if @contact redirect_to(contacts_url) end def report_as_ham @contact = Contact.find(params[:id]) @contact.report_as_ham if @contact redirect_to(contacts_url) end
These are both very simple methods, since all they need to do is invoke the corresponding method on the contact object. Those methods are provided by the Defensio plugin.
h. Testing it out
You’re done! Go ahead and create a few contact messages. The go to the Contacts admin page, and you’ll see how the messages were rates in terms of their spaminess. Click the Report Spam link for one of them, and you’ll see its spam state change to TRUE, and its spaminess score go to 1.0. Click the Report Ham link for another, and its spaminess score will go to zero.
After some training, Defensio will begin recognizing messages as spam and marking them as such automatically when they are saved, which prevents them from being emailed to you.
Next Steps
As you extend this code, there lots of things you can do to:
- Improve the contact form validations. You might require a minimum length for the name, subject, and message, and validate the email address with a regular expression that checks its format.
- The HTML/CSS layout of the contact form could use a lot of help.
- If Defensio marks a message as spam, and you later mark it as Ham in the admin interface, you might want to send it as an email, since the initial sending of the email was inhibited when it was first marked as spam.
- If the spaminess value is high enough, you might want to not even store the message in your database, but simply discard it.