#!/usr/bin/ruby require 'rmail' if ARGV[0] == "spam" MAILIN="mail/spam-in" MAILLEARN="mail/spam-learn" OUTBOX="mail/outbox" SEND_DISCARD_MAIL = true elsif ARGV[0] == "ham" MAILIN="mail/ham-in" MAILLEARN="mail/ham-learn" OUTBOX="mail/outbox" SEND_DISCARD_MAIL = false else stderr.puts "Usage: $0 ham|spam" end DOMAIN='lists.oftc.net' FROM="listmoderators@lists.oftc.net" HOSTNAME=`hostname`.chop class Counter @@counter = 0 def Counter.value @@counter += 1 end end def uniqueName "%d.%d_%d.%s"%[ Time.now.to_i, $$, Counter.value, HOSTNAME] end def check_maildir(d) throw "#{d} is not a maildir" unless FileTest.directory?(d) throw "#{d} is not a maildir" unless FileTest.directory?(d+"/new") throw "#{d} is not a maildir" unless FileTest.directory?(d+"/cur") throw "#{d} is not a maildir" unless FileTest.directory?(d+"/tmp") true end def store_in_maildir(md, msg) fn = uniqueName File.open(md+"/tmp/"+fn, "w", 0600) do |f| f.puts msg end File.link(md+"/tmp/"+fn, md+"/new/"+fn) File.unlink(md+"/tmp/"+fn) md+"/new/"+fn end def process_mail(filename) message = File.open(filename) { |f| RMail::Parser.read(f) } # some sanity checks throw "Mailman moderation mails are expected to have 3 mime parts" unless message.body.length == 3 throw "Mime Part 0 does have an unexpected content type: #{message.body[0].header['Content-Type']}" unless message.body[0].header['Content-Type'] == 'text/plain; charset="us-ascii"' throw "Mime Part 1 does have an unexpected content type: #{message.body[1].header['Content-Type']}" unless message.body[1].header['Content-Type'] == 'message/rfc822' throw "Mime Part 2 does have an unexpected content type: #{message.body[2].header['Content-Type']}" unless message.body[2].header['Content-Type'] == 'message/rfc822' explanation_body = message.body[0].body held_part = RMail::Parser.read( message.body[1].body ) discard_part = RMail::Parser.read( message.body[2].body ) # more sanity checks throw "Did not find 'As list administrator, your..' boilerplate in mail" unless explanation_body =~ /^As list administrator, your authorization is requested for the/ throw "Did not find listname in mail" unless explanation_body =~ /^ *List: *.*@#{DOMAIN}/ throw "discard_part does have an unexpected content type: #{discard_part.header['Content-Type']}" unless discard_part.header['Content-Type'] == 'text/plain; charset="us-ascii"' request_address = discard_part.header['From'] throw "discard_part does not have a from address" unless request_address matchdata = /confirm ([0-9a-f]*)/.match discard_part.header['Subject'] throw "Could not find cookie in discard_part" unless matchdata and matchdata[1] cookie = matchdata[1] store_in_maildir(MAILLEARN, held_part) if SEND_DISCARD_MAIL mail_request = RMail::Message.new() mail_request.header['From'] = FROM mail_request.header['To'] = request_address mail_request.header['Subject'] = "Re: confirm #{cookie}" mail_request.body = '' store_in_maildir(OUTBOX, mail_request) end end check_maildir MAILIN check_maildir MAILLEARN check_maildir OUTBOX Dir[MAILIN+"/new/*"].each do |filename| begin process_mail filename File.unlink filename rescue Exception => e STDERR.puts "Error when processing #{filename}: #{e}" end end