Aug 07 2007

ruby bot: email processing

Category: Ruby, Shell Scriptetd @ 12:11 am

Pinky: Gee, Brain, what are we going to do tonight?
Brain: The same thing we do every night, try to take over the world!

Have you ever wanted to have the ability to send commands to your box using email? Use RubyBot, the brand new plugin-driven ruby script that makes the task of taking over the world a bit easier!

Goals of the project:

  • We want to have a script that can be used directly by the MTA (much in the way procmail works).
  • We want to deal with the internals of email format as less as possible.
  • Flexibility is an issue! We want to be able to do all sorts of things.
  • We want to have some feedback/output from our commands

But first, stand and relax, there is lots of stuff comming in this post, so maybe it is a good idea to download the code and have a look at it before we start. :)

MTAs can usually be configured to pass received messages to certain applications. The internals of how this mechanism works is out of the scope of this post. However, as an example, let’s see how qmail uses the .qmail files to do it. The following .qmail file will pipe the contents of all the incoming mail to our script:

|./rubybot.rb

The first thing thatour script needs to do is to get the piped message. In ruby we do this by reading the contents of the standar input:

#0: get email from standard input
email = ''
while gets
  email << $_
end

The previous code will store in the email variable the contents of the email. Instead of trying to parse the email with regular expressions we are going to use the ActionMailer package of the Ruby on Rails (RoR) framework.

What is ActionMailer?
Action Mailer is a framework for designing email-service layers.

Sounds like ideal, does it? In order to use the package we will need to include it and also to create a class that extends the ActionMailer::Base, here is the code:

require 'rubygems'
require_gem 'actionmailer'

#wrapper of RoR ActionMailer
class RubyBot < ActionMailer::Base
  def receive(email)
    return email
  end
end

Now we can convert the raw email string into a ruby object with the following call:

msg = RubyBot.receive(email)

We can send signals such as msg.subject or msg.parts to the object to query the email’s information.

Before continuing with the rest of the script a word should be said regarding logging and configuration. After all, our RubyBot is quick-and-dirty script so for configuration we will use a hash at the begining of the file:

#--------------------------------------- config
$options = {
  :myself => 'rubybot[_at_]nomejortu.com',
  :admin => 'etd[_at_]nomejortu.com',
  :subject => '[rubybot] notificacion:',
  :plugins_dir => './rbplugins',
  :pluginopt => {
    :tmpdir => './tmp',
    :logger => nil
  }
}
#--------------------------------------- /config

All options are self explanatory except maybe two:

  • :plugins_dir will contain all of our plugins.
  • :pluginopt contains the options that we will be passing to the plugins (mainly an object for logging and a temporary directory).

For logging we are going to use ruby’s standard Logger and we are going to store it’s output in a log file under the :plugins_dir directory. This is the code that initializes the logger:

logfile = $options[:plugins_dir]+'/msg.log'
File.rm_f(logfile) if File.exist?(logfile)
log = Logger.new(logfile)

log.level = Logger::DEBUG
$options[:pluginopt][:logger] = log

We are saving the name of the file for the last bit of the script where we attach the log file to the notification that RubyBot sends to the administrator. Also we are saving a reference to the logger in the configuration hash so we can pass it to out plugins.

Our script will use the subject line to decide which plugin should handle the request. The dispatching algorithm is show below:

#2nd process the command (from email's subject)
module_name = msg.subject.split[0]
module_file = $options[:plugins_dir] + '/' + module_name + '.rb'
if (FileTest.exists?(module_file))
  log.info{ "valid plugin found: #{module_name}" }
  begin
    load module_file
    plugin = Kernel.const_get(module_name.capitalize + 'Plugin').new
    output = plugin.process(msg, $options[:pluginopt])
  rescue
    log.error{ "error while processing command: #{$!}" }
    log.debug{ $!.backtrace.join("n") }
    output = "error while processing command: #{$!}"
  end
else
  log.error {"module not found in plugins dir (#{$options[:plugins_dir]})"}
end

A few things are going on in the previous piece of code. First we split the subject of the email and we take the first word as a module name. Then we try to determine if a file called module_name.rb exists in the plugin directory. An error is logged if the file is not found. However, if we find the file we try to load the file and create an instance of the module:

load module_file
plugin = Kernel.const_get(module_name.capitalize + 'Plugin').new

For example, if our message’s subject is “simple” te previous lines will try to instantiate a copy of SimplePlugin from the file ./rbplugins/simple.rb. If something goes wrong we capture and log the exception, otherwise we send the .process signal to the plugin passing the email and the options as arguments.

All RubyBot’s plugins should include the Plugin module as defined in ./rbplugins/plugin.rb. The module has five methods and I will not give details of all of them here for the sake of clarity in the post. However, a brief description follows (MIME stands for Multipurpose Internet Mail Extensions):-

  • part_filename: returns the filename that we should use for a given part in a MIME multipart messages.
  • ext: given a MIME type the method returns a file extension.
  • save: saves all the attachments of the email into a given folder.
  • clear: given a folder, deletes it’s contents.

The last and most interesting method is process. It will perform three operations: first save all the email’s attachments to the temporary folder, then process the body of the message looking for commands and finally delete the attachments from the temporary folder.

Depending on whether the email received is multipart or not a slightly different approach is needed to get the requested commands:

#process the body, 1 command per line
if (@msg.parts.empty?)
  commands = @msg.body.split(/n/)
else
  commands = @msg.parts[0].body.split(/n/)
end

Now we have an array that contains all the commands. The processing cycle goes as follows:

commands.each do |cmdline|
  args = cmdline.split
  if (self.respond_to?(args[0]))
    begin
      @log.info( 'plugin' ) { "[#{args[0]}] processed by #{args[0]} plugin. " }
      out << self.send(args[0], args[1,args.size-1])
    rescue
      @log.error('plugin') { "error while processing command: #{$!}" }
      @log.debug('plugin') { $!.backtrace.join("\n") }
      out << "error while processing command: #{$!}"
    end
  else
    @log.error('plugin') { "undefined command: #{args[0]}" }
    out << ''
  end
end

Appart from the error handling the interesting calls are two: self.respond_to? and self.send. With the first one we check whether the plugin implements the requested command and with the second one we delegate the execution of the command to the implementation.

Following our previous example let’s say that we receive the following email:

Subject: simple
[...]
echo hello world
echo good bye

RubyBot will parse the Subject line and will load and instantiate SimplePlugin. Then a process signal will be sent to the plugin. Since our example has no attachments, the process method (of the Plugin module) will just walk through the commands checking if we have implemented echo in SimplePlugin module. Provided that SimpleCode is defined as follows (in ./rbplugins/simple.rb):

require 'rbplugins/plugin'

class SimplePlugin
  include Plugin
  def echo(args)
    return "good to go: #{args.join('|')}"
  end
end

The output of the commands issued above would be:

good to go: hello|world
good to go: good|bye

To comply with our last goal we need to add an extra method to the RubyBot class. The method notification will be used at the end of the script to send us a notification with all the details of the procession of our commands.

class RubyBot < ActionMailer::Base
[...]
  def notification(msg, out, log)
    recipients $options[:admin]
    subject "#{$options[:subject]} #{msg.subject}"
    from $options[:myself]
    
    commands = 'empty'
    if (msg.parts.empty?)
      commands = msg.body
    else
      commands = msg.parts[0].body
    end

    body =<<EOF
    ------------------------------------
    Se ha recibido el mensaje anterior
    From: #{msg.from}
    Subject: #{msg.subject}
    Date: #{msg.date}
    Body:
      #{commands}
    Output: 
      #{out} 
    ------------------------------------
EOF
      
    part :content_type => 'text/plain', :body => body

    attachment :content_type => 'text/plain', :body => File.readlines(log).join("n")
  end
end

The method creates a new email message with the recipient and subject specified by the options of the plugin. Then it creates a body for the email consisting of information regarding the received commands and at last attaches the log file created during the processing of the commands.

The last call of our script will be RubyBot.deliver_notification(msg, output, logfile). ActionMailer’s deliver_something will first call something, that should return an email object, and then will attempt to send the email to the specified recipients.

And that was all for today…

Pinky: Why? What are we going to do tomorrow night?
Brain: The same thing we do every night, Pinky. Try to take over the world!

Share and Enjoy: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • Digg
  • del.icio.us
  • Slashdot
  • Technorati

2 Responses to “ruby bot: email processing”

  1. Karnali says:

    Cool!

  2. Patriarca says:

    Cool article! Never thought that was possible!

    Can’t wait to try myself!! :)
    Thanks

Leave a Reply