Thomas Hurst <tom.hurst / clara.net> writes: > After seeing the huge ugly mess of procmail's code, I'm feeling a > compulsion to change. http://www.lickey.com/rubymail/ is the result of similar feelings on my part. It is a work in progress. Another Ruby solution is Scarlet. It is much more mature but is mainly documented in Japanese so I don't know too many details. :-) It does lack one thing I like about my solution -- in RubyMail the mail filter script is written in pure Ruby. > The most "standard" alternative seems to be maildrop, which at least > looks fairly clean and has a much more readable filter language. True, but it is a crippled little filtering language akin to procmail. I.e. to do anything complex you have to shell out to an external command. > But it still lacks some features I want, such as the ability to > queue messages and perform higher level comparisons on them to, for > instance, intelligently deal with dupes in group replies (try > filtering on X-Mailing-List: headers and tell me to use formail and > a message id cache..) Yeah, this is often the kind of stuff you often want or need a "real" language to do. > Then I started thinking about how to use ruby as a filter language; [...] > queue = Queue.new(20, '3 min') > queue.prune_dupes > queue.run do |mail| Not sure what you mean here...does this read form your inbox or something? > mail.cc('Backup') > if list = MailingList.filter(mail) > mail.deliver('lists/' + list) > end > mail.filter('/usr/local/bin/spamassassin -P') > if mail.header.match(/^X-Spam-Flag: Yes/) > mail.deliver('SPAM') > end > mail.deliver('Inbox') Yeah, I have basic mail handling classes (Mail::Message, Mail::Header), basic classes use to parse messages (Mail::Parser) and, soon, a basic class used to print a message to an output stream (Mail::Printer -- class name is undecided!). In the design of these classes I'm finding I'm stealing lots of ideas from Python's email library and to a lesser extent Perl's Mail::Internet and MIME-tools packages, and Ruby's TMail. Then I have a Mail::LDA (local delivery agent) class that makes it easy to write mail filters without worrying about a random exception in your code causing a bounce. Then there is an rdeliver.rb script that uses Mail::LDA and friends to parse a message from stdin. It then creates a Deliver class, and evals the user's ~/.rdeliver file within the context of that class, which is responsible for defining Deliver#main. The script then instantiates a Deliver object can calls Deliver#main. The simplest possible .rdeliver file is: def main lda.save("inbox") end This defines a Deliver#main method that saves the message to a unix mbox file called "inbox" in the user's home directory. Here is some snippets from my .rdeliver file. There is some stuff here that could be factored out into nicer pieces/modules. But you'll get the idea. Remember, this is evaluated in the context of the Deliver class, so each def here adds a method to the Deliver class. def spool_save(folder, continue = false) lda.save(".incoming/#{folder}.spool", continue) end def save_if_list(list, folder) name, domain = list.split(/@/).collect { |s| Regexp.quote(s) } if h.match?(/^((Resent-)?Sender|Errors-To|X-Loop|(X-)?Mailing-List)$/i, /(owner-)?#{name}(-request|-help|-admin|-bounce|-errors|-owner)?@#{domain}/im) || h.match?(/(List-Id|X-Mailing-List)$/i, /<#{name}.#{domain}>/im) || h.match?("x-ml-name", /^\s*#{name}([^ \t]|$)/im) || h.match?(/^(to|cc)$/i, /#{name}@#{domain}/im) spool_save("list.#{folder}") end end # This function checks if a given +ip+ is listed by the indicated DNS # blackhole list +service+. It returns a text string indicating why # the ip was blocked if listed, or nil if not listed. # # This function relies on the 'host' external command. def dnsbl(ip, service) if ip =~ /\A\d+\.\d+\.\d+\.\d+\z/ ip.untaint end raise "bad ip #{ip.inspect}" if ip.tainted? query = ip.split(/\./).reverse.join('.') + '.' + service result = `host -t txt #{query} 2> /dev/null` return nil unless $? == 0 unless result =~ /\"(.*)\"/m raise "can't find text record in ${result.inspect}" end return "#{service} lists ip #{ip}: " + $1 end def main [...] save_if_list('briefs / lists.freeswan.org', 'freeswan') save_if_list('announce / lists.freeswan.org', 'freeswan') [...] save_if_list('zsh-workers / sunsite.dk', 'zsh') save_if_list('zsh-users / sunsite.dk', 'zsh') # backup everything lda.save('.incoming/backup-' + Time.now.strftime('%Y-%V'), true) # Check if we got this through bigfoot and bounce it if it looks # suspicious. if h.match?('delivered-to', /matt\+bigfoot@lickey\.com/im) && h.match?('received', /from.*by.*bigfoot\.com.*LiteMail/im) temp = h.match('received', /from.*by.*bigfoot\.com.*LiteMail/im)[0] if temp =~ /\(\[([\d\.]+)\]\)/ ip = $1 raise if defined? blocked lda.log(1, "bigfoot ip is #{ip}") blocked ||= dnsbl(ip, 'relays.ordb.org') blocked ||= dnsbl(ip, 'inputs.orbz.org') blocked ||= dnsbl(ip, 'bl.spamcop.net') lda.reject(blocked) unless blocked.nil? end end [...] if h.match?('from', /matt@lickey\.com/im) && h.match?('subject', /testme bounce/im) lda.reject("You want to test a bounce? I'll give you a bounce!") end [...] if h.match?('from', /Cron Daemon|root@lickey\.com|uucp@lickey\.com|cfengine@lickey\.com/im) spool_save("daemons") end # FIXME: need a forward mechanism if h.match?('from', /relaytest / lickey.com/im) && h.match?('subject', /open relay test for/im) lda.pipe("/usr/sbin/sendmail -oi qspam / orbz.org relays / ordb.org") end [...] spool_save("inbox") end -- matt