Tuesday, September 18, 2007

Building a Ruby on Rails Contact Form

------------------------------------------------------------------------------------
CONTENTS
------------------------------------------------------------------------------------

How to build a contact form using ActiveRecord and ActionMailer

Introduction
Background..............................................general information
How To Do It............................................step-by-step instructions
General Notes...........................................programming issues and gotchas
Original Sources.......................................where I found the basic info



------------------------------------------------------------------------------------
INTRODUCTION
------------------------------------------------------------------------------------

Purpose: To create a contact form under Ruby on Rails allowing a site visitor to directly contact you from your web site. Using this form should prevent various automated software thingies from spamming you, as will happen if you go the usual "mailto:foo@bar.com" route.

These notes are based on "Simple Contact Form using a Table-less Model" found at www.bphogan.com/learn/pdf/tableless_contact_form.pdf (See more notes at the bottom of this piece.)

The point is to use the Rails model mechanism to handle error messages, but without having a database (or skipping the database interactions if you do have a database, because we don't really want to muck around with that just for sending email messages).


------------------------------------------------------------------------------------
BACKGROUND
------------------------------------------------------------------------------------

My host is Site5 (www.site5.com), and these notes apply to their system.

Start with a new application so you can play with it and keep it from interfering with any real projects you have. When you're ready you can do it over in production mode on your web site.

My version (what you see here) was developed on Windows. Change your paths and commands as needed to reflect the requirements of your own operating system.

Also, I'm assuming that you're first working in development mode on your local computer. Therefore you'll probably be using Webrick as a server and your copy of the file development.rb (config\environments\development.rb) will include the following setting:
 config.action_mailer.raise_delivery_errors = false

In other words, your application won't care that your email doesn't actually get sent, and in a development environment it probably shouldn't. Other people have managed to jigger their desktop systems to send email from a Rails application out through their home internet service providers. I don't care about that, so I'm sticking with the default setup where we just fake it (a.k.a a development setup).



------------------------------------------------------------------------------------
HOW TO DO IT
------------------------------------------------------------------------------------
  1. Here's a tree view of the directories and files you'll end up having for this toy application. It's a standard Ruby on Rails application but only the relevant directories and files are shown here.

    Note that this is a minimal, bare-bones "application". It has a home page (ix.rhtml), a contact page with a form on it (contact.rhtml), and a page which echoes the message, if it got sent (sent.rhtml).

  2. Mailtome
    |
    \---test
    |
    +---app
    | +---controllers
    | | application.rb
    | | home_controller.rb
    | |
    | +---helpers
    | | application_helper.rb
    | | home_helper.rb
    | |
    | \---views
    | +---home
    | | contact.rhtml
    | | ix.rhtml
    | | sent.rhtml
    | |
    | +---layouts
    | |
    | \---mailtome
    | contact_message.rhtml
    |
    +---config
    | | config.yml
    | | environment.rb
    | | routes.rb
    | |
    | \---environments
    | development.rb
    | production.rb
    | test.rb
    |
    +---log
    | development.log
    |
    +---public
    x_index.html (deleted by renaming; was index.html)

  3. I started in a new directory I called "Mailtome". Use whatever you want -- it doesn't matter (maybe -- see note later on). You'll end up working relative to the application's root. For this document it's called "test". So let's create the application called "test":
     C:\...\Mailtome> rails test

  4. Now switch to the application root (the test directory) and create a controller called "home":
     C:\...\Mailtome\test> ruby script/generate controller home

  5. Start the WEBrick server from the application root:
     C:\...\Mailtome\test> ruby script/server

  6. Open the application's default page in your browser by entering the standard URL (http://localhost:3000/) and verify that things are working. If so, you will get the usual "Welcome aboard You're riding the Rails!" page. You've seen this before. No surprises there.

  7. Now reconfigure your environment to make experimentation a little easier.

    1. Copy public\index.html to app\views\home\ix.rhtml (Note the change in file name and extension from "index.html" to "ix.rhtml".)

    2. Rename public\index.html to hide it from the application, or just delete it. Your choice.

    3. Configure the file config\routes.rb

      1. Open routes.rb

      2. Find this section at the bottom:
         # You can have the root of your site routed by hooking up ''
        # -- just remember to delete public\index.html.
        # map.connect '', :controller => "welcome"

      3. Assuming that your controller is "home", and the desired action is "ix", set the last two lines as shown here:
         # You can have the root of your site routed by hooking up ''
        # -- just remember to delete public\index.html.
        # map.connect '', :controller => "welcome"
        map.connect ':controller/:action'
        map.connect '', :controller => 'home', :action => 'ix'

      4. Now three things will work
        1. Entering "http://localhost:3000/" will take you to the home page (views\home\ix.rhtml) by default.

        2. Entering "http://localhost:3000/home/ix" will also do this. Yay!

        3. If you have a web page named "foo" (known as a "view" in the Rails world) in the view directory (app\views\home\), then entering the URL "http://localhost:3000/home/foo" will take you to that page. You won't need an explicit action for it in the controller.

          If you have a view with some other name, then it will also work, assuming that it exists and is stored with the other views belonging to the "home" controller.

    4. Manually create a file in the models directory called "tableless.rb". This is a file containing a table-less model (i.e., there is no database behind it). It will have the following contents:
       # ...\models\tableless.rb
      class Tableless < ActiveRecord::Base
      #----------------------------------------------------
      # create an array of columns
      #----------------------------------------------------
      def self.columns()
      @columns ||= [];
      end
      #----------------------------------------------------
      # add new column to columns array
      #----------------------------------------------------
      def self.column(name, sql_type = nil, default = nil, null = true)
      columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s,
      default,
      sql_type.to_s,
      null)
      end
      #----------------------------------------------------
      # override the save method to prevent exceptions
      #----------------------------------------------------
      def save(validate = true)
      validate ? valid? : true
      end
      end #class

    5. Create the file "contact.rb" in the models directory to extend the Tableless class that you just created.
       # ...\models\contact.rb
      class Contact < Tableless
      column :name, :string
      column :email_address, :string
      column :message, :text
      validates_presence_of :name, :email_address, :message
      validates_format_of :email_address,
      :with => %r{\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z}i,
      :message => "should be like xxx@yyy.zzz"
      end #class

      The explicit calls to "column" in this file are how you add new attributes to the model. This class works just like an ActiveRecord class, including the mechanism for using the standard validation methods, but the cool part is that you don't need to have a database.

      I chose to use just three items per email message: user name, user email address, and the body of the message. The example I based this on also asked for street address, city, state, zip and phone number. If you want to use those fields as well, just add them wherever the user name, user email address, and the user's message body are referenced throughout this example, and use the right format when you do that (i.e., be consistent).

      The last two lines in the Contact class are validators.

      The "validates_presence_of" line verifies the presence of required data (whichever fields you decide are mandatory) when someone uses your form to contact you.

      The "validates_format_of" line makes sure that whatever the user enters as a return email address, though it may be bogus, will at least have a reasonable format. The regular expression used was lifted straight from the Rails API documentation.


    6. Generate a mailer named "Mailtome" (this is an arbitrary name). The following command will create the file "mailtome.rb" in the models directory and an empty "...\views\mailtome" directory:
       C:\...\Mailtome\test> ruby script/generate mailer Mailtome

      Open the file models\mailtome.rb and you will see:
       class Mailtome < ActionMailer::Base
      end

      Edit this file by adding a delivery handler so it looks like this (the actual text of the subject field is up to you):
       # ...\models\mailtome.rb
      class Mailtome < ActionMailer::Base
      def contact_message(contact)
      recipients CONTACT_RECIPIENT
      from contact.email_address
      subject "Contact from my web site"
      body['name'] = contact.name
      body['email_address'] = contact.email_address
      body['message'] = contact.message
      end
      end #class

      Note: I used the minimum number of fields: name, email address, and message.


    7. Manually create the file "contact_message.rhtml".
       <%# ...\views\mailtome\contact_message.rhtml -%>
      Contact from my web site.
      ========================================
      User: <%=@name -%>
      Email: <%=@email_address -%>
      Message:
      ----------------------------------------
      <%=@message -%>
      ----------------------------------------
      Generated at <%=Time.now.to_s -%>

      This will constitute the body of the email message sent to you.

      Note: In "mailtome.rb" (the Mailtome model class) the values are stored in the @body[] array but are referenced directly here via clever magic behind the scenes somewhere, of whose inner workings I am totally ignorant. You can choose to have the time added, or not, and the wording in the heading line (where it says "Contact from my web site.") as well as the exact format are up to you. (So what else is new?)


    8. Code the controller actions.

      Earlier you created a controller named "home". It is in the file called "app\controllers\home_controller.rb", and it needs at least two actions, but I added a third one to make things clearer for me during development. (I need all the help I can get.) Here is the finished controller:
       # ...\controllers\home_controller.rb
      class HomeController < ApplicationController
      #----------------------------------------------------
      # show contact form
      #----------------------------------------------------
      def contact
      @contact = Contact.new
      end
      #----------------------------------------------------
      # process email
      #----------------------------------------------------
      def send_contact_request
      @contact = Contact.new(params['contact'])
      flash[:notice] = ''
      if @contact.save
      begin
      Mailtome.deliver_contact_message(@contact)
      notify_user
      rescue
      flash[:notice] = 'FLASH/RESCUE: Saved, then problems'
      render :action=>"contact"
      end
      else
      flash[:notice] = 'FLASH: Not saved'
      render :action=>"contact"
      end
      end
      #----------------------------------------------------
      # happens when mail successfully sent
      #----------------------------------------------------
      def notify_user
      flash[:notice] = 'FLASH: Saved and sent via notify_user'
      render :action=> "sent"
      end
      end #class

      This is what the various parts do:

      1. The first action, "contact", displays the input form to the user:
         def contact
        @contact = Contact.new
        end

      2. The second action, "send_contact_request", receives data from the user and turns the crank to validate and send the email.

        The first thing that happens is that it creates an instance of the Contact class just as though we had a database connected to the application.

        Then we call the "save" method on our contact object. The "save" has been overridden so it doesn't actually save anything (because we're not using a database).

        If OK so far, we then invoke "deliver", a class method of Mailtome. This indirectly calls our contact_message method via the call: "Mailtome.deliver_contact_message(@contact)".

        The "validates_presence_of" and "validates_format_of" validators in the Contact class check for problems. But even if all fields are filled you can still have garbage data.

        The email address does need the right format. There is no way to guarantee that it's a real email address, but it should at least look like one (it's the best we can do). If you type something like "jdsflk;sdkj@sd,fmas,.com" or "@.123" it won't work. Those pesky "special" characters or an odd format will cause the sending process to fail.

        The right format is "xxxxx@yyyyy.zzz", with one @ sign and one period, each in the right place. The "xxxxx", "yyyyy", and "zzz" parts just have to be there -- their length is not checked, except for the "zzz" part, which has to be at least two characters long, if I've read this the right way. The regular expression came right out of the Rails framework documentation under "validates_format_of" and it's good enough for me.

        OK, if something does go wrong with the send process, then the rescue clause will kick in and bounce the user back to the contact form. Once there the user can see an error message in the flash buffer.

        In case the "save" didn't work, and we never got to the send step at all, then once again the user is bounced back to the contact form, but this time with a different error message in the flash buffer.

        The "validates_presence_of" and "validates_format_of" routines will provide error messages in a production environment.

      3. Action three, "notify_user" affirms to the user that the email was sent. It does this by redisplaying as confirmation back to the user the user's name, email address, and the body of the message via the "sent.rhtml" page.
        Note on Flash messages: This is a test program in a development environment. I designed these messages to be minimally meaningful for me. Use what's right for you. You may not want flash messages visible in a production environment, so you can comment them out, while leaving them there, in case you need to do some tweaking again later on.


    9. Add the "sent" page: "sent.rhtml". (Nonessential HTML and CSS are excluded here for clarity, but are based on the default public\index.html page.)
       <%# ...\views\home\sent.rhtml -%>
      <body>
      <div id="page">
      <div id="content">
      <div id="header">
      <%= link_to "Home", {:controller => "home", :action => "ix"} -%>
      <br /><br />
      <% if @flash[:notice] -%><div><%= @flash[:notice] -%></div><% end -%>
      <br />
      <h1>Message sent</h1>
      </div>
      <br /><br />
      <p><strong>Your Name: </strong><%=@contact.name -%></p>
      <p><strong>Your Email Address: </strong><%=@contact.email_address -%></p>
      <p><strong>Your Message: </strong></p>
      <p><%=@contact.message -%></p>
      <p><strong>Sent at: </strong> <%=Time.now.to_s -%></p>
      </div>
      <div id="footer"> </div>
      </div>
      </body>

    10. Create a Qwik-N-Dirty (TM) form in "contact.rhtml". (Nonessential HTML and CSS are excluded for clarity, but are based on the default public\index.html page.)
       <%# ...\views\home\contact.rhtml-%>
      <body>
      <div id="page">
      <div id="content">
      <%= link_to "Home", {:controller => "home", :action => "ix"} -%>
      <% if @flash[:notice] -%><div><%= @flash[:notice] -%></div><% end -%>
      <%=error_messages_for "contact" -%>
      <%= form_tag :controller=>"home", :action=>"send_contact_request" do -%>
      <p>Name <%= text_field("contact", "name") -%></p>
      <p>Email <%= text_field("contact", "email_address") -%></p>
      <p>Message<br />
      <%= text_area("contact", "message", {"cols"=>"40", "rows"=>"5"}) -%>
      </p>
      <%= submit_tag "Send" -%>
      <%= end -%>
      </div>
      <div id="footer"> </div>
      </div>
      </body>

      Note: This is the newer form syntax. The "start_form_tag" syntax has been deprecated.


    11. Configure the ActionMailer environment. Note: You can hard-code the values into "...\config\environment.rb", or put them into a "config.yml" file as shown in step "2" here.

      1. Open "...\config\environment.rb" and add the following lines at the bottom of the file (extra whitespace added for clarity). We're assuming that you are using SMTP to send email:
         # ...\config\environment.rb
        ActionMailer::Base.delivery_method = :smtp
        c = YAML::load(File.open("#{RAILS_ROOT}/config/config.yml"))
        ActionMailer::Base.server_settings = {
        :address => c[RAILS_ENV]['email']['server'],
        :port => c[RAILS_ENV]['email']['port'],
        :domain => c[RAILS_ENV]['email']['domain'],
        :authentication => c[RAILS_ENV]['email']['authentication'],
        :user_name => c[RAILS_ENV]['email']['username'],
        :password => c[RAILS_ENV]['email']['password']
        }
        CONTACT_RECIPIENT = c[RAILS_ENV]['email']['contact_recipient']

      2. Create the "...\config\config.yml" file like the following, but with your own values as appropriate for your own needs. The development settings don't really matter. For this example, we'll assume that your:
         domain is "www.foobar.com"
        username is "fred"
        password is "secretpassword"
        send-to email address is "fred@gmail.com"

        # ...\config\config.yml
        development:
        email:
        server: mail.development_foobar.com
        port: 25
        domain: development_foobar.com
        authentication: none
        username:
        password:
        contact_recipient: fred@gmail.com
        production:
        email:
        server: mail.foobar.com
        port: 25
        domain: foobar.com
        authentication: login
        username: fred
        password: secretpassword
        contact_recipient: fred@gmail.com

    12. After each test run you can view the "sent" email on the "sent" page ("...\views\home\sent.rhtml") or by viewing the contents of the development log file "...\log\development.log" (this file is also helpful for debugging).

      For your production environment, use the production log file "...\log\production.log" instead.



------------------------------------------------------------------------------------
GENERAL NOTES
------------------------------------------------------------------------------------

The other ActionMailer examples I've found were all designed to automatically send email out to a customer after some event (i.e., an order is received or filled).

What I wanted was a form that a visitor to my site could fill out and use to send email to ME (i.e., email sent from my own application to my personal email account).

Using the example in "Agile Web Development with Rails" helped a little but not much. That example uses the aforementioned auto-generated email process, and the example is too loose to clearly explain exactly what's happening. Hey, I'm slow but I can't help it. I just didn't have enough info to puzzle it out.

I developed this toy application from a sample published under the name "Simple Contact Form using a Table-less Model". I found it at www.bphogan.com/learn/pdf/tableless_contact_form.pdf (Copyright 2006 by Brian Hogan.)

This helped a bunch but I still had a few problems with the original sample I needed to work through, such as:

  1. Flash messages were not hooked up to anything, which made them invisible and therefore useless (Duh). I realize that this is a minor quibble, but I'm not as smart as everyone else, and can't see invisible things, so I changed that.

  2. In the original example the data item "column :email_address, :string" was referred to in the file "contact_message.rhtml"
     as Email        : <%=@email -%>
    instead of Email: <%=@email_address -%>

    This made the message delivery fail at the line that read
     "Mailtome::deliver_contact_message(@contact)"
    in home_controller.rb.

    This was a major time-waster and took me way too long to work this out. Since I had no idea if the original sample actually worked I wasn't sure if there was a problem with it or with me. (My second Duh moment. And yes, I introduced a lot of my own problems and mistakes along the way -- there's always enough blame to go around.)

  3. The original sample uses a model mailer called "Mailer". This didn't seem to work right for me. Maybe the environment got confused by a mailer called Mailer. Things got better when I renamed it to "Mailtome".
     original  :  ruby script/generate mailer Mailer
    my version: ruby script/generate mailer Mailtome <-- Notice the clever change.

  4. The original sample of "...\models\mailtome.rb" has syntax differences from the example shown at http://api.rubyonrails.org/classes/ActionMailer/Base.html. This also seemed to give me some problems. I originally had so many problems that I can't remember them all, but watch your syntax.

  5. The author of the original sample uses some verbal shortcuts and his code is a little too sparsely commented for my taste. My version should be more obvious.

  6. There was one more thing that I wasn't sure of.
    In the original example, in "home_controller.rb", in the action I call "send_contact_request", there is a line which originally read
     "Mailtome::deliver_contact_message(@contact)"

    Originally I thought this was a problem, but it's not. For clarity though, in keeping with more standard OO notation, I changed it to
     "Mailtome.deliver_contact_message(@contact)".

    Note the original "::" vs the changed ".". In the book "The Ruby Language" by Dave Thomas, these examples are given:
     Foo.Bar()      # method call
    Foo.Bar # method call
    Foo::Bar() # method call
    Foo::Bar # constant access

  7. If you configure "perform_deliveries" or "raise_delivery_errors", (in "...\config\environment.rb", if you set up things there) then remember which settings you want to use:
     ActionMailer::Base.perform_deliveries = true | false
    ActionMailer::Base.raise_delivery_errors = true | false

    You can also set these for production in "...\config\environments\production.rb" as:
     config.action_mailer.raise_delivery_errors = true
    config.action_mailer.perform_deliveries = true

  8. Another thing: At least on Site5, for the "address" or "server" denoted in "environment.rb" (or in "config.yml" if you use that), you need to prefix the server name with "mail.". So assuming your domain name is
     foobar.com
    then this line should be
     mail.foobar.com
    Granted, I'm not as bright as all of you, so this took me a while to figure out, along with all the other odd problems along the way. I kept getting erratic results when I got something wrong. Duh, yet again. The thing would work for several tries, and then stop, and wouldn't restart. I'm still not sure why.

    I've learned the hard way that it doesn't do much good to get something working and then forget how I did it. No point in doing all the research and development work more than once. MUCH easier just to read from a clear set of notes that I made for the benefit of my future (and less intelligent) self. That's why I wrote this up. No doubt you are much smarter, so ignore whatever you don't need to remember.

  9. If suddenly your whole production web site stops working (like you can't even bring up your home page), then carefully review all your config and environment files. It really helps to keep a complete set of versions of each file as you work. I usually do something like the following for each file I change.
     environment.rb
    environment.rb.2007_08_08A
    environment.rb.2007_08_08B
    environment.rb.2007_08_08C
    environment.rb.2007_08_09A
    environment.rb.2007_08_09B
    environment.rb.2007_08_09C
    environment.rb.2007_08_09D

    In this example "environment.rb" is the current file, and the latest copy matches it (in this case "environment.rb" has the same contents as "environment.rb.2007_08_09D"). I keep these versions on my desktop system and upload only the latest version to my web site.

    If I need to backtrack I have the exact version at hand, viewable in an instant, no guessing needed.

    Once I'm sure that I have things nailed down I can archive the changes and/or feed them into my version control system and delete the various intermediate copies.

    Manually-created files take some effort but can be a real godsend if I screw myself up so badly that I have to start over. And it has happened. I have had to go back a week or two (on the job, no less) and rewrite something from memory. It's much easier to roll a file back a version or two than to recreate several hundred lines of original code from memory. Whether or not you have a boss watching. A version control system may not guarantee you know which change you need to go back to either, if you're juggling a lot of files and making quick changes all day.

  10. When you change anything in the config directory tree, it's likely that you'll have to restart your web server. If you are working in development mode on your desktop with Webrick, this is really easy. If you are working on your production site, and you will have to, when you install this, then you need to know how to restart Apache. If
     1 - your logon id is "foobarx", and
    2 - your password is "secretpassword", and
    3 - you are using fastcgi, then the following command will restart your
    production server (at least on a shared server)

    pkill -9 -u foobarx -f dispatch.fcgi

  11. Watch your version of Rails. If it's different on your desktop from what you have at your web site, you can suddenly see things stop dead and have no clue. Make sure that the environment variable "RAILS_GEM_VERSION" in "environment.rb" is correct.

  12. One final thing on sending email to yourself: If your site is www.foobar.com, and your email address for that site is "info@foobar.com", and you have email auto forwarded from "info@foobar.com" to an account like "fred@gmail.com", then use "fred@gmail.com" as the email address inside your form-based system and not "info@foobar.com".

    In other words, don't send email from www.foobar.com through "mail.foobar.com" to "info@foobar.com". Send it from www.foobar.com through "mail.foobar.com" to an outside account.

    I've found through trial and error that sending mail through "mail.foobar.com" to "info@foobar.com", and expecting it to be forwarded to "fred@gmail.com" works sometimes and sometimes it doesn't. It also can work perfectly for a while and then stop dead and not work again.

    I also can't reliably send email from "fred@gmail.com" to "info@foobar.com", if "info@foobar.com" forwards email back to "fred@gmail.com". If I want to test my email system without using the contact form I have to send from another email account to "info@foobar.com", and wait for it to show up at "fred@gmail.com".

    OK, maybe I'm especially dumb and missed all the basics somewhere, and shouldn't have tried half of the stuff I did try, but I believe I now have a reliable system.

    At one time I had it all working and then my ENTIRE site locked up SOLID for no particular reason that I could fathom. Nothing I tried would make it work, so I had to delete everything and rebuild the whole site, including uploading over 700 image files. That is not fun. There seem to be some obscure interactions going on somewhere in the background, and I believe that I've gotten around them all.


------------------------------------------------------------------------------------
ORIGINAL SOURCES
------------------------------------------------------------------------------------

1 - "Simple Contact Form using a Table-less Model", at http://www.bphogan.com/learn/pdf/tableless_contact_form.pdf This is based on the following.

2 - "Tableless model" from Rick Olson - http://rails.technoweenie.net/tip/2005/11/19/validate_your_forms_with_a_table_less_model

Note: This second URL doesn't work for me. You can try to find the original idea at http://weblog.techno-weenie.net/ or http://techno-weenie.net/

1 comments :