Tag: ruby


dspam experiement

So finally, SpamAssasin just could not hack it any more on my server. This year there has been a steady influx of spam, and some users of my mail services are literally getting 1 good message out of 100 messages. I feel like I have truly exhausted all SA resources out there. dspam in action From custom rule sets after more rule sets, after more configuration, SA just can't seem to learn fast enough. I also employ RBL/SBL checks and the works. So, what to do? Enter DSPAM.

Previously, I was a bit weary when I heard about the DSpam project and at the time, SpamAssassin was working for me, so why fix something that is not broken or so I thought. Well that mindset finally passed, and I dove into configuring DSpam for my setup. I run a Postfix/Cyrus type of a virtual email setup that is pretty complicated to say the least. I knew in advance that embarking down this road would mean learning some things and some elbow grease, but that is ok. I finally settled on a configuration that is similar to both Neale's Setup and Cepcep's Setup in different ways. In my setup, I have 2 independent chains, one for inbound email and one for outbound email. I decided to leave my outbound chain alone, and continue to send it thru amavisd. For the inbound chain, all mail is subject to getting sent to dspam, then dspam re-routes it back into postfix with a result attached.

Lastly, I wrote the following script, so that I may simply forward my mails to ham@ or spam@, and have postfix deliver it directly to my script, for retraining purposes.

It is piped to dspam, from postfix, like so:

dspam-retrain   unix    -       n       n       -       10      pipe
  flags=Ru user=dspam argv=ruby /usr/local/bin/dspam-retrain.rb $nexthop $sender $recipient

#!/usr/bin/ruby
# Ruby version of dspam-retrain perl script.
# Perl version: http://dspamwiki.expass.de/DspamRetrainScript
# Author: wsr@rushforthnetworks.com
# License: BSD
# Abstract: setup postfix to pipe mail into this script
#           so that we may then pass it off to dspam in
#           an appropriate way.
#
#           dspam-retrain.rb handles spam-user@domain.tld, and
#                                    spam@domain.tld formats
#
#           If dspam-retrain.rb does not find a signature,
#           it will train dspam using corpus or innoculation
#           mode.      
#
# Requirements: open4 gem is installed
# -----------------------------------------------------------

#### Configuration BEGIN ####################################

#enable logging?

@enable_logging = true
@log_file = '/tmp/dspam_retrain.log'

#what mode to train dspam in if we do not find signature?
#corpus or innoculation

@alternative_mode = 'corpus'

#### Configuration END #####################################


require 'rubygems'
require 'open4'

if @enable_logging
        require 'logger'
        @logfile = File.new(@log_file, 'a+')
        @log = Logger.new(@logfile)
end

def logthis(message)
        if @enable_logging
                @log.info(message)
        end
end

# Get arguments
spam_class  = ARGV[0]
sender = ARGV[1]
recip  = ARGV[2]

logthis("dspam-retrain Started. Arguments: #{spam_class}, #{sender}, #{recip}")

#see if we were passed spam-user@ or just user@
if match = recip.to_s.match(/^(spam|ham)-(\w+)@/)
        user = recip.gsub(/#{match[1]}\-/, '')
elsif match = recip.to_s.match(/^(\w+)@/)
        user = sender
else
        logthis("\tCant't determine user")
        exit 75                
end

signature = String.new
message   = String.new

#loop through email (passed via stdinput)
#search for signature

$stdin.each do |line|
        if line.match(/X-DSPAM-Signature/)
                signature = line.gsub(/X-DSPAM-Signature:/, '')
                #remove any potential whitespace
                signature.strip!
                #since we found signature, break loop
                break
        end
        message << line
end


if signature.length.to_i == 0 
        #we did not find a signature, do normal training

        mode = 'train'

        logthis("\tEmail did not have signature passed in. Attempting #{@alternative_mode} train.")

        #open up dspam with appropriate options
        pid, dspam_in, dspam_out, dspam_err = Open4::popen4 "/usr/bin/dspam --source=#{@alternative_mode} --class=#{spam_class} --user #{user}"

        #attempt to feed message in
        begin
                dspam_in << message
        rescue
                #means dspam closed stdinput because our
                #options failed
                dspam_err.each_line do |o|
                        logthis("\t" + o.gsub(/\n/, ''))
                end
        end
        #close dspams stdinput
        dspam_in.close_write

        #see if dspam left any messages for us
        dspam_out.each_line do |o|
                if o.strip.length == 0 then next end
                logthis("\t" + o.gsub(/\n/, ''))
        end

else
        #we found signature, so we will only pass that.

        mode = 'retrain'

        logthis("\tRetraining Signature: #{signature} for User: #{user} as: #{spam_class}")

        #open up dspam with appropriate options
        pid, dspam_in, dspam_out, dspam_err = Open4::popen4 "/usr/bin/dspam --source=error --signature=#{signature} --class=#{spam_class} --user #{user}"

        #see if dspam left any messages for us
        dspam_out.each_line do |o|
                if o.strip.length == 0 then next end
                logthis("\t" + o.gsub(/\n/,''))
        end
        dspam_err.each_line do |o|
                if o.strip.length == 0 then next end
                logthis("\t" + o.gsub(/\n/,''))
        end
end

ignored, status = Process::waitpid2 pid

#see if we exited cleanly and log it
if status.exitstatus == 0
        logthis("\tMessage successfully #{mode}ed as #{spam_class}")  
else
        logthis("\tMessage NOT #{mode}ed")
end

DRY is pretty cool

Ok, I have to admit, I was a little turned off by some of the acronyms that were/are being flung around so much when I started getting into rails. Do you like your KISSes DRY? Ugh.

Then it hit me. Both of these things are pretty darn powerful, and can be life changing things. At least life changing if your life is mostly based around writing code. KISS is a funny thing to go around mentioning to a lot of geeks such as myself, but in the end, I really do agree with it and try to fashion code in that style. It stands for Keep-It-Simple-Stupid. Don't complicate things that don't need to be complicated. Enough said.

So this new one, DRY, what's that all about? Well, it stands for Don't-Repeat-Yourself. After working with rails for a bit, I think it has made me a better programmer even in languages and frameworks that are outside of ruby and rails. I often cringe at the thought of having to accept and be cool with new acronyms, but in the end, I don't think that rails and DRY are synchronous, I just think that rails was fashioned with DRY in mind. Today I installed 2 plugins that each took me a days work in one rails application, into a brand new separate rails application. Guess what? It only took me about 2 minutes of tuning to get it to work perfectly. Talk about saving time, its uncanny.

Now I just have to try apply DRY to other projects outside of the rails ones, and try not be upset that they are not rails ;).

Time Sensitive Background

I just added a new feature to my website. If you notice, the top graphic is somewhat like a sky. Now it changes depending on the time of day you are visiting my website. However, I did not accomplish this by making multiple graphics, and changing them out. I actually wrote a new model for rails that uses the Imagemagick/MiniMagick image processing library to accomplish this. So every few minutes of the day when it is called it will actually tint the picture an appropriate shade. It also saves these images, so they only have to be generated once. Here is the code for the model:

require 'rubygems'
gem 'mini_magick'
require 'mini_magick'

class Tphoto
  attr_accessor :time, :file
  def initialize(time,file)
    @filename = file
    @noon = Time.parse("%Y-%m-%d " << "12:45:00")
    distance = time.to_i - @noon.to_i
    if(distance > 0) then
      time = time - (distance * 2)
    end
    @seconds = time.strftime("%H%M").to_i/10
    @viewurl = "/images/tphoto/#{@seconds}/#{@filename}"
    unless File.exist?("#{RAILS_ROOT}/public/images/tphoto/#{@seconds}/#{@filename}")
      generate_time(@seconds, @file)
    end
  end

  def generate_time(seconds, file)
    unless File.exist?("#{RAILS_ROOT}/public/images/tphoto/#{@seconds}")
       Dir.mkdir("#{RAILS_ROOT}/public/images/tphoto/#{@seconds}")
    end
    image = MiniMagick::Image.from_file("#{RAILS_ROOT}/public/images/tphoto/#{@filename}")
      image.combine_options do |c|
        c.fill("#FFFFFF")
        c.tint(seconds+10)
    end
    image.write("#{RAILS_ROOT}/public/images/tphoto/#{@seconds}/#{@filename}")
  end

  def viewurl
    return @viewurl
  end

  def filepath
    return "#{RAILS_ROOT}/public/images/tphoto/#{@seconds}/#{@filename}"
  end

end

and here is the code for the controller:

class TphotoController < ApplicationController
  def index
    @time = Time.now
    @tphoto = Tphoto.new(@time, "monday.jpg")
    @response.headers['Last-Modified'] = Time.now.httpdate
    @response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
    @response.headers['Pragma'] = 'no-cache'
    @response.headers['Expires'] = 'Thu, 19 Nov 1981 08:52:00 GMT'
    send_file @tphoto.filepath, :type => 'image/jpeg', :disposition => 'inline'
  end
end

Human Time

I recently added the capability to display time in more human friendly terms. Basically, instead of saying Posted at 2007-11-30 20:18:36, a relevant post will say "posted 3 days ago". You can see this on every post or twitter on my site. I did not realize the full potential of Ruby until I sat down to write the code to make this happen. I simply extended Ruby's default Time class with the following:

class Time
   def humantime
            age = Time.now.to_i - self.to_i
            seconds_max = 60
            minutes_max = 3600
            hours_max   = 86400
            days_max    = 604800
            weeks_max   = 2629743
            months_max  = 31556926
            p = ''
            unit = ' second'
            if age < seconds_max
                amt = age
            elsif age < minutes_max
                amt = age/seconds_max
                unit = ' minute'
            elsif age < hours_max
                amt = age/minutes_max
                unit = ' hour'
            elsif age < days_max
                amt = age/hours_max
                unit = ' day'
            elsif age < weeks_max
                amt = age/days_max
                unit = ' week'
            elsif age < months_max
                amt = age/weeks_max
                unit = ' month'
            else
                amt = age/months_max
                unit = ' year'
            end

            if amt > 1
              p = 's'
            end

            humantime = amt.to_s + unit + p + " ago"
       end
end

So now, to get the time in human time, I just call the .humantime method that is now a part of every Time object.

new website

Welcome to my new website :)

It is still very much under construction, but the jist of it is here. This site has been built using Ruby on Rails as an experiment, and then it turned into a full site. Ruby is awesome.

Tag cloud

  1. 1 entries are tagged with 2007
  2. 2 entries are tagged with applevalley
  3. 1 entries are tagged with archlinux
  4. 1 entries are tagged with automation
  5. 1 entries are tagged with bigsur
  6. 5 entries are tagged with burningman
  7. 3 entries are tagged with code
  8. 1 entries are tagged with cplusplus
  9. 1 entries are tagged with desert
  10. 1 entries are tagged with drinks
  11. 1 entries are tagged with dspam
  12. 2 entries are tagged with dunes
  13. 1 entries are tagged with energy
  14. 1 entries are tagged with esplanade
  15. 5 entries are tagged with europe
  16. 1 entries are tagged with evdo
  17. 1 entries are tagged with flv
  18. 1 entries are tagged with gadgets
  19. 1 entries are tagged with government
  20. 2 entries are tagged with losangeles
  21. 1 entries are tagged with motivation
  22. 1 entries are tagged with moving
  23. 1 entries are tagged with newserver
  24. 1 entries are tagged with rails
  25. 2 entries are tagged with recipe
  26. 5 entries are tagged with ruby
  27. 1 entries are tagged with salmon
  28. 3 entries are tagged with site
  29. 1 entries are tagged with snow
  30. 6 entries are tagged with travel
  31. 1 entries are tagged with trip
  32. 1 entries are tagged with website
  33. 2 entries are tagged with work
  34. 1 entries are tagged with x4200
  35. 3 entries are tagged with yz250

Twitter Updates

Links to check out

Subscribe RSS