#!/usr/bin/ruby # filter-dspam.rb: Qmail + DSPAM glue script # This script takes a piece of mail on STDIN and sends it through DSPAM for # filtering. It then delivers to the appropriate mailbox depending on the # results of the DSPAM check. # # Copyright 2008 Alkaloid Networks LLC # Released under the terms of the GNU Public License version 2 # http://projects.alkaloid.net # Author: Ben Klang # $Id$ # require 'rubygems' require 'tmail' require 'logger' require 'pp' ## Customize these variables as required # Configure the path to DSPAM and the program arguments here $dspambin = "/opt/dspam/bin/dspam" $dspamargs = "--mode=teft --stdout --deliver=innocent,spam" # Path to qmail/control $qmailbase = "/opt/qmail" # Only check messages smaller than 256KiB $maxchecksize = 256 * 1024 # Level of information sent to STDERR $verbose = Logger::DEBUG # Folder to hold quarantined messages $spamfolder = "SPAM" # Path to per-user log files area. Must be writeable by the UID doing the # filtering. If multiple user IDs will be filtering, try either logging # directly into each user's home directory (ENV['HOME']) or into /tmp # Valid replacements: # %u - Delivery username # %e - Destination email address # %l - "Local" (user) portion of destination email address # %d - Destination domain name $logpath = "/opt/qmail/log/filter-dspam/%u" EXIT_HARD = 100 EXIT_SOFT = 111 # Sanity check: make sure we have a required environment variables if (!ENV.key?('USER') || !ENV.key?('RECIPIENT') || !ENV.key?('LOCAL') || !ENV.key?('HOST') || !ENV.key?('HOME')) $stderr.puts("Missing required environment variable.\nMust have $USER, $RECIPIENT, $LOCAL, $HOME and $HOST set.\nBailing out!") exit EXIT_HARD end # Create a log instance begin # While email addresses and domain names are not case sensitive, the # username may be. # Downcase the email components. user = ENV['USER'] recipient = ENV['RECIPIENT'].downcase local = ENV['LOCAL'].downcase host = ENV['HOST'].downcase $logpath.gsub!(/%u/, user) $logpath.gsub!(/%e/, recipient) $logpath.gsub!(/%l/, local) $logpath.gsub!(/%d/, host) log = Logger.new(File.open($logpath, File::WRONLY | File::APPEND | File::CREAT, 0600)) log.level = $verbose log.info("Processing new mail message to #{ENV['RECIPIENT']}") rescue Exception => e $stderr.puts("Unable to open logger instance: #{e.to_s}") exit EXIT_SOFT end # Parse the incoming message msg = TMail::Mail.parse(STDIN.read) # Bail out if the message is too large if (msg.body.length < $maxchecksize) # Load the message for processing begin agentstr = `#{$dspambin} --version`.grep(/DSPAM Anti-Spam Suite/).pop.chop msg['X-AntiSpam-Agent'] = agentstr dspam = IO.popen("#{$dspambin} #{$dspamargs} --user #{user}", "w+") dspam.puts(msg.to_s) dspam.close_write msg = TMail::Mail.parse(dspam.read) dspam.close rescue Exception => e log.error("Error while scanning message for spam: #{e.to_s}") exit EXIT_SOFT end end # Try to determine the default delivery path if File.readable?("#{$qmailbase}/control/defaultdelivery") defaultdelivery = File.open("#{$qmailbase}/control/defaultdelivery").read.chop else defaultdelivery = "./Maildir/" end # FIXME: If using qmail-ldap, how does mailMessageStore get propagated? mbpath = "#{ENV['HOME']}/#{defaultdelivery}" # Try to deliver the message if (msg.key?('X-DSPAM-Result')) if (msg['X-DSPAM-Result'].to_s.downcase == 'spam') log.info("SPAM detected. Quarantining message to #{$spamfolder}") mailbox = TMail::Maildir.new("#{mbpath}/.#{spamfolder}") elsif (msg['X-DSPAM-Result'].to_s.downcase == 'innocent') log.info("DSPAM result clean; delivering to INBOX") mailbox = TMail::Maildir.new(mbpath) else log.error("Unknown DSPAM return code: #{msg['X-DSPAM-Result']}") exit EXIT_HARD end else log.info("DSPAM return empty or message filtering skipped. Delivering to INBOX.") mailbox = TMail::Maildir.new(mbpath) end mailbox.new_port{|filehandle| filehandle.puts(msg.to_s)}.move_to_new