gurgitate-mail/CHANGELOG0000644000076500001440000002126210747761052014746 0ustar dagbrownusers1.10.0: - Added support for MH mailboxes 1.9.1: - Made a bunch of little changes to ensure that gurgitate-mail is compatible with Ruby 1.9.0 (whilst not sacrificing compatibility with Ruby 1.8.x) - Added the unit tests to the tarball for those who are curious 1.9.0: - Added the ability to create new Gurgitate::Mailmessage objects with the "create" constructor--instead of giving it the text of a message to parse, you give it headers as a hash and it puts the mail message together. 1.8.3: - Added an "exit 75 (TEMPFAIL)" for when it tries its best to deliver a mail message anywhere at all and fails no matter how hard it tries. With a sensible MTA, this should result in the message being requeued and reprocessed later. 1.8.2: - Fixed a bug discovered by TheWordNerd where gurgitate was writing to the default spool after every config file was processed, instead of holding off until all of them had gone through. 1.8.1: - Whoops! Fixed a bug (thanks tchan!) in argument parsing that was causing to parsing to break. 1.8.0: - Let you (only TEN FREAKIN' YEARS LATE) explicitly specify sender and receivers on the command line. - Added methods (sub and sub!) to HeaderBag to allow you to alter the contents of headers 1.7.2: - Added gem support 1.7.1: - Fixed Yet Another Header Parsing Bug[tm] 1.7.0: - Changed it so that when you say "folderstyle Maildir", it changes the default mail spool dir and mail spool file to $HOME and $HOME/Maildir respectively. - Changed the way that config params work. Now instead of having to say: self.sendmail = "/usr/sbin/sendmail" you can say: sendmail "/usr/sbin/sendmail" Sort of in the style of attr_reader and attr_writer, or in the style of some other programs' config files. - Added -f option to select rules files from the command-line 1.6.3: - Restored Gurgitate::Gurgitate#process() with a block, which seemed to have disappeared with the config file commotion. - Added mention of the systemwide configuration files to the manual. Which, by the way, could still use something of an overhaul. Way too much stuff is still SEKRIT. 1.6.2: - Fixed a bug introduced in the last code reorg, whereby it would forget to default to saving mail to the spool 1.6.1: - Fixed a bug introduced in the last code reorg, whereby it would forget to default to saving mail to the spool - Fixed file permissions in the tarball - Fixed a really stupid bug with Maildir folder creation where it made invalid mail directories. 1.6.0: - Added site-wide gurgitate-rules-file capability. - Made it so that headers that start with a number pass through without breaking stuff, which is a shame, because those are quite illegal as far as I know. - Also, headers with a . in the *header* name should pass though. - ALSO, headers with a leading - in the header name, likewise. - Fixed a (very minor) maildir-writing bug involving a misnamed variable, which might trigger were gurgitate to be used as an LMTP process or a Sendmail milter. 1.5.3: - Fixed a problem caused by me not touching filter in way too long: there were namespace and parameter problems. 1.5.2: - Made it not blow up when it encounters a header of the form "To:\n emailaddress@example.com" (as seen in email messages from the obscure Japanese email client Becky) 1.5.1: - Made it use Postfix's heuristic for determining whether a nonexistent mailbox is a mail spool or a Maildir by looking for a slash on the end of the mailbox's name. - Added code from Bertram Scharpf to make error-in-rules handling a little more graceful 1.5: - Made it not throw an exception on (illegal, mind) headers with underscores in their names. - Added a thing to the "filter" method so that you can say something like filter("spamc") do if headers["X-Spam"] =~ "Yes" then delete end end if you want to. - Also added the ability to put folderstyle = Maildir into your .gurgitate-rules.rb to tell it that if it doesn't find a mailbox, it should create a Maildir mailbox rather than its usual default (mbox). 1.4.1: I said I'd given up on minor changes? I lied. - Had it assume that if a file isn't present, that it's a MBox mailbox, and create it. - Changed the constructor for Gurgitate to take all login information from the effective UID instead of trusting the real UID and the EUID to be the same. 1.4: I seem to have given up on minor changes. More big stuff. - Changed the API! (WARNING WARNING WARNING) Now Message#to_s returns the email message without the "From " line. You get that with Message#to_mbox - Broke the code into lots of little pieces instead of the one great big file. I hope this makes it slightly easier for people to understand--it certainly made it easier for me - Added maildir delivery! Plus a sort of mechanism for delivering to various kinds of mailboxes. 1.3: Some pretty big things here (in my opinion), so I figured it was worth making it a 1.3 release instead of 1.2.2. - Added the ability to say headers["From", "Subject", "Cc"] =~ /hi there/ in your .gurgitate-rules, and have it do the right thing. - Added "to" so you can now say if to =~ /mailing-list/ then ...;end instead of the cumbersome if headers.matches(["To","Cc"],/mailing-list/) then ...; end syntax which I just plain ol' hated. - Made it possible to change the contents of headers. - Made the man page not claim to be user-contributed Perl documentation :-) - Made it deal correctly with headers that have tabs between the colon and the data, instead of spaces (bug #154). 1.2.1: - Made it NOT BLOW UP when you give it an email with a header that has no contents. (Aren't those supposed to be illegal? Regardless, I have to deal with whatever turns up, be it kosher or no) - Made its installer also politely install a man page 1.2: - Fixed some header handling which was kind of, er, broken. Specifically, I'd get an exception on a header which looked like: Subject: Hi there The only kind of email that I've ever seen this kind of header on has been spam, but nonetheless, it's hardly fair for a mail filter to blow up just because it's given crappy input. - Made the comments more rdoc-friendly. Fixed the handling of multi-line headers--it was getting it RONG RONG RONG (but I've probably implemented this RONG RONG RONG too). - Made it so that as well as saying g=Gurgitate::Gurgitate.new(filehandle) if g.head =~ /evil@bad.com/ then delete end you can also say Gurgitate::Gurgitate.new(filehandle) do if head =~ /evil@bad.com/ then delete end end Which gives you another place to put your .gurgitate-rules. - Made install.rb work as both a script as a library, because I switched to using rake to build things, rather than make. - Made it canonicalize headers to a standard capitalization, because some SMTP client (*cough*virus*cough*) is incorrectly sending out all-uppercase headers. 1.1.3: - Put the whole thing into a big module, and added some extra comments to make it more rdoc-friendly. You shouldn't need to change your .gurgitate-rules.rb if you use it like that, but if you use it as a module, you'll need to prefix 'Gurgitate::' to your Gurgitate object construction. 1.1.2: maintenance release - Changed gurgitate-mail so that it compiles cleanly under Ruby 1.8, and made it work with no warnings. 1.1.1: maintenance release - Fixed the regexes for my own email addresses--they had backslashes in strange place, causing people to ask odd questions. Fixed a typo (thanks to Tom Wadlow)--I was using an undefined local variable "sendmail" instead of a class variable "@sendmail". 1.1: - Implemented Pavel Kolar's suggestion that if you filter email through an external program, you might want the results of that, instead of just a return code. - Added a default value to the Gurgitate initializer parameter. - Added an "install.rb" script to install the script and library files into their proper locations. - Pulled the "Gurgitate" class and friends out into a separate file, and made "gurgitate-mail" into a tiny script which does a "require" to pull the gurgitate-mail stuff in. This should make it easier to extend in the future. - Added this file to the distribution. :-) 1.0.1: maintenance release - Small code cleanup--changed some accessors to use attr_*. 1.0: - Initial release of gurgitate-mail gurgitate-mail/INSTALL0000644000076500001440000000012307720326045014552 0ustar dagbrownusersTo install gurgitate-mail, simply run the "install.rb" script: ruby install.rb gurgitate-mail/install.rb0000644000076500001440000000522210747013737015525 0ustar dagbrownusers#!/usr/bin/ruby -w require "rbconfig" require "fileutils" module Gurgitate Package = "gurgitate-mail" class Install def self.mkdir(d) print "Creating #{d}..." begin Dir.mkdir(d) print "\n" rescue Errno::EEXIST if FileTest.directory? d puts "no need, it's already there." else puts "there's something else there already." raise end rescue Errno::ENOENT puts "its parent doesn't exist!" raise end end def self.install(prefix=nil) include Config if prefix then bindir = File.join prefix, "bin"; mkdir bindir dest = File.join prefix, "lib"; mkdir dest mkdir File.join(prefix, "man") mandir = File.join prefix, "man", "man1"; mkdir mandir else version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] sitedir = CONFIG["sitedir"] bindir = CONFIG["bindir"] mandir = File.join(CONFIG["mandir"],"man1") dest = CONFIG["sitelibdir"] end destgur = File.join(dest,"gurgitate") destdel = File.join(destgur,"deliver") print "Installing #{Package}.rb in #{dest}...\n" FileUtils.install("#{Package}.rb", dest, :mode => 0644) mkdir destgur Dir.glob(File.join("gurgitate","*.rb")).each { |f| puts "Installing #{f} in #{destgur}..." FileUtils.install(f,destgur) } mkdir destdel Dir.glob(File.join(File.join("gurgitate","deliver"),"*.rb")).each { |f| puts "Installing #{f} in #{destdel}..." FileUtils.install(f,destdel) } print "Installing #{Package}.1 in #{mandir}...\n" FileUtils.install("#{Package}.man","#{mandir}/#{Package}.1", :mode => 0644) print "Installing #{Package} in #{bindir}...\n" # Not so simple; need to put in the shebang line from_f=File.open("gurgitate-mail") to_f=File.open("#{bindir}/gurgitate-mail","w") to_f.print("#!#{bindir}/ruby -w\n\n") from_f.each do |l| to_f.print l end to_f.close() from_f.close() File.chmod(0755,"#{bindir}/gurgitate-mail") end end end if __FILE__ == $0 then Gurgitate::Install.install() end gurgitate-mail/gurgitate-mail.rb0000644000076500001440000002500410747761261016774 0ustar dagbrownusers#!/opt/bin/ruby #------------------------------------------------------------------------ # Mail filter package #------------------------------------------------------------------------ require 'etc' require 'gurgitate/mailmessage' require 'gurgitate/deliver' module Gurgitate #======================================================================== # The actual gurgitator; reads a message and then it can do # other stuff with it, like save to a mailbox or forward # it somewhere else. class Gurgitate < Mailmessage include Deliver # Instead of the usual attributes, I went with a # reader-is-writer type thing (as seen quite often in Perl and # C++ code) so that in your .gurgitate-rules, you can say # # maildir "#{homedir}/Mail" # sendmail "/usr/sbin/sendmail" # spoolfile "Maildir" # spooldir homedir # # This is because of an oddity in Ruby where, even if an # accessor exists in the current object, if you say: # name = value # it'll always create a local variable. Not quite what you # want when you're trying to set a config parameter. You have # to say "self.name = value", which (I think) is ugly. # # In the interests of promoting harmony, of course, the previous # syntax will continue to work. def self.attr_configparam(*syms) syms.each do |sym| class_eval %/ def #{sym} (*vals) if vals.length == 1 @#{sym} = vals[0] elsif vals.length == 0 @#{sym} else raise ArgumentError, "wrong number of arguments " + "(\#{vals.length} for 0 or 1)" end end # Don't break it for the nice people who use # old-style accessors though. Breaking people's # .gurgitate-rules is a bad idea. attr_writer :#{sym} / end end # The directory you want to put mail folders into attr_configparam :maildir # The path to your log file attr_configparam :logfile # The full path of your "sendmail" program attr_configparam :sendmail # Your home directory attr_configparam :homedir # Your default mail spool attr_configparam :spoolfile # The directory where user mail spools live attr_configparam :spooldir # What kind of mailboxes you prefer # attr_configparam :folderstyle # What kind of mailboxes you prefer def folderstyle(*style) if style.length == 0 then @folderstyle elsif style.length == 1 then if style[0] == Maildir then spooldir homedir spoolfile File.join(spooldir,"Maildir") maildir spoolfile elsif style[0] == MH then mh_profile_path = File.join(ENV["HOME"],".mh_profile") if File.exists?(mh_profile_path) then mh_profile = YAML.load(File.read(mh_profile_path)) maildir mh_profile["Path"] else maildir File.join(ENV["HOME"],"Mail") end spoolfile File.join(maildir,"inbox") else spooldir "/var/spool/mail" spoolfile File.join(spooldir, @passwd.name) end @folderstyle = style[0] else raise ArgumentError, "wrong number of arguments "+ "(#{style.length} for 0 or 1)" end @folderstyle end # Set config params to defaults, read in mail message from # +input+ # input:: # Either the text of the email message in RFC-822 format, # or a filehandle where the email message can be read from # recipient:: # The contents of the envelope recipient parameter # sender:: # The envelope sender parameter # spooldir:: # The location of the mail spools directory. def initialize(input=nil, recipient=nil, sender=nil, spooldir="/var/spool/mail", &block) @passwd = Etc.getpwuid @homedir = @passwd.dir; @maildir = File.join(@passwd.dir,"Mail") @logfile = File.join(@passwd.dir,".gurgitate.log") @sendmail = "/usr/lib/sendmail" @spooldir = spooldir @spoolfile = File.join(@spooldir,@passwd.name ) @folderstyle = MBox @rules = [] input_text = "" input.each_line do |l| input_text << l end super(input_text, recipient, sender) instance_eval(&block) if block_given? end def add_rules(filename, options = {}) if not Hash === options raise ArgumentError.new("Expected hash of options") end if filename == :default filename=homedir+"/.gurgitate-rules" end if not FileTest.exist?(filename) filename = filename + '.rb' end if not FileTest.exist?(filename) if options.has_key?(:user) log("#{filename} does not exist.") end return false end if FileTest.file?(filename) and ( ( not options.has_key? :system and FileTest.owned?(filename) ) or ( options.has_key? :system and options[:system] == true and File.stat(filename).uid == 0 ) ) and FileTest.readable?(filename) @rules << filename else log("#{filename} has bad permissions or ownership, not using rules") return false end end # Deletes the current message. def delete # Well, nothing here, really. end # This is a neat one. You can get any header as a method. # Say, if you want the header "X-Face", then you call # x_face and that gets it for you. It raises NameError if # that header isn't found. # meth:: # The method that the caller tried to call which isn't # handled any other way. def method_missing(meth) headername=meth.to_s.split(/_/).map {|x| x.capitalize}.join("-") if headers[headername] then return headers[headername] else raise NameError,"undefined local variable or method, or header not found `#{meth}' for #{self}:#{self.class}" end end # Forwards the message to +address+. # address:: # A valid email address to forward the message to. def forward(address) self.log "Forwarding to "+address IO.popen(@sendmail+" "+address,"w") do |f| f.print(self.to_s) end end # Writes +message+ to the log file. def log(message) if(@logfile)then File.open(@logfile,"a") do |f| f.flock(File::LOCK_EX) f.print(Time.new.to_s+" "+message+"\n") f.flock(File::LOCK_UN) end end end # Pipes the message through +program+. If +program+ # fails, puts the message into +spoolfile+ def pipe(program) self.log "Piping through "+program IO.popen(program,"w") do |f| f.print(self.to_s) end return $?>>8 end # Pipes the message through +program+, and returns another # +Gurgitate+ object containing the output of the filter def filter(program,&block) self.log "Filtering with "+program IO.popen("-","w+") do |filter| unless filter then begin exec(program) rescue exit! # should not get here anyway end else if fork filter.close_write g=Gurgitate.new(filter) g.instance_eval(&block) if block_given? return g else begin filter.close_read filter.print to_s filter.close rescue nil ensure exit! end end end end end # Processes your .gurgitate-rules.rb. def process(&block) begin if @rules.size > 0 or block @rules.each do |configfilespec| begin eval File.new(configfilespec).read, nil, configfilespec rescue ScriptError log "Couldn't load #{configfilespec}: "+$! save(spoolfile) rescue Exception log "Error while executing #{configfilespec}: #{$!}" $@.each { |tr| log "Backtrace: #{tr}" } folderstyle = MBox save(spoolfile) end end if block instance_eval(&block) end log "Mail not covered by rules, saving to default spool" save(spoolfile) else save(spoolfile) end rescue Exception log "Error while executing rules: #{$!}" $@.each { |tr| log "Backtrace: #{tr}" } log "Attempting to save to spoolfile after error" folderstyle = MBox save(spoolfile) end end end end gurgitate-mail/gurgitate-mail0000644000076500001440000000234110747761261016371 0ustar dagbrownusers#------------------------------------------------------------------------ # Mail filter invocation script #------------------------------------------------------------------------ require "gurgitate-mail" require 'optparse' # change this on installation to taste GLOBAL_RULES="/etc/gurgitate-rules" GLOBAL_RULES_POST="/etc/gurgitate-rules-default" commandline_files = [] mail_sender = nil opts = OptionParser.new do |o| o.on "-f FILE", "--file FILE", "Use FILE as a rules file" do |file| commandline_files << file end o.on "-s SENDER", "--sender SENDER", "Use SENDER as sender" do |sender| mail_sender = sender end o.on_tail "-h", "--help", "Show this message" do puts opts exit end end mail_recipients = opts.parse(ARGV) if mail_recipients.length == 0 then mail_recipients = nil end gurgitate = Gurgitate::Gurgitate.new(STDIN, mail_recipients, mail_sender) if commandline_files.length > 0 commandline_files.each do |file| gurgitate.add_rules(file, :user => true) end else gurgitate.add_rules(GLOBAL_RULES, :system => true) gurgitate.add_rules(:default) gurgitate.add_rules(GLOBAL_RULES_POST, :system => true) end begin gurgitate.process rescue CouldNotProcessMailError exit 75 end gurgitate-mail/gurgitate-mail.html0000644000076500001440000004324310747761262017343 0ustar dagbrownusers gurgitate-mail - an easy-to-use mail filter


NAME

gurgitate-mail - an easy-to-use mail filter


SYNOPSIS

gurgitate-mail


DESCRIPTION

gurgitate-mail is a program which reads your mail and filters it according to the .gurgitate-rules.rb file in your home directory. The configuration file uses Ruby syntax and is thus quite flexible.

It's generally invoked either through your .forward file:

    "|/path/to/gurgitate-mail"

Or through your .procmailrc file:

    :0:
    | /path/to/gurgitate-mail

Alternatively, if you're the sysadmin at your site, or your sysadmin is friendly, you can use gurgitate-mail as a local delivery agent. For postfix, put

    mailbox_command=/opt/bin/gurgitate-mail

in /etc/postfix/main.cf. If you use any other MTA, and configure gurgitate-mail as a local delivery agent, please tell me how! I want to include this in the documentation.


CONFIGURATION FILES

There are three configuration files used by gurgitate-mail: two are system-wide, and the third, is the user rules file.

The two system-wide configuration files are /etc/gurgitate-rules and /etc/gurgitate-rules-default. These are processed before and after the user rules, respectively.

/etc/gurgitate-rules is used to handle system-wide filtering needs: setting the default mailbox style to Maildir rather than the default MBox, setting the spool directory, things like that.

The user configuration file is $HOME/.gurgitate-rules (or, alternatively, $HOME/.gurgitate-rules.rb. Either work). You put your own rules here. If the user configuration file doesn't encounter a "return" during processing, then the additional rules contained in /etc/gurgitate-rules-default are run. If that also doesn't return, then mail messages are saved into the default mail spool location.

If the -f option is used on the commandline, then the file specified will be used and the default rules will not. The -f option can be used more than once:

    gurgitate-mail -f test-rules -f additional-rules


CONFIGURATION PARAMETERS

There are several parameters that you can set to change the way that gurgitate-mail behaves. You set a config parameter by saying, for instance:

    sendmail "/usr/sbin/sendmail"

which sets the "sendmail" parameter to "/usr/sbin/sendmail".

maildir

The directory you want to put mail folders into. This defaults to $HOME/Mail.

logfile

Where you went gurgitate-mail's log messages to go to. The standard location for this is $HOME/.gurgitate.log

sendmail

The full path to the sendmail program, used to deliver mail. This can be any program that takes as its parameters the list of addresses to deliver mail to, and that takes a mail message on standard input.

homedir

The full path of your home directory. This defaults to whatever your actual home directory is.

spooldir

The path where the system's mail spools goes to. This defaults to "/var/spool/mail". On a Maildir system, this should be set to the same as "homedir".

spoolfile

The mail spool file component of the full path of your mail spool. This is generally your username. Maildir users should set this to "Maildir".

folderstyle

The style of folders you prefer. This can be (at the moment) either MBox or Maildir.


FILTER RULES

The filter rules are a series of Ruby statements, with the following methods and variables available:

Variables

from

This contains the envelope "from" address of the email message. (Note that this isn't necessarily the same as the contents of the "From:" header)

headers

This is an object containing the headers of the message. There are several methods that come with this object:

body

This contains the body of the email message. As of yet, there's nothing really interesting which you can do with this, apart from assigning to it; you can rewrite the body of an email message this way. Dealing with attachments is planned for a future release of gurgitate-mail.

maildir

The directory which contains the folders, used by the save method when you specify a folder as "=folder" (like Elm). Defaults to "$HOME/Mail".

homedir

Your home directory. Read-only.

logfile

The location of the gurgitate-mail logfile. If set to nil, then no logging is done. Defaults to "$HOME/.gurgitate.log".

sendmail

The location of the sendmail program. Used by the forward method. Defaults to "/usr/lib/sendmail".

spoolfile

The location of the mail spool. Read-only.

Methods

matches(name(s),regex)

Returns true if the header name matches the regular expression regex. If name is an array of header names, then it returns true if at least one of the headers matches. Useful for testing whether both "To:" and "Cc:" headers match.

from

Returns the envelope "from" address of the email message. Note that this is the same as the bare "from".

to

Returns a HeaderBag (a kind of array) with the contents of the "To" and the "Cc" headers.

to_s

As per Ruby convention, returns all the headers as a String object.

save(mailbox)

This saves the message to a mailbox. You can specify the mailbox as a word with an = sign in front of it, in which case it puts it into maildir. If you don't use the =name format, then you need to specify an absolute pathname. If it can't write the message to the file you request it to, it'll attempt to write it to spoolfile.

forward(address)

This forwards the email message to another email address.

pipe(program)

This pipes the message through program. pipe returns the exit code of the program that the message was piped through.

filter(program)

This pipes the message through program and returns a new Gurgitate object containing the filtered mail. (This is handy for external filters which modify email like, for example, SpamAssassin, which adds a spam-score header.)

You can also say

    filter(program) do
        # code here
    end

and it yields the newly-created Gurgitate object to the block.

headers

This returns the headers as an object of their own. This object has its own methods:

headers[*headernames]

This returns a HeaderBag (a subclass of array) containing the headers you asked for. You can then use the =~ operator on this result to match the RHS regex with everything in the HeaderBag.

You can change a header's value with headers[name]=newvalue.

headers.match(name,regex)

Matches the header with the name "name" against the regex. This is the same as headers[name] =~ /regex/.

headers.matches(names,regex)

Matches the headers with the names "names" against the regex. This is the same as headers[*names] =~ /regex/.

headers.from

Returns the envelope from. You can change this with headers.from=newaddress too.

return

This tells gurgitate-mail to stop processing the email message. If you don't use return, then gurgitate-mail will continue processing the same mail again with the next rule. If there isn't a return at the end of gurgitate-rules.rb, then gurgitate-mail will save the email message in the normal mail spool.

log(message)

This writes a log message to the log file.


SIMPLE EXAMPLES

Here are some examples of gurgitate-mail rules, with explanations:

    if from =~ /ebay.com/ then save("=ebay"); return; end

Any email from eBay (automatic end-of-auction notifications, for example, and outbid notices) gets filed into the "ebay" folder.

    if from =~ /root@/ then save("=root"); return; end

Any email from root (at any host) gets filed into a special folder. Useful for sysadmins monitoring crontab email.

    if headers.matches(["To","Cc"],"webmaster@") then
        save("=webmaster")
        return
    end

Any email with a To: or Cc: line of "sysadmin" is saved to a "sysadmin" folder. Useful for people with multiple role accounts redirected to their address.

    if headers["Subject"] =~ /\[SPAM\]/ then
        save("=spam")
        return
    end

This is a different syntax for matching patterns against headers. You can also match multiple headers in the square brackets.

    if headers["Subject","Keywords"] =~ /a bad word/ then
        save("=swearing")
        return
    end

Searches for "a bad word" in the Subject and Keywords headers, and if it's there, saves the email in the "swearing" folder.

    if headers.matches(["To","Cc"],"mailing-list@example.com") then
        pipe("|rcvstore +mailing-list")
        return
    end

Any email to a mailing list is piped through "rcvstore" to store it into an MH folder.

That

    headers.matches(["To","Cc"],/regex/)

idiom happens often enough that there's a shorthand for it:

    if to =~ /mailing-list@example.com/ then
        pipe("|rcvstore +mailing-list")
        return
    end

Pipes the mail to the mailing list through "rcvstore".


ADVANCED EXAMPLES

Here are some slightly more clever examples to give you an idea of what you can do with gurgitate-mail. Let's suppose you have an email whitelist in a file called $HOME/.friends, so you can determine whether some email is likely to be spam or not.

Then if someone on your whitelist sends you email, then you automatically save that into the "inbox" folder:

    friends=homedir+"/.friends"
    if FileTest.exists?(friends) and FileTest.readable?(friends) then
        File.new(friends).each do |friend|
            if from =~ friend.chomp then
                log "Mail from friend "+friend.chomp
                save("=inbox")
                return
            end
        end
    end

Okay, if someone sends you email, and it's addressed specifically to you (and gurgitate-mail hasn't caught it in another form already), then it might or might not be spam: put it into a "grey" folder:

    my_addresses= [ /me@example\.com/i,
                    /me@example\.org/i,
                    /me@example\.net/i];  # I have three email addresses
    my_addresses.each do |addr|
        if headers.matches(["To","Cc"],addr) then
            save("=possibly-not-spam")
            return
        end
    end

And after that, if it's not from someone you know, and it's not addressed to your email address either, then it's probably save to assume that it's spam:

    save("=spam")
    return

This can be improved by using a Bayesian filter, though; for example, Eric Raymond's bogofilter program (http://bogofilter.sourceforge.net) can be automatically trained and used with the help of the white/grey/black distinctions. Taking the example above, I'll adjust it by adding in calls to bogofilter:

    friends=homedir+"/.friends"
    if FileTest.exists?(friends) and FileTest.readable?(friends) then
        File.new(friends).each do |friend|
            if from =~ friend.chomp then
                log "Mail from friend "+friend.chomp
                pipe("bogofilter -h")  # <-- LINE ADDED HERE
                save("=inbox")
                return
            end
        end
    end

bogofilter -h trains bogofilter that mail from whitelisted-people is not to be considered spam. Okay, at the end of the .gurgitate-rules, change

    save("=spam")
    return

to

    save("=spam")
    pipe("bogofilter -s")
    return

This trains bogofilter that anything which doesn't pass the rest of the filter should be considered spam. Now for the interesting bit: Change the bit between these to use "bogofilter" to decide whether email is to be considered spam or not:

    my_addresses= [ /me@example\.com/i,
                    /me@example\.org/i,
                    /me@example\.net/i];  # I have three email addresses
    my_addresses.each do |addr|
        if headers.matches(["To","Cc"],addr) then
            if pipe("bogofilter")==1
            then
                log("bogofilter suspects it might not be spam")
                save("=possibly-not-spam")
            else
                log("bogofilter thinks it's probably spam")
                save("=spam")
            end
            return
        end
    end

bogofilter has an exit code of "1" if it thinks the message is not spam, and "0" if it thinks the message is spam.

Hopefully this should give you an idea of the kinds of things that you can use bogofilter for.


AUTHOR

Dave Brown <gurgitate-mail@dagbrown.com>

gurgitate-mail/gurgitate-mail.man0000644000076500001440000004435310747761263017156 0ustar dagbrownusers.\" Automatically generated by Pod::Man 2.16 (Pod::Simple 3.05) .\" .\" Standard preamble: .\" ======================================================================== .de Sh \" Subsection heading .br .if t .Sp .ne 5 .PP \fB\\$1\fR .PP .. .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.Sh), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .ie \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .el \{\ . de IX .. .\} .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "GURGITATE-MAIL 1" .TH GURGITATE-MAIL 1 "2006-06-07" "perl v5.10.0" "Gurgitate-Mail" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" gurgitate\-mail \- an easy\-to\-use mail filter .SH "SYNOPSIS" .IX Header "SYNOPSIS" gurgitate-mail .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\f(CW\*(C`gurgitate\-mail\*(C'\fR is a program which reads your mail and filters it according to the \fI.gurgitate\-rules.rb\fR file in your home directory. The configuration file uses Ruby syntax and is thus quite flexible. .PP It's generally invoked either through your \fI.forward\fR file: .PP .Vb 1 \& "|/path/to/gurgitate\-mail" .Ve .PP Or through your \fI.procmailrc\fR file: .PP .Vb 2 \& :0: \& | /path/to/gurgitate\-mail .Ve .PP Alternatively, if you're the sysadmin at your site, or your sysadmin is friendly, you can use gurgitate-mail as a local delivery agent. For postfix, put .PP .Vb 1 \& mailbox_command=/opt/bin/gurgitate\-mail .Ve .PP in \fI/etc/postfix/main.cf\fR. If you use any other \s-1MTA\s0, and configure gurgitate-mail as a local delivery agent, please tell me how! I want to include this in the documentation. .SH "CONFIGURATION FILES" .IX Header "CONFIGURATION FILES" There are three configuration files used by gurgitate-mail: two are system-wide, and the third, is the user rules file. .PP The two system-wide configuration files are \fI/etc/gurgitate\-rules\fR and \&\fI/etc/gurgitate\-rules\-default\fR. These are processed before and after the user rules, respectively. .PP \&\fI/etc/gurgitate\-rules\fR is used to handle system-wide filtering needs: setting the default mailbox style to Maildir rather than the default MBox, setting the spool directory, things like that. .PP The user configuration file is \fI\f(CI$HOME\fI/.gurgitate\-rules\fR (or, alternatively, \fI\f(CI$HOME\fI/.gurgitate\-rules.rb\fR. Either work). You put your own rules here. If the user configuration file doesn't encounter a \*(L"return\*(R" during processing, then the additional rules contained in \fI/etc/gurgitate\-rules\-default\fR are run. If that also doesn't return, then mail messages are saved into the default mail spool location. .PP If the \f(CW\*(C`\-f\*(C'\fR option is used on the commandline, then the file specified will be used and the default rules will not. The \f(CW\*(C`\-f\*(C'\fR option can be used more than once: .PP .Vb 1 \& gurgitate\-mail \-f test\-rules \-f additional\-rules .Ve .SH "CONFIGURATION PARAMETERS" .IX Header "CONFIGURATION PARAMETERS" There are several parameters that you can set to change the way that gurgitate-mail behaves. You set a config parameter by saying, for instance: .PP .Vb 1 \& sendmail "/usr/sbin/sendmail" .Ve .PP which sets the \*(L"sendmail\*(R" parameter to \*(L"/usr/sbin/sendmail\*(R". .IP "maildir" 5 .IX Item "maildir" The directory you want to put mail folders into. This defaults to \&\f(CW$HOME\fR/Mail. .IP "logfile" 5 .IX Item "logfile" Where you went gurgitate-mail's log messages to go to. The standard location for this is \f(CW$HOME\fR/.gurgitate.log .IP "sendmail" 5 .IX Item "sendmail" The full path to the sendmail program, used to deliver mail. This can be any program that takes as its parameters the list of addresses to deliver mail to, and that takes a mail message on standard input. .IP "homedir" 5 .IX Item "homedir" The full path of your home directory. This defaults to whatever your actual home directory is. .IP "spooldir" 5 .IX Item "spooldir" The path where the system's mail spools goes to. This defaults to \&\*(L"/var/spool/mail\*(R". On a Maildir system, this should be set to the same as \*(L"homedir\*(R". .IP "spoolfile" 5 .IX Item "spoolfile" The mail spool file component of the full path of your mail spool. This is generally your username. Maildir users should set this to \&\*(L"Maildir\*(R". .IP "folderstyle" 5 .IX Item "folderstyle" The style of folders you prefer. This can be (at the moment) either MBox or Maildir. .SH "FILTER RULES" .IX Header "FILTER RULES" The filter rules are a series of Ruby statements, with the following methods and variables available: .Sh "Variables" .IX Subsection "Variables" .IP "from" 5 .IX Item "from" This contains the envelope \*(L"from\*(R" address of the email message. (Note that this isn't necessarily the same as the contents of the \&\*(L"From:\*(R" header) .IP "headers" 5 .IX Item "headers" This is an object containing the headers of the message. There are several methods that come with this object: .IP "body" 5 .IX Item "body" This contains the body of the email message. As of yet, there's nothing really interesting which you can do with this, apart from assigning to it; you can rewrite the body of an email message this way. Dealing with attachments is planned for a future release of \&\f(CW\*(C`gurgitate\-mail\*(C'\fR. .IP "maildir" 5 .IX Item "maildir" The directory which contains the folders, used by the \f(CW\*(C`save\*(C'\fR method when you specify a folder as "=\fIfolder\fR\*(L" (like Elm). Defaults to \*(R"$HOME/Mail". .IP "homedir" 5 .IX Item "homedir" Your home directory. Read-only. .IP "logfile" 5 .IX Item "logfile" The location of the \f(CW\*(C`gurgitate\-mail\*(C'\fR logfile. If set to \f(CW\*(C`nil\*(C'\fR, then no logging is done. Defaults to \*(L"$HOME/.gurgitate.log\*(R". .IP "sendmail" 5 .IX Item "sendmail" The location of the \f(CW\*(C`sendmail\*(C'\fR program. Used by the \f(CW\*(C`forward\*(C'\fR method. Defaults to \*(L"/usr/lib/sendmail\*(R". .IP "spoolfile" 5 .IX Item "spoolfile" The location of the mail spool. Read-only. .Sh "Methods" .IX Subsection "Methods" .IP "matches(name(s),regex)" 5 .IX Item "matches(name(s),regex)" Returns \f(CW\*(C`true\*(C'\fR if the header \f(CW\*(C`name\*(C'\fR matches the regular expression \f(CW\*(C`regex\*(C'\fR. If \f(CW\*(C`name\*(C'\fR is an array of header names, then it returns true if at least one of the headers matches. Useful for testing whether both \*(L"To:\*(R" and \*(L"Cc:\*(R" headers match. .IP "from" 5 .IX Item "from" Returns the envelope \*(L"from\*(R" address of the email message. Note that this is the same as the bare \*(L"from\*(R". .IP "to" 5 .IX Item "to" Returns a HeaderBag (a kind of array) with the contents of the \&\*(L"To\*(R" and the \*(L"Cc\*(R" headers. .IP "to_s" 5 .IX Item "to_s" As per Ruby convention, returns all the headers as a \f(CW\*(C`String\*(C'\fR object. .IP "save(mailbox)" 5 .IX Item "save(mailbox)" This saves the message to a mailbox. You can specify the mailbox as a word with an = sign in front of it, in which case it puts it into \f(CW\*(C`maildir\*(C'\fR. If you don't use the =\fIname\fR format, then you need to specify an absolute pathname. If it can't write the message to the file you request it to, it'll attempt to write it to \f(CW\*(C`spoolfile\*(C'\fR. .IP "forward(address)" 5 .IX Item "forward(address)" This forwards the email message to another email address. .IP "pipe(program)" 5 .IX Item "pipe(program)" This pipes the message through \f(CW\*(C`program\*(C'\fR. \f(CW\*(C`pipe\*(C'\fR returns the exit code of the program that the message was piped through. .IP "filter(program)" 5 .IX Item "filter(program)" This pipes the message through \f(CW\*(C`program\*(C'\fR and returns a new Gurgitate object containing the filtered mail. (This is handy for external filters which modify email like, for example, SpamAssassin, which adds a spam-score header.) .Sp You can also say .Sp .Vb 3 \& filter(program) do \& # code here \& end .Ve .Sp and it yields the newly-created Gurgitate object to the block. .IP "headers" 5 .IX Item "headers" This returns the headers as an object of their own. This object has its own methods: .RS 5 .IP "headers[*headernames]" 5 .IX Item "headers[*headernames]" This returns a HeaderBag (a subclass of array) containing the headers you asked for. You can then use the =~ operator on this result to match the \s-1RHS\s0 regex with everything in the HeaderBag. .Sp You can change a header's value with \f(CW\*(C`headers[name]=newvalue\*(C'\fR. .IP "headers.match(name,regex)" 5 .IX Item "headers.match(name,regex)" Matches the header with the name \*(L"name\*(R" against the regex. This is the same as headers[name] =~ /regex/. .IP "headers.matches(names,regex)" 5 .IX Item "headers.matches(names,regex)" Matches the headers with the names \*(L"names\*(R" against the regex. This is the same as headers[*names] =~ /regex/. .IP "headers.from" 5 .IX Item "headers.from" Returns the envelope from. You can change this with \&\f(CW\*(C`headers.from=newaddress\*(C'\fR too. .RE .RS 5 .RE .IP "return" 5 .IX Item "return" This tells \f(CW\*(C`gurgitate\-mail\*(C'\fR to stop processing the email message. If you don't use \f(CW\*(C`return\*(C'\fR, then \f(CW\*(C`gurgitate\-mail\*(C'\fR will continue processing the same mail again with the next rule. If there isn't a \f(CW\*(C`return\*(C'\fR at the end of \fIgurgitate\-rules.rb\fR, then \&\f(CW\*(C`gurgitate\-mail\*(C'\fR will save the email message in the normal mail spool. .IP "log(message)" 5 .IX Item "log(message)" This writes a log message to the log file. .SH "SIMPLE EXAMPLES" .IX Header "SIMPLE EXAMPLES" Here are some examples of \f(CW\*(C`gurgitate\-mail\*(C'\fR rules, with explanations: .PP .Vb 1 \& if from =~ /ebay.com/ then save("=ebay"); return; end .Ve .PP Any email from eBay (automatic end-of-auction notifications, for example, and outbid notices) gets filed into the \*(L"ebay\*(R" folder. .PP .Vb 1 \& if from =~ /root@/ then save("=root"); return; end .Ve .PP Any email from root (at any host) gets filed into a special folder. Useful for sysadmins monitoring crontab email. .PP .Vb 4 \& if headers.matches(["To","Cc"],"webmaster@") then \& save("=webmaster") \& return \& end .Ve .PP Any email with a To: or Cc: line of \*(L"sysadmin\*(R" is saved to a \&\*(L"sysadmin\*(R" folder. Useful for people with multiple role accounts redirected to their address. .PP .Vb 4 \& if headers["Subject"] =~ /\e[SPAM\e]/ then \& save("=spam") \& return \& end .Ve .PP This is a different syntax for matching patterns against headers. You can also match multiple headers in the square brackets. .PP .Vb 4 \& if headers["Subject","Keywords"] =~ /a bad word/ then \& save("=swearing") \& return \& end .Ve .PP Searches for \*(L"a bad word\*(R" in the Subject and Keywords headers, and if it's there, saves the email in the \*(L"swearing\*(R" folder. .PP .Vb 4 \& if headers.matches(["To","Cc"],"mailing\-list@example.com") then \& pipe("|rcvstore +mailing\-list") \& return \& end .Ve .PP Any email to a mailing list is piped through \*(L"rcvstore\*(R" to store it into an \s-1MH\s0 folder. .PP That .PP .Vb 1 \& headers.matches(["To","Cc"],/regex/) .Ve .PP idiom happens often enough that there's a shorthand for it: .PP .Vb 4 \& if to =~ /mailing\-list@example.com/ then \& pipe("|rcvstore +mailing\-list") \& return \& end .Ve .PP Pipes the mail to the mailing list through \*(L"rcvstore\*(R". .SH "ADVANCED EXAMPLES" .IX Header "ADVANCED EXAMPLES" Here are some slightly more clever examples to give you an idea of what you can do with \f(CW\*(C`gurgitate\-mail\*(C'\fR. Let's suppose you have an email whitelist in a file called \fI\f(CI$HOME\fI/.friends\fR, so you can determine whether some email is likely to be spam or not. .PP Then if someone on your whitelist sends you email, then you automatically save that into the \*(L"inbox\*(R" folder: .PP .Vb 10 \& friends=homedir+"/.friends" \& if FileTest.exists?(friends) and FileTest.readable?(friends) then \& File.new(friends).each do |friend| \& if from =~ friend.chomp then \& log "Mail from friend "+friend.chomp \& save("=inbox") \& return \& end \& end \& end .Ve .PP Okay, if someone sends you email, and it's addressed specifically to you (and gurgitate-mail hasn't caught it in another form already), then it might or might not be spam: put it into a \*(L"grey\*(R" folder: .PP .Vb 9 \& my_addresses= [ /me@example\e.com/i, \& /me@example\e.org/i, \& /me@example\e.net/i]; # I have three email addresses \& my_addresses.each do |addr| \& if headers.matches(["To","Cc"],addr) then \& save("=possibly\-not\-spam") \& return \& end \& end .Ve .PP And after that, if it's not from someone you know, and it's not addressed to your email address either, then it's probably save to assume that it's spam: .PP .Vb 2 \& save("=spam") \& return .Ve .PP This can be improved by using a Bayesian filter, though; for example, Eric Raymond's bogofilter program (http://bogofilter.sourceforge.net) can be automatically trained and used with the help of the white/grey/black distinctions. Taking the example above, I'll adjust it by adding in calls to bogofilter: .PP .Vb 11 \& friends=homedir+"/.friends" \& if FileTest.exists?(friends) and FileTest.readable?(friends) then \& File.new(friends).each do |friend| \& if from =~ friend.chomp then \& log "Mail from friend "+friend.chomp \& pipe("bogofilter \-h") # <\-\- LINE ADDED HERE \& save("=inbox") \& return \& end \& end \& end .Ve .PP \&\f(CW\*(C`bogofilter \-h\*(C'\fR trains bogofilter that mail from whitelisted-people is not to be considered spam. Okay, at the end of the \&.gurgitate\-rules, change .PP .Vb 2 \& save("=spam") \& return .Ve .PP to .PP .Vb 3 \& save("=spam") \& pipe("bogofilter \-s") \& return .Ve .PP This trains \f(CW\*(C`bogofilter\*(C'\fR that anything which doesn't pass the rest of the filter should be considered spam. Now for the interesting bit: Change the bit between these to use \*(L"bogofilter\*(R" to decide whether email is to be considered spam or not: .PP .Vb 10 \& my_addresses= [ /me@example\e.com/i, \& /me@example\e.org/i, \& /me@example\e.net/i]; # I have three email addresses \& my_addresses.each do |addr| \& if headers.matches(["To","Cc"],addr) then \& if pipe("bogofilter")==1 \& then \& log("bogofilter suspects it might not be spam") \& save("=possibly\-not\-spam") \& else \& log("bogofilter thinks it\*(Aqs probably spam") \& save("=spam") \& end \& return \& end \& end .Ve .PP \&\f(CW\*(C`bogofilter\*(C'\fR has an exit code of \*(L"1\*(R" if it thinks the message is not spam, and \*(L"0\*(R" if it thinks the message is spam. .PP Hopefully this should give you an idea of the kinds of things that you can use \f(CW\*(C`bogofilter\*(C'\fR for. .SH "AUTHOR" .IX Header "AUTHOR" Dave Brown gurgitate-mail/README0000644000076500001440000003237310747761263014425 0ustar dagbrownusersNAME gurgitate-mail - an easy-to-use mail filter SYNOPSIS gurgitate-mail DESCRIPTION "gurgitate-mail" is a program which reads your mail and filters it according to the .gurgitate-rules.rb file in your home directory. The configuration file uses Ruby syntax and is thus quite flexible. It's generally invoked either through your .forward file: "|/path/to/gurgitate-mail" Or through your .procmailrc file: :0: | /path/to/gurgitate-mail Alternatively, if you're the sysadmin at your site, or your sysadmin is friendly, you can use gurgitate-mail as a local delivery agent. For postfix, put mailbox_command=/opt/bin/gurgitate-mail in /etc/postfix/main.cf. If you use any other MTA, and configure gurgitate-mail as a local delivery agent, please tell me how! I want to include this in the documentation. CONFIGURATION FILES There are three configuration files used by gurgitate-mail: two are system-wide, and the third, is the user rules file. The two system-wide configuration files are /etc/gurgitate-rules and /etc/gurgitate-rules-default. These are processed before and after the user rules, respectively. /etc/gurgitate-rules is used to handle system-wide filtering needs: setting the default mailbox style to Maildir rather than the default MBox, setting the spool directory, things like that. The user configuration file is $HOME/.gurgitate-rules (or, alternatively, $HOME/.gurgitate-rules.rb. Either work). You put your own rules here. If the user configuration file doesn't encounter a "return" during processing, then the additional rules contained in /etc/gurgitate-rules-default are run. If that also doesn't return, then mail messages are saved into the default mail spool location. If the "-f" option is used on the commandline, then the file specified will be used and the default rules will not. The "-f" option can be used more than once: gurgitate-mail -f test-rules -f additional-rules CONFIGURATION PARAMETERS There are several parameters that you can set to change the way that gurgitate-mail behaves. You set a config parameter by saying, for instance: sendmail "/usr/sbin/sendmail" which sets the "sendmail" parameter to "/usr/sbin/sendmail". maildir The directory you want to put mail folders into. This defaults to $HOME/Mail. logfile Where you went gurgitate-mail's log messages to go to. The standard location for this is $HOME/.gurgitate.log sendmail The full path to the sendmail program, used to deliver mail. This can be any program that takes as its parameters the list of addresses to deliver mail to, and that takes a mail message on standard input. homedir The full path of your home directory. This defaults to whatever your actual home directory is. spooldir The path where the system's mail spools goes to. This defaults to "/var/spool/mail". On a Maildir system, this should be set to the same as "homedir". spoolfile The mail spool file component of the full path of your mail spool. This is generally your username. Maildir users should set this to "Maildir". folderstyle The style of folders you prefer. This can be (at the moment) either MBox or Maildir. FILTER RULES The filter rules are a series of Ruby statements, with the following methods and variables available: Variables from This contains the envelope "from" address of the email message. (Note that this isn't necessarily the same as the contents of the "From:" header) headers This is an object containing the headers of the message. There are several methods that come with this object: body This contains the body of the email message. As of yet, there's nothing really interesting which you can do with this, apart from assigning to it; you can rewrite the body of an email message this way. Dealing with attachments is planned for a future release of "gurgitate-mail". maildir The directory which contains the folders, used by the "save" method when you specify a folder as "=folder" (like Elm). Defaults to "$HOME/Mail". homedir Your home directory. Read-only. logfile The location of the "gurgitate-mail" logfile. If set to "nil", then no logging is done. Defaults to "$HOME/.gurgitate.log". sendmail The location of the "sendmail" program. Used by the "forward" method. Defaults to "/usr/lib/sendmail". spoolfile The location of the mail spool. Read-only. Methods matches(name(s),regex) Returns "true" if the header "name" matches the regular expression "regex". If "name" is an array of header names, then it returns true if at least one of the headers matches. Useful for testing whether both "To:" and "Cc:" headers match. from Returns the envelope "from" address of the email message. Note that this is the same as the bare "from". to Returns a HeaderBag (a kind of array) with the contents of the "To" and the "Cc" headers. to_s As per Ruby convention, returns all the headers as a "String" object. save(mailbox) This saves the message to a mailbox. You can specify the mailbox as a word with an = sign in front of it, in which case it puts it into "maildir". If you don't use the =name format, then you need to specify an absolute pathname. If it can't write the message to the file you request it to, it'll attempt to write it to "spoolfile". forward(address) This forwards the email message to another email address. pipe(program) This pipes the message through "program". "pipe" returns the exit code of the program that the message was piped through. filter(program) This pipes the message through "program" and returns a new Gurgitate object containing the filtered mail. (This is handy for external filters which modify email like, for example, SpamAssassin, which adds a spam-score header.) You can also say filter(program) do # code here end and it yields the newly-created Gurgitate object to the block. headers This returns the headers as an object of their own. This object has its own methods: headers[*headernames] This returns a HeaderBag (a subclass of array) containing the headers you asked for. You can then use the =~ operator on this result to match the RHS regex with everything in the HeaderBag. You can change a header's value with "headers[name]=newvalue". headers.match(name,regex) Matches the header with the name "name" against the regex. This is the same as headers[name] =~ /regex/. headers.matches(names,regex) Matches the headers with the names "names" against the regex. This is the same as headers[*names] =~ /regex/. headers.from Returns the envelope from. You can change this with "headers.from=newaddress" too. return This tells "gurgitate-mail" to stop processing the email message. If you don't use "return", then "gurgitate-mail" will continue processing the same mail again with the next rule. If there isn't a "return" at the end of gurgitate-rules.rb, then "gurgitate-mail" will save the email message in the normal mail spool. log(message) This writes a log message to the log file. SIMPLE EXAMPLES Here are some examples of "gurgitate-mail" rules, with explanations: if from =~ /ebay.com/ then save("=ebay"); return; end Any email from eBay (automatic end-of-auction notifications, for example, and outbid notices) gets filed into the "ebay" folder. if from =~ /root@/ then save("=root"); return; end Any email from root (at any host) gets filed into a special folder. Useful for sysadmins monitoring crontab email. if headers.matches(["To","Cc"],"webmaster@") then save("=webmaster") return end Any email with a To: or Cc: line of "sysadmin" is saved to a "sysadmin" folder. Useful for people with multiple role accounts redirected to their address. if headers["Subject"] =~ /\[SPAM\]/ then save("=spam") return end This is a different syntax for matching patterns against headers. You can also match multiple headers in the square brackets. if headers["Subject","Keywords"] =~ /a bad word/ then save("=swearing") return end Searches for "a bad word" in the Subject and Keywords headers, and if it's there, saves the email in the "swearing" folder. if headers.matches(["To","Cc"],"mailing-list@example.com") then pipe("|rcvstore +mailing-list") return end Any email to a mailing list is piped through "rcvstore" to store it into an MH folder. That headers.matches(["To","Cc"],/regex/) idiom happens often enough that there's a shorthand for it: if to =~ /mailing-list@example.com/ then pipe("|rcvstore +mailing-list") return end Pipes the mail to the mailing list through "rcvstore". ADVANCED EXAMPLES Here are some slightly more clever examples to give you an idea of what you can do with "gurgitate-mail". Let's suppose you have an email whitelist in a file called $HOME/.friends, so you can determine whether some email is likely to be spam or not. Then if someone on your whitelist sends you email, then you automatically save that into the "inbox" folder: friends=homedir+"/.friends" if FileTest.exists?(friends) and FileTest.readable?(friends) then File.new(friends).each do |friend| if from =~ friend.chomp then log "Mail from friend "+friend.chomp save("=inbox") return end end end Okay, if someone sends you email, and it's addressed specifically to you (and gurgitate-mail hasn't caught it in another form already), then it might or might not be spam: put it into a "grey" folder: my_addresses= [ /me@example\.com/i, /me@example\.org/i, /me@example\.net/i]; # I have three email addresses my_addresses.each do |addr| if headers.matches(["To","Cc"],addr) then save("=possibly-not-spam") return end end And after that, if it's not from someone you know, and it's not addressed to your email address either, then it's probably save to assume that it's spam: save("=spam") return This can be improved by using a Bayesian filter, though; for example, Eric Raymond's bogofilter program (http://bogofilter.sourceforge.net) can be automatically trained and used with the help of the white/grey/black distinctions. Taking the example above, I'll adjust it by adding in calls to bogofilter: friends=homedir+"/.friends" if FileTest.exists?(friends) and FileTest.readable?(friends) then File.new(friends).each do |friend| if from =~ friend.chomp then log "Mail from friend "+friend.chomp pipe("bogofilter -h") # <-- LINE ADDED HERE save("=inbox") return end end end "bogofilter -h" trains bogofilter that mail from whitelisted-people is not to be considered spam. Okay, at the end of the .gurgitate-rules, change save("=spam") return to save("=spam") pipe("bogofilter -s") return This trains "bogofilter" that anything which doesn't pass the rest of the filter should be considered spam. Now for the interesting bit: Change the bit between these to use "bogofilter" to decide whether email is to be considered spam or not: my_addresses= [ /me@example\.com/i, /me@example\.org/i, /me@example\.net/i]; # I have three email addresses my_addresses.each do |addr| if headers.matches(["To","Cc"],addr) then if pipe("bogofilter")==1 then log("bogofilter suspects it might not be spam") save("=possibly-not-spam") else log("bogofilter thinks it's probably spam") save("=spam") end return end end "bogofilter" has an exit code of "1" if it thinks the message is not spam, and "0" if it thinks the message is spam. Hopefully this should give you an idea of the kinds of things that you can use "bogofilter" for. AUTHOR Dave Brown gurgitate-mail/gurgitate/deliver.rb0000644000076500001440000000564310747761263017517 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # Code to handle saving a message to a mailbox (and a framework for detecting # what kind of mailbox it is) #------------------------------------------------------------------------ require "gurgitate/deliver/mbox" require "gurgitate/deliver/maildir" require "gurgitate/deliver/mh" module Gurgitate module Deliver class MailboxFound < Exception # more of a "flag" than an exception really end # Saves a message to +mailbox+, after detecting what the mailbox's # format is. # mailbox:: # A string containing the path of the mailbox to save # the message to. If it is of the form "=mailbox", it # saves the message to +Maildir+/+mailbox+. Otherwise, # it simply saves the message to the file +mailbox+. def save(mailbox) if mailbox[0,1]=='=' and @maildir != nil then if @folderstyle == Maildir and mailbox !~ /^=\./ then mailbox["="]=@maildir+"/." else mailbox["="]=@maildir+"/" end end if mailbox[0,1] != '/' then log("Cannot save to relative filenames! Saving to spool file"); mailbox=spoolfile end begin [MBox,Maildir,MH].each do |mod| if mod::check_mailbox(mailbox) then extend mod raise MailboxFound end end # Huh, nothing could find anything. Oh well, # let's default to whatever's in @folderstyle. (I # guess we'll be making a new mailbox, eh?) if Module === @folderstyle then # # Careful we don't get the wrong instance variable folderstyle=@folderstyle extend folderstyle else # No hints from the user either. Let's guess! # I'll use the same heuristic that Postfix uses--if the # mailbox name ends with a /, then make it a Maildir, # otherwise make it a mail file if mailbox =~ /\/$/ then extend Maildir else extend MBox end end rescue MailboxFound # Don't need to do anything--we only have to worry # about it if there wasn't a mailbox there. nil end begin deliver_message(mailbox) rescue SystemCallError self.log "Gack! Something went wrong: " + $!.to_s raise # exit 75 end end end end gurgitate-mail/gurgitate/headers.rb0000644000076500001440000002272610747761263017501 0ustar dagbrownusers#!/opt/bin/ruby -w require "gurgitate/header" module Gurgitate class IllegalHeader < RuntimeError ; end # ======================================================================== class HeaderBag < Array def =~(regex) inject(false) do |y,x| y or ( ( x =~ regex ) != nil ) end end def sub!(regex, replacement) each do |header| header.contents = header.contents.sub regex, replacement end end def sub(regex, replacement) ::Gurgitate::HeaderBag.new( self.map do |header| ::Gurgitate::Header.new( "#{header.name}: " + header.contents.sub(regex, replacement) ) end ) end def to_s map do |member| member.to_s end.join "" end end # A slightly bigger class for all of a message's headers class Headers private # Figures out whether the first line of a mail message is an # mbox-style "From " line (say, if you get this from sendmail), # or whether it's just a normal header. # -- # If you run "fetchmail" with the -m option to feed the # mail message straight to gurgitate, skipping the "local # MTA" step, then it doesn't have a "From " line. So I # have to deal with that by hand. First, check to see if # there's a "From " line present in the first place. def figure_out_from_line(headertext) (unix_from,normal_headers) = headertext.split(/\n/,2) if unix_from =~ /^From / then headertext=normal_headers unix_from=unix_from else # If there isn't, then deal with it after we've # worried about the rest of the headers, 'cos we'll # have to make our own. unix_from=nil end return unix_from, headertext end def parse_headers @headertext.each_line do |h| h.chomp! if(h=~/^\s+/) then @lastheader << h else header=Header.new(h) @headers[header.name] ||= HeaderBag.new @headers[header.name].push(header) @lastheader=header end end @headers_changed=false end # Get the envelope From information. This comes with a # whole category of rants: this information is absurdly hard # to get your hands on. The best you can manage is a sort # of educated guess. Thus, this horrible glob of hackiness. # I don't recommend looking too closely at this code if you # can avoid it, and further I recommend making sure to # configure your MTA so that it sends proper sender and # recipient information to gurgitate so that this code never # has to be run at all. def guess_sender # Start by worrying about the "From foo@bar" line. If it's # not there, then make one up from the Return-Path: header. # If there isn't a "Return-Path:" header (then I suspect we # have bigger problems, but still) then use From: as a wild # guess. If I hope that this entire lot of code doesn't get # used, then I _particularly_ hope that things never get so # bad that poor gurgitate has to use the From: header as a # source of authoritative information on anything. # # And then after all that fuss, if we're delivering to a # Maildir, I have to get rid of it. And sometimes the MTA # gives me a mbox-style From line and sometimes it doesn't. # It's annoying, but I have no choice but to Just Deal With # It. if @unix_from then # If it is there, then grab the email address in it and # use that as our official "from". fromregex=/^From ([^ ]+@[^ ]+) / fromregex.match(@unix_from) @from=$+ # or maybe it's local if @from == nil then @unix_from =~ /^From (\S+) / @from=$+ end else fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+)/ if self["Return-Path"] != nil then fromregex.match(self["Return-Path"][0].contents) else if self["From"] != nil then fromregex.match(self["From"][0].contents) end end address_candidate=$+ # If there STILL isn't a match, then it's probably safe to # assume that it's local mail, and doesn't have an @ in its # address. unless address_candidate if self["Return-Path"] != nil then self["Return-Path"][0].contents =~ /(\S+)/ address_candidate=$+ else self["From"][0].contents =~ /(\S+)/ address_candidate=$+ end end @from=address_candidate @unix_from="From "+self.from+" "+Time.new.to_s end end public # Creates a Headers object. # headertext:: # The text of the message headers. def initialize(headertext=nil, sender=nil, recipient=nil) @from = sender @to = recipient @headers = Hash.new(nil) if Hash === headertext @headers_changed = true headertext.each_key do |key| headername = key.to_s.gsub("_","-") header=Header.new(headername, headertext[key]) @headers[header.name] ||= HeaderBag.new @headers[header.name].push(header) end else if headertext @unix_from, @headertext = figure_out_from_line headertext parse_headers if @headertext if sender # then don't believe the mbox separator @from = sender @unix_from="From "+self.from+" "+Time.new.to_s else guess_sender end end end end # Grab the headers with names +names+ # names:: The names of the header. def [](*names) if names.inject(false) do |accum,name| accum or @headers.has_key? name end then return HeaderBag.new(names.collect { |name| @headers[name] }.flatten.delete_if { |e| e == nil } ) else return nil end end # Set the header named +name+ to +value+ # name:: The name of the header. # value:: The new value of the header. def []=(name,value) @headers_changed = true @headers[name]=HeaderBag.new([Header.new(name,value)]) end # Who the message is to (the envelope to) # # Yet another bucket of rants. Unix mail sucks. def to return @to || @headers["X-Original-To"] || nil end # Who the message is from (the envelope from) def from return @from || "" end # Change the envelope from line to whatever you want. This might # not be particularly neighborly, but oh well. # newfrom:: An email address def from=(newfrom) @from=newfrom @unix_from="From "+self.from+" "+Time.new.to_s end # Match header +name+ against +regex+ # name:: # A string containing the name of the header to match (for example, # "From") # regex:: The regex to match it against (for example, /@aol.com/) def match(name,regex) ret=false if(@headers[name]) then @headers[name].each do |h| ret |= h.matches(regex) end end return ret end # Return true if headers +names+ match +regex+ # names:: An array of header names (for example, %w{From Reply-To}) # regex:: The regex to match the headers against. def matches(names,regex) ret=false if names.class == String then names=[names] end names.each do |n| ret |= match(n,regex) end return ret end # Returns the headers properly formatted for an email # message. def to_mbox return @unix_from+"\n"+to_s end # Returns the headers formatted for an email message (without # the "From " line def to_s if @headers_changed then return @headers.map do |name,hdr| hdr.map do |hdr_content| hdr_content.to_s end.join("\n") end.join("\n") else return @headertext end end end end gurgitate-mail/gurgitate/header.rb0000644000076500001440000000471210747761263017311 0ustar dagbrownusers#!/opt/bin/ruby -w module Gurgitate class IllegalHeader < RuntimeError ; end # A little class for a single header class Header # The name of the header attr_accessor :name # The contents of the header attr_accessor :contents alias_method :value, :contents # A recent rash of viruses has forced me to canonicalize # the capitalization of headers. Sigh. def capitalize_words(s) return s.split(/-/).map { |w| w.capitalize }.join("-") rescue return s end private :capitalize_words # Creates a Header object. # header:: # The text of the email-message header def initialize(*header) name,contents=nil,nil if header.length == 1 then # RFC822 says that a header consists of some (printable, # non-whitespace) crap, followed by a colon, followed by # some more (printable, but can include whitespaces) # crap. if(header[0] =~ /^[\x21-\x39\x3b-\x7e]+:/) then (name,contents)=header[0].split(/:\s+/,2) if(name =~ /:$/ and contents == nil) then # It looks like someone is using Becky! name=header[0].gsub(/:$/,"") contents = "" end raise IllegalHeader, "Empty name" \ if (name == "" or name == nil) contents="" if contents == nil @@lastname=name else raise IllegalHeader, "Bad header syntax: no colon in #{header}" end elsif header.length == 2 then name,contents = *header end @name=capitalize_words(name) @contents=contents end # Extended header def << (text) @contents += "\n" + text end # Matches a header's contents. # regex:: # The regular expression to match against the header's contents def matches (regex) if String === regex regex = Regexp.new(Regexp.escape(regex)) end @contents =~ regex end alias :=~ :matches # Returns the header, ready to put into an email message def to_s @name+": "+@contents end end end gurgitate-mail/gurgitate/mailmessage.rb0000644000076500001440000000517410747761263020353 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # Handles a complete mail message #------------------------------------------------------------------------ require 'gurgitate/headers' module Gurgitate # A complete mail message. class Mailmessage Fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<][<](.*@.*)[>]|([^ ]+@[^ ]+)/; # The headers of the message attr_reader :headers # The body of the message attr_accessor :body # The envelope sender and recipient, if anyone thought to # mention them to us. attr_accessor :sender attr_accessor :recipient # Creates a new mail message with headers built from the options hash, # and the body of the message in a string. def self.create(*args) options = body = nil if String === args[0] options = args[1] body = args[0] elsif Hash === args[0] options = args[0] else options = {} end message = self.new message.instance_eval do if body @body=body end %w/sender recipient body/.each do |key| if options.has_key? key.to_sym instance_variable_set("@#{key}", options[key.to_sym]) options.delete key.to_sym end end @headers = Headers.new(options) end message end def initialize(text=nil, recipient=nil, sender=nil) @recipient = recipient @sender = sender if text (@headertext,@body)=text.split(/\n\n/,2) @headers=Headers.new(@headertext); Fromregex.match(@headers["From"][0].contents); @from=$+ else @headers = Headers.new @body = "" end end # Returns the header +name+ def header(name) @headers[name].each { |h| h.contents }.join(", ") end # custom accessors # Returns the message's sender def from; @sender || @headers.from; end # Returns all the candidates for a recipient def to; @recipient || @headers["To", "Cc"][0].contents; end # Returns the formatted mail message def to_s; @headers.to_s + "\n\n" + ( @body || ""); end # Returns the mail message formatted for mbox def to_mbox; @headers.to_mbox + "\n\n" + @body; end end end gurgitate-mail/gurgitate/deliver/maildir.rb0000644000076500001440000001014610747761263021132 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # Deliver mail to a maildir (also, detects maildir) #------------------------------------------------------------------------ require "socket" # for gethostname (!) module Gurgitate module Deliver module Maildir # Figures out if +mailbox+ is a Maildir mailbox # mailbox:: # A string containing the path of the mailbox to save the # message to. If it is of the form "=mailbox", it saves # the message to +Maildir+/+mailbox+. Otherwise, it # simply saves the message to the file +mailbox+. def self::check_mailbox(mailbox) begin if File.stat(mailbox).directory? then if File.stat(File.join(mailbox,"cur")).directory? then return Maildir end end rescue Errno::ENOENT return nil end end # Figures out the first available filename in the mail dir # +dir+ and returns the filename to use. # dir:: # One of "+mailbox+/tmp" or "+mailbox+/new", but that's # only because that's what the maildir spec # (http://cr.yp.to/proto/maildir.html) says. def maildir_getfilename(dir) time=Time.now.to_f counter=0 hostname=Socket::gethostname filename=nil loop do filename=File.join(dir,sprintf("%.4f.%d_%d.%s", time,$$,counter,hostname)) break if not File.exists?(filename) counter+=1 end return filename end # Creates a new Maildir folder +mailbox+ # mailbox:: # The full path of the new folder to be created def make_mailbox(mailbox) Dir.mkdir(mailbox) %w{cur tmp new}.each do |dir| Dir.mkdir(File.join(mailbox,dir)) end end # Delivers a message to the maildir-format mailbox +mailbox+. # mailbox:: # A string containing the path of the mailbox to save the # message to. If it is of the form "=mailbox", it saves # the message to +Maildir+/+mailbox+. Otherwise, it # simply saves the message to the file +mailbox+. def deliver_message(mailbox) begin File.stat(mailbox) rescue Errno::ENOENT make_mailbox(mailbox) end unless File.stat(mailbox).directory? raise SystemError, 'not a directory' end tmpfilename=maildir_getfilename(File.join(mailbox,"tmp")) File.open(tmpfilename,File::CREAT|File::WRONLY) do |fh| fh.write(self.to_s) fh.flush # I should put a caveat here, unfortunately. Ruby's # IO#flush only flushes Ruby's buffers, not the # operating system's. If anyone knows how to force # a real fflush(), I'd love to know. Otherwise, I'm # going to hope that closing the file does the trick # for me. end # ...and link to new. # (I guess Maildir mailboxes don't work too well # on Windows, eh?) newfilename = maildir_getfilename( File.join(mailbox,"new")) begin File.link(tmpfilename,newfilename) rescue SystemCallError log("Couldn't create maildir link to \"new\"!") exit 75 # Argh, I tried, it didn't work out end File.delete(tmpfilename) end end end end gurgitate-mail/gurgitate/deliver/mbox.rb0000644000076500001440000000340410747761264020456 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # Delivers a message to an mbox (also includes mbox detector) #------------------------------------------------------------------------ module Gurgitate module Deliver module MBox # Checks to see if +mailbox+ is an mbox mailbox # mailbox:: # A string containing the path of the mailbox to save # the message to. If it is of the form "=mailbox", it # saves the message to +Maildir+/+mailbox+. Otherwise, # it simply saves the message to the file +mailbox+. def self::check_mailbox(mailbox) begin if File.stat(mailbox).file? then return MBox else return nil end rescue Errno::ENOENT return nil end end # Delivers the message to +mailbox+ # mailbox:: # A string containing the path of the mailbox to save # the message to. If it is of the form "=mailbox", it # saves the message to +Maildir+/+mailbox+. Otherwise, # it simply saves the message to the file +mailbox+. def deliver_message(mailbox) File.open(mailbox,File::WRONLY | File::APPEND | File::CREAT) do |f| f.flock(File::LOCK_EX) message=(if f.stat.size > 0 then "\n" else "" end) + to_mbox f.print message f.flock(File::LOCK_UN) end end end end end gurgitate-mail/gurgitate/deliver/mh.rb0000644000076500001440000001232210747761264020114 0ustar dagbrownusers#!/opt/bin/ruby -w require "yaml" #------------------------------------------------------------------------ # Delivers a message to an mbox (also includes mbox detector) #------------------------------------------------------------------------ module Gurgitate module Deliver module MH # Checks to see if +mailbox+ is an mbox mailbox # mailbox:: # A string containing the path of the mailbox to save # the message to. If it is of the form "=mailbox", it # saves the message to +Maildir+/+mailbox+. Otherwise, # it simply saves the message to the file +mailbox+. def self::check_mailbox(mailbox) begin # Rather annoyingly, pretty well any directory can # be a MH mailbox, but this just checks to make sure # it's not actually a Maildir by mistake. # # I could put in a check for the path given in # $HOME/.mh_profile, but Claws-Mail uses MH mailboxes and # disregards $HOME/.mh_profile. if File.stat(mailbox).directory? and not ( File.exists?(File.join(mailbox, "cur")) or File.exists?(File.join(mailbox, "tmp")) or File.exists?(File.join(mailbox, "new"))) then return MH end rescue Errno::ENOENT return nil end end # Delivers the message to +mailbox+ # mailbox:: # A string containing the path of the mailbox to save # the message to. If it is of the form "=mailbox", it # saves the message to +Maildir+/+mailbox+. Otherwise, # it simply saves the message to the file +mailbox+. def deliver_message(mailbox) if ! File.exists? mailbox then Dir.mkdir(mailbox) end if File.exists? mailbox and not File.directory? mailbox then raise SystemError, "not a directory" end new_msgnum = next_message(mailbox) do |filehandle| filehandle.print self.to_s end update_sequences(mailbox, new_msgnum) end private def update_sequences(mailbox, msgnum) sequences = File.join(mailbox, ".mh_sequences") lockfile = sequences + ".lock" # how quaint loop do begin File.open(lockfile, File::WRONLY | File::CREAT | File::EXCL ) do |lock| File.open(sequences, File::RDWR | File::CREAT) do |seq| seq.flock(File::LOCK_EX) metadata = YAML.load(seq.read) || Hash.new metadata["unseen"] = update_unseen \ metadata["unseen"], msgnum seq.rewind metadata.each do |key, val| seq.puts "#{key}: #{val}" end seq.truncate seq.tell seq.flock(File::LOCK_UN) end end File.unlink(lockfile) break rescue Errno::EEXIST # some other process is doing something, so wait a few # milliseconds until it's done sleep(0.01) end end end def update_unseen unseen, msgnum prevmsg = msgnum - 1 if unseen unseenstring = unseen.to_s if unseenstring =~ /-#{prevmsg}/ then return unseenstring.sub(/\b#{prevmsg}\b/, msgnum.to_s) end if unseenstring.match(/\b#{prevmsg}\b/) then return "#{unseenstring}-#{msgnum}" end return "#{unseenstring} #{msgnum}" else return msgnum end end def next_message(mailbox) next_msgnum = Dir.open(mailbox).map { |ent| ent.to_i }.max + 1 loop do begin File.open(File.join(mailbox, next_msgnum.to_s), File::WRONLY | File::CREAT | File::EXCL ) do |filehandle| yield filehandle end break rescue Errno::EEXIST next_msgnum += 1 end end return next_msgnum end end end end gurgitate-mail/test/gurgitate-test.rb0000644000076500001440000000266610747014532020011 0ustar dagbrownusersrequire 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'fileutils' require 'pathname' require 'irb' $:.unshift File.dirname(__FILE__) + "/.." require "gurgitate-mail" class GurgitateTest < Test::Unit::TestCase def setup currentdir = Pathname.new(File.join(File.dirname(__FILE__), "..")).realpath.to_s @testdir = File.join(currentdir,"test-data") @folders = File.join(@testdir,"folders") FileUtils.rmtree @testdir if File.exists? @testdir Dir.mkdir @testdir Dir.mkdir @folders m = StringIO.new("From: me\nTo: you\nSubject: test\n\nHi.\n") @gurgitate = nil @gurgitate = Gurgitate::Gurgitate.new(m) testdir = @testdir folders = @folders @gurgitate.instance_eval do sendmail "/bin/cat" homedir testdir spooldir testdir spoolfile File.join(testdir, "default") maildir folders end @spoolfile = File.join(testdir, "default") end def maildirmake mailbox # per the UNIX command FileUtils.mkdir mailbox %w/cur tmp new/.each do |subdir| FileUtils.mkdir File.join(mailbox, subdir) end end def mhdirmake mailbox # per "maildirmake" FileUtils.mkdir mailbox end def teardown FileUtils.rmtree @testdir end def test_truth assert true end end gurgitate-mail/test/runtests.rb0000644000076500001440000000134510727646633016734 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # Unit tests for gurgitate-mail #------------------------------------------------------------------------ require 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'pathname' def runtests(testcases) testcases.each do |testcase| Test::Unit::UI::Console::TestRunner.run testcase end end testpath = Pathname.new(__FILE__).dirname.realpath testcases = Dir[File.join(testpath,"test_*")].map do |file| load file eval("TC_" + File.basename(file,".rb").sub(/^test_/,'').capitalize) end if __FILE__ == $0 then if(ARGV[0] == '-c') require 'coverage' end runtests testcases end gurgitate-mail/test/test_configuration.rb0000644000076500001440000000216710727646633020756 0ustar dagbrownusersrequire "test/gurgitate-test" require "etc" class TC_Configuration < GurgitateTest def test_default_configuration assert_equal Gurgitate::Deliver::MBox, @gurgitate.folderstyle assert_equal @folders, @gurgitate.maildir assert_equal @spoolfile, @gurgitate.spoolfile assert_equal @testdir, @gurgitate.homedir end def test_changing_folderstyle_to_maildir assert_nothing_raised do @gurgitate.folderstyle Gurgitate::Deliver::Maildir end assert_equal File.join(@testdir,"Maildir"), @gurgitate.spoolfile assert_equal File.join(@testdir,"Maildir"), @gurgitate.maildir end def test_changing_folderstyle_to_mbox assert_nothing_raised do @gurgitate.folderstyle Gurgitate::Deliver::MBox end assert_equal "/var/spool/mail", @gurgitate.spooldir assert_equal File.join("/var/spool/mail", Etc.getpwuid.name), @gurgitate.spoolfile end def test_illegal_folderstyle_syntax assert_raises ArgumentError do @gurgitate.folderstyle 1, 2, 3 end end end gurgitate-mail/test/test_deliver.rb0000644000076500001440000000544710747014142017527 0ustar dagbrownusersrequire 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'fileutils' require 'pathname' require 'irb' require "test/gurgitate-test" require "gurgitate/deliver" class DeliverTest include Gurgitate::Deliver attr_accessor :folderstyle def to_mbox "From test\n" + to_s end def to_s "From: test\nTo:test\nSubject: test\n\nTest\n" end end class TC_Deliver < Test::Unit::TestCase def setup currentdir = Pathname.new(File.join(File.dirname(__FILE__), "..")).realpath.to_s @testdir = File.join(currentdir,"test-data") @folders = File.join(@testdir,"folders") FileUtils.rmtree @testdir if File.exists? @testdir Dir.mkdir @testdir Dir.mkdir @folders m = StringIO.new("From: me\nTo: you\nSubject: test\n\nHi.\n") @deliver_test = DeliverTest.new testdir = @testdir folders = @folders @deliver_test.folderstyle = Gurgitate::Deliver::MBox @spoolfile = File.join(testdir, "default") end def teardown FileUtils.rmtree @testdir end # ------------------------------------------------------------------------ # And the tests # ------------------------------------------------------------------------ def test_setup_worked assert true end def test_basic_delivery assert_nothing_raised do @deliver_test.save(@spoolfile) end assert File.exists?(@spoolfile) assert File.file?(@spoolfile) assert_equal File.read(@spoolfile), @deliver_test.to_mbox end def test_basic_delivery_maildir @deliver_test.folderstyle = Gurgitate::Deliver::Maildir assert_nothing_raised do @deliver_test.save(@spoolfile) end assert File.exists?(@spoolfile) assert File.directory?(@spoolfile) assert File.exists?(File.join(@spoolfile,"cur")) assert File.exists?(File.join(@spoolfile,"new")) assert File.exists?(File.join(@spoolfile,"tmp")) contents = Dir[File.join(@spoolfile,"new","*")] assert contents.length == 1 assert File.exists?(contents[0]) assert_equal File.read(contents[0]), @deliver_test.to_s end def test_basic_delivery_mh @deliver_test.folderstyle = Gurgitate::Deliver::MH assert_nothing_raised do @deliver_test.save(@spoolfile) end assert File.exists?(@spoolfile) assert File.directory?(@spoolfile) mess = File.join(@spoolfile,"1") seq = File.join(@spoolfile,".mh_sequences") assert File.exists?(mess) assert File.exists?(seq) assert_equal File.read(mess), @deliver_test.to_s assert_equal File.read(seq), "unseen: 1\n" end end gurgitate-mail/test/test_delivery.rb0000644000076500001440000001503410734403267017717 0ustar dagbrownusersrequire 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'fileutils' require 'pathname' require 'irb' require "test/gurgitate-test" require "gurgitate-mail" class TC_Delivery < GurgitateTest #************************************************************************ # tests #************************************************************************ def test_basic_delivery assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) end def test_detect_mbox Dir.mkdir(@testdir) rescue nil File.open(@spoolfile, File::WRONLY | File::CREAT) do |f| f.print "" end assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) end def test_detect_maildir maildirmake @spoolfile assert_nothing_raised do @gurgitate.process { nil } end assert Dir[File.join(@spoolfile,"new","*")].length > 0 assert File.exists?(Dir[File.join(@spoolfile,"new","*")][0]) FileUtils.rmtree @spoolfile teardown test_detect_mbox setup end def test_save_folders assert_nothing_raised do @gurgitate.process do save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).file? end def test_save_guess_maildir maildirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert_equal 0, Dir[File.join(@folders, "test", "new", "*")].length assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 1, Dir[File.join(@folders, "test", "new", "*")].length end def test_save_maildir_collision maildirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert_equal 0, Dir[File.join(@folders, "test", "new", "*")].length assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 2, Dir[File.join(@folders, "test", "new", "*")].length end def test_save_create_maildir maildirmake @spoolfile assert_nothing_raised do @gurgitate.process do @folderstyle = Gurgitate::Deliver::Maildir @maildir = @spoolfile save "=test" break end end assert File.exists?(File.join(@spoolfile, ".test")) assert File.stat(File.join(@spoolfile, ".test")).directory? assert File.exists?(File.join(@spoolfile, ".test", "new")) assert File.stat(File.join(@spoolfile, ".test","new")).directory? assert_equal 0, Dir[File.join(@spoolfile, ".test", "cur", "*")].length assert_equal 1, Dir[File.join(@spoolfile, ".test", "new", "*")].length end def test_save_bad_filename assert_nothing_raised do @gurgitate.process do save "testing" end end assert File.exists?(@spoolfile) assert !File.exists?("testing") end def test_cannot_save FileUtils.touch @spoolfile FileUtils.chmod 0, @spoolfile assert_raises Errno::EACCES do @gurgitate.process do nil end end end def test_mailbox_heuristics_mbox @gurgitate.instance_eval do @folderstyle = nil end assert_nothing_raised do @gurgitate.process do save "=test" end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).file? end def test_mailbox_heuristics_maildir @gurgitate.instance_eval do @folderstyle = nil end assert_nothing_raised do @gurgitate.process do save "=test/" end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 1, Dir[File.join(@folders, "test", "new", "*")].length end def test_message_parsed_correctly assert_equal("From: me",@gurgitate.header("From")) assert_equal("To: you", @gurgitate.header("To")) assert_equal("Subject: test", @gurgitate.header("Subject")) assert_equal("Hi.\n", @gurgitate.body, "Message body is wrong") end def test_message_written_correctly test_message_parsed_correctly assert_nothing_raised do @gurgitate.process end mess=nil assert_nothing_raised do mess = Gurgitate::Mailmessage.new(File.read(@spoolfile)) end assert_equal("From: me", mess.header("From"), "From header is wrong") assert_equal("To: you", mess.header("To"), "To header is wrong") assert_equal("Hi.\n", mess.body, "Body is wrong") assert_equal("Subject: test", mess.header("Subject"), "Subject header wrong") end end gurgitate-mail/test/test_header.rb0000644000076500001440000001344010734403270017315 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # #------------------------------------------------------------------------ builddir = File.join(File.dirname(__FILE__),"..") unless $:[0] == builddir $:.unshift builddir end require 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' class TC_Header < Test::Unit::TestCase def setup require 'gurgitate-mail' end def test_simple_header h=Gurgitate::Header.new("From: fromheader@example.com") assert_equal(h.name,"From", "Simple header name is From") assert_equal(h.contents,"fromheader@example.com", "Contents is fromheader@example.com") assert_equal(h.value,"fromheader@example.com", "Contents is fromheader@example.com") end def test_canonicalize_crashing String.class_eval do alias old_capitalize capitalize def capitalize raise RuntimeError end end test_simple_header String.class_eval do alias capitalize old_capitalize end end # This is an illegal header that turns up in spam sometimes. # Crashing when you get spam is bad. def test_malcapitalized_header h=Gurgitate::Header.new("FROM: fromheader@example.com") assert_equal(h.name,"From", "Badly-capitalized header is From") assert_equal(h.contents,"fromheader@example.com", "Badly-capitalized header") assert_equal(h.value,"fromheader@example.com", "Badly-capitalized header") end # I got a message with "X-Qmail-Scanner-1.19" once. I hate whoever did # that. def test_dot_in_header h=Gurgitate::Header.new("From.Header: fromheader@example.com") assert_equal(h.name, "From.header", "header with dot in it is From.header") assert_equal(h.contents, "fromheader@example.com", "header with dot in it") assert_equal(h.value, "fromheader@example.com", "header with dot in it") end # Dammit! My new "anything goes" header parser was parsing too much def test_delivered_to h=Gurgitate::Header.new("Delivered-To: dagbrown@example.com") assert_equal("Delivered-To", h.name) assert_equal "dagbrown@example.com", h.contents assert_equal "dagbrown@example.com", h.value end # This is another particularly horrible spamware-generated not-a-header. def test_header_that_starts_with_hyphen h=Gurgitate::Header.new("-From: -fromheader@example.com") assert_equal(h.name, "-From", "header with leading hyphen is -From") assert_equal(h.contents, "-fromheader@example.com", "header with leading hyphen") assert_equal(h.value, "-fromheader@example.com", "header with leading hyphen") end # This is another illegal header that turns up in spam sometimes. # Crashing when you get spam is bad. def test_nonalphabetic_initial_char_header h=Gurgitate::Header.new("2From: fromheader@example.com") assert_equal(h.name,"2from", "Header that starts with illegal char is 2From") assert_equal(h.contents, "fromheader@example.com", "Header that starts with illegal char") assert_equal(h.value, "fromheader@example.com", "Header that starts with illegal char") end def test_bad_headers assert_raises(Gurgitate::IllegalHeader,"Empty name") { h=Gurgitate::Header.new(": Hi") } # assert_raises(Gurgitate::IllegalHeader,"Empty header contents") { # h=Gurgitate::Header.new("From: ") # } assert_raises(Gurgitate::IllegalHeader,"Bad header syntax") { h=Gurgitate::Header.new("This is completely wrong") } end def test_extending_header h=Gurgitate::Header.new("From: fromheader@example.com") h << " (Dave Brown)" assert_equal(h.name,"From","Extended header is From") assert_equal(h.contents,"fromheader@example.com\n (Dave Brown)", "Extended header contains all data") assert_equal(h.contents,h.value,"Contents same as value") end def test_empty_header_with_extension h=Gurgitate::Header.new("From:") h << " fromheader@example.com" assert_equal("From",h.name,"Empty extended header is From") assert_equal("\n fromheader@example.com", h.contents, "Empty extended header contains all data") assert_equal(h.contents, h.value, "Contents same as value") end def test_changing_header h=Gurgitate::Header.new("From: fromheader@example.com") h.contents="anotherfromheader@example.com" assert_equal(h.contents,"anotherfromheader@example.com", "header contents contains new data") end def test_tabseparated_header h=Gurgitate::Header.new("From:\tfromheader@example.com") assert_equal(h.name,"From","Tabseparated header is From (bug#154)") assert_equal(h.contents,"fromheader@example.com", "Tabseparated header's contents are correct (bug#154)") assert_equal(h.contents,h.value,"Contents same as value (bug#154)") end def test_conversion_to_String h=Gurgitate::Header.new("From: fromheader@example.com") assert_equal(h.to_s,"From: fromheader@example.com", "Conversion to string returns input") end def test_regex_match h=Gurgitate::Header.new("From: fromheader@example.com") assert_equal(0,h.matches(/fromheader/),"Matches regex that would match input") assert_equal(nil,h.matches(/notininput/),"Does not match regex that would not match input") end end gurgitate-mail/test/test_process.rb0000644000076500001440000000441310734403270017543 0ustar dagbrownusersbuilddir = File.join(File.dirname(__FILE__),"..") unless $:[0] == builddir $:.unshift builddir end require 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'fileutils' require 'pathname' require 'irb' require "gurgitate-mail" require "test/gurgitate-test" class TC_Process < GurgitateTest #************************************************************************ # tests #************************************************************************ def test_basic_delivery assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) end def test_pipe_raises_no_exceptions assert_nothing_raised do @gurgitate.process { pipe('cat > /dev/null') } end end def test_break_does_not_deliver assert_nothing_raised do @gurgitate.process { break } end assert !File.exists?(@spoolfile) end def test_message_parsed_correctly assert_equal("From: me",@gurgitate.header("From")) assert_equal("To: you", @gurgitate.header("To")) assert_equal("Subject: test", @gurgitate.header("Subject")) assert_equal("Hi.\n", @gurgitate.body, "Message body is wrong") end def test_message_written_correctly test_message_parsed_correctly assert_nothing_raised do @gurgitate.process end mess=nil assert_nothing_raised do mess = Gurgitate::Mailmessage.new(File.read(@spoolfile)) end assert_equal("From: me", mess.header("From"), "From header is wrong") assert_equal("To: you", mess.header("To"), "To header is wrong") assert_equal("Hi.\n", mess.body, "Body is wrong") assert_equal("Subject: test", mess.header("Subject"), "Subject header wrong") end def test_method_missing assert_equal "test", @gurgitate.subject[0].contents assert_raises NameError do p @gurgitate.nonexistentheader end end def test_filter new_obj = nil assert_nothing_raised do new_obj = @gurgitate.filter("tr a-z A-Z") end assert_equal "HI.\n", new_obj.body assert_equal "ME", new_obj.from assert_equal "TEST", new_obj.subject[0].contents end end gurgitate-mail/test/test_rules.rb0000644000076500001440000000541510727646634017241 0ustar dagbrownusersrequire "test/gurgitate-test" require "etc" class TC_Rules < GurgitateTest def setup super @rulesfile = File.join(@testdir, "rules_1.rb") File.open(rulesfile, File::CREAT|File::WRONLY) do |f| f.puts "nil" end @defaultrules = File.join(@testdir, ".gurgitate-rules.rb") File.open File.join(@defaultrules), File::CREAT|File::WRONLY do |f| f.puts "nil" end end attr_reader :rulesfile def teardown File.unlink rulesfile if File.exist? rulesfile end def test_add_rules_normal assert @gurgitate.add_rules(rulesfile) assert_equal [rulesfile], @gurgitate.instance_variable_get("@rules") end def test_add_unreadable_rules FileUtils.chmod 0, rulesfile assert_equal false, @gurgitate.add_rules(rulesfile) end def test_add_nonexistent_rules File.unlink rulesfile assert_equal false, @gurgitate.add_rules(rulesfile) end def test_add_default_rules assert_nothing_raised do @gurgitate.add_rules :default end assert_equal [@defaultrules], @gurgitate.instance_variable_get("@rules") end def test_add_system_rules unless Etc.getpwuid.uid == 0 assert_equal false, @gurgitate.add_rules(rulesfile, :system => true) else # but really, fer crying out loud, DON'T RUN TESTS AS ROOT assert true, @gurgitate.add_rules(rulesfile, :system => true) end end def test_add_user_rules assert @gurgitate.add_rules(rulesfile, :user => true) end def test_add_user_rules_file_not_found File.unlink rulesfile assert_equal false, @gurgitate.add_rules(rulesfile, :user => true) end def test_bad_syntax assert_raises ArgumentError do @gurgitate.add_rules(rulesfile, :honk) end end end class TC_ExecuteRules < TC_Rules def setup super @invalidrules = File.join(@testdir, "rules_invalid.rb") File.open @invalidrules, "w" do |f| f.puts "invalid syntax" end @exceptionrules = File.join(@testdir, "rules_exception.rb") File.open @exceptionrules, "w" do |f| f.puts "raise RuntimeError, 'testing'" end end def teardown File.unlink @invalidrules if File.exists? @invalidrules end def test_process_default @gurgitate.add_rules :default assert_nothing_raised do @gurgitate.process end end def test_process_rules_not_found @gurgitate.add_rules @rulesfile File.unlink @rulesfile assert_nothing_raised do @gurgitate.process end assert File.exists?(@spoolfile) end end gurgitate-mail/test/test_writing.rb0000644000076500001440000000775510734403270017564 0ustar dagbrownusers#/opt/bin/ruby -w builddir = File.join(File.dirname(__FILE__),"..") unless $:[0] == builddir $:.unshift builddir end require 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' class TC_Writing < Test::Unit::TestCase def setup require 'gurgitate-mail' end def test_null_message assert_nothing_raised do Gurgitate::Mailmessage.new end end def test_one_header_message mess = Gurgitate::Mailmessage.new mess.headers["From"] = "test@test" assert_equal "From: test@test", mess.headers.to_s assert_equal "From: test@test\n\n", mess.to_s end def test_two_headers_message mess = Gurgitate::Mailmessage.new mess.headers["From"] = "test@test" mess.headers["To"] = "test2@test2" assert_equal "From: test@test\nTo: test2@test2", mess.headers.to_s end def test_initialization_headers_only mess = Gurgitate::Mailmessage.create :from => "test@test", :to => "test2@test2" assert_equal [ "From: test@test", "To: test2@test2" ], mess.headers.to_s.split(/\n/) end def test_initialization_headers_and_body mess = Gurgitate::Mailmessage.create "This is a test", :from => "test@test", :to => "test2@test" assert_equal "This is a test", mess.body assert_equal [ "From: test@test", "To: test2@test" ], mess.headers.to_s.split(/\n/) end def test_initialization_headers_body_in_initialization_hash mess = Gurgitate::Mailmessage.create :body => "This is a test", :from => "test@test", :to => "test2@test" assert_equal "This is a test", mess.body assert_equal [ "From: test@test", "To: test2@test" ], mess.headers.to_s.split(/\n/) end def test_creation_round_trip mess = Gurgitate::Mailmessage.create "This is a test", :from => "test@test", :to => "test2@test2", :subject => "Test subject" reparsed_mess = Gurgitate::Mailmessage.new(mess.to_s) assert_equal reparsed_mess.to_s, mess.to_s end def test_creation_sender_specified mess = Gurgitate::Mailmessage.create :body => "This is a test", :from => "from@test", :to => "to@test", :sender => "sender@test" assert_equal "This is a test", mess.body assert_equal "From: from@test", mess.headers["From"].to_s assert_equal "To: to@test", mess.headers["To"].to_s assert_equal "sender@test", mess.from end def test_creation_recipient_specified mess = Gurgitate::Mailmessage.create :body => "This is a test", :from => "from@test", :to => "to@test", :recipient => "recipient@test" assert_equal "This is a test", mess.body assert_equal "From: from@test", mess.headers["From"].to_s assert_equal "To: to@test", mess.headers["To"].to_s assert_equal "recipient@test", mess.to end def test_creation_sender_not_specified mess = Gurgitate::Mailmessage.create :body => "This is a test", :from => "from@test", :to => "to@test" assert_equal "This is a test", mess.body assert_equal "From: from@test", mess.headers["From"].to_s assert_equal "To: to@test", mess.headers["To"].to_s assert_equal "", mess.from assert_equal "to@test", mess.to end def test_creation_incrementally mess = Gurgitate::Mailmessage.create mess.sender = "sender@test" mess.recipient = "recipient@test" mess.body = "This is a test" mess.headers["From"] = "from@test" mess.headers["To"] = "to@test" assert_equal "sender@test", mess.from assert_equal "recipient@test", mess.to assert_equal "This is a test", mess.body assert_equal "From: from@test", (mess.headers["From"]).to_s assert_equal "To: to@test", (mess.headers["To"]).to_s end end gurgitate-mail/test/test_gurgitate_delivery.rb0000644000076500001440000002321610747017435021775 0ustar dagbrownusersrequire 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' require 'fileutils' require 'pathname' require 'irb' require "test/gurgitate-test" require "gurgitate-mail" class TC_Gurgitate_delivery < GurgitateTest #************************************************************************ # tests #************************************************************************ def test_basic_delivery assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) end def test_detect_mbox Dir.mkdir(@testdir) rescue nil File.open(@spoolfile, File::WRONLY | File::CREAT) do |f| f.print "" end assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) end def test_detect_maildir maildirmake @spoolfile assert_nothing_raised do @gurgitate.process { nil } end assert Dir[File.join(@spoolfile,"new","*")].length > 0 assert File.exists?(Dir[File.join(@spoolfile,"new","*")][0]) FileUtils.rmtree @spoolfile teardown test_detect_mbox setup end def test_detect_mhdir mhdirmake @spoolfile assert_nothing_raised do @gurgitate.process { nil } end assert File.exists?(@spoolfile) assert File.directory?(@spoolfile) assert File.exists?(File.join(@spoolfile,"1")) assert File.exists?(File.join(@spoolfile,".mh_sequences")) FileUtils.rmtree @spoolfile teardown test_detect_mbox setup end def test_save_folders assert_nothing_raised do @gurgitate.process do save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).file? end def test_save_guess_maildir maildirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert_equal 0, Dir[File.join(@folders, "test", "new", "*")].length assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 1, Dir[File.join(@folders, "test", "new", "*")].length end def test_save_guess_mh mhdirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert_equal 0, Dir[File.join(@folders, "test", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "1")) assert File.stat(File.join(@folders, "test","1")).file? assert File.exists?(File.join(@folders, "test", ".mh_sequences")) assert File.stat(File.join(@folders, "test", ".mh_sequences")).file? assert_equal "unseen: 1\n", File.read(File.join(@folders, "test", ".mh_sequences")) end def test_save_maildir_collision maildirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert_equal 0, Dir[File.join(@folders, "test", "new", "*")].length assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 2, Dir[File.join(@folders, "test", "new", "*")].length end def test_save_mh_collision mhdirmake File.join(@folders,"test") assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert_equal 0, Dir[File.join(@folders, "test", "*")].length assert_nothing_raised do @gurgitate.process do save "=test" save "=test" break end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "1")) assert File.stat(File.join(@folders, "test","1")).file? assert File.exists?(File.join(@folders, "test", "2")) assert File.stat(File.join(@folders, "test","2")).file? assert File.exists?(File.join(@folders, "test", ".mh_sequences")) assert File.stat(File.join(@folders, "test", ".mh_sequences")).file? assert_equal "unseen: 1-2\n", File.read(File.join(@folders, "test", ".mh_sequences")) end def test_save_create_maildir maildirmake @spoolfile assert_nothing_raised do @gurgitate.process do @folderstyle = Gurgitate::Deliver::Maildir @maildir = @spoolfile save "=test" break end end assert File.exists?(File.join(@spoolfile, ".test")) assert File.stat(File.join(@spoolfile, ".test")).directory? assert File.exists?(File.join(@spoolfile, ".test", "new")) assert File.stat(File.join(@spoolfile, ".test","new")).directory? assert_equal 0, Dir[File.join(@spoolfile, ".test", "cur", "*")].length assert_equal 1, Dir[File.join(@spoolfile, ".test", "new", "*")].length end def test_save_create_mh maildirmake @spoolfile assert_nothing_raised do @gurgitate.process do @folderstyle = Gurgitate::Deliver::MH @maildir = @spoolfile save "=test" break end end assert File.exists?(File.join(@spoolfile, "test")) assert File.stat(File.join(@spoolfile, "test")).directory? assert File.exists?(File.join(@spoolfile, "test", ".mh_sequences")) assert File.stat(File.join(@spoolfile, "test",".mh_sequences")).file? assert File.exists?(File.join(@spoolfile, "test", "1")) assert File.stat(File.join(@spoolfile, "test", "1")).file? end def test_save_bad_filename assert_nothing_raised do @gurgitate.process do save "testing" end end assert File.exists?(@spoolfile) assert !File.exists?("testing") end def test_cannot_save FileUtils.touch @spoolfile FileUtils.chmod 0, @spoolfile assert_raises Errno::EACCES do @gurgitate.process do nil end end end def test_mailbox_heuristics_mbox @gurgitate.instance_eval do @folderstyle = nil end assert_nothing_raised do @gurgitate.process do save "=test" end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).file? end def test_mailbox_heuristics_maildir @gurgitate.instance_eval do @folderstyle = nil end assert_nothing_raised do @gurgitate.process do save "=test/" end end assert File.exists?(File.join(@folders, "test")) assert File.stat(File.join(@folders, "test")).directory? assert File.exists?(File.join(@folders, "test", "new")) assert File.stat(File.join(@folders, "test","new")).directory? assert_equal 0, Dir[File.join(@folders, "test", "cur", "*")].length assert_equal 1, Dir[File.join(@folders, "test", "new", "*")].length end def test_message_parsed_correctly assert_equal("From: me",@gurgitate.header("From")) assert_equal("To: you", @gurgitate.header("To")) assert_equal("Subject: test", @gurgitate.header("Subject")) assert_equal("Hi.\n", @gurgitate.body, "Message body is wrong") end def test_message_written_correctly test_message_parsed_correctly assert_nothing_raised do @gurgitate.process end mess=nil assert_nothing_raised do mess = Gurgitate::Mailmessage.new(File.read(@spoolfile)) end assert_equal("From: me", mess.header("From"), "From header is wrong") assert_equal("To: you", mess.header("To"), "To header is wrong") assert_equal("Hi.\n", mess.body, "Body is wrong") assert_equal("Subject: test", mess.header("Subject"), "Subject header wrong") end end gurgitate-mail/test/test_headers.rb0000644000076500001440000004237110747500326017511 0ustar dagbrownusers#!/opt/bin/ruby -w #------------------------------------------------------------------------ # #------------------------------------------------------------------------ require 'test/unit' require 'test/unit/ui/console/testrunner' require 'stringio' builddir = File.join(File.dirname(__FILE__),"..") unless $:[0] == builddir $:.unshift builddir end class TC_Headers < Test::Unit::TestCase def setup require 'gurgitate-mail' end def test_single_header h=Gurgitate::Headers.new(<<'EOF' From fromline@example.com Sat Sep 27 12:20:25 PDT 2003 From: fromheader@example.com EOF ) assert_equal("fromline@example.com",h.from) assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) end def test_fromline_simple_username h=Gurgitate::Headers.new(<<'EOF' From fromline Sat Sep 27 12:20:25 PDT 2003 From: fromheader@example.com EOF ) assert_equal("fromline",h.from) assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal nil, h["To"] end def test_changing_headers h = Gurgitate::Headers.new(<<'EOF', "sender@example.com", "recipient@example.com") From: fromheader@example.com To: toheader@example.com Subject: Subject EOF assert_equal(1,h["From"].length) assert_equal("From", h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) h["From"].sub! "fromheader", "changedheader" assert_equal("changedheader@example.com",h["From"][0].contents) end def test_altered_headers h = Gurgitate::Headers.new(<<'EOF', "sender@example.com", "recipient@example.com") From: fromheader@example.com To: toheader@example.com Subject: Subject EOF assert_equal(1,h["From"].length) assert_equal("From", h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) new_header = h["From"].sub "fromheader", "changedheader" assert Gurgitate::HeaderBag === new_header assert_equal("changedheader@example.com", new_header[0].contents) assert_equal("fromheader@example.com",h["From"][0].contents) end def test_matches h = Gurgitate::Headers.new(<<'EOF', "sender@example.com", "recipient@example.com") From: fromheader@example.com To: toheader@example.com Subject: Subject EOF assert h.matches(["From", "To"], /example.com/) assert !h.matches(["From", "To"], /example.net/) assert h.matches("From", /example.com/) assert !h.matches("From", /example.net/) end def test_fromline_no_username h=Gurgitate::Headers.new(<<'EOF' From Sat Sep 27 12:20:25 PDT 2003 From: fromheader@example.com EOF ) assert_equal("",h.from) assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) end def test_missing_toline h=Gurgitate::Headers.new(<<'EOF' From: fromheader@example.com EOF ) assert_equal('fromheader@example.com',h.from) assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) end def test_sender_and_recipient h = Gurgitate::Headers.new(<<'EOF', "sender@example.com", "recipient@example.com") From: fromheader@example.com To: toheader@example.com Subject: Subject EOF assert_equal('sender@example.com', h.from) assert_equal('recipient@example.com', h.to) end def test_multiple_headers h=Gurgitate::Headers.new(<<'EOF' From fromline@example.com Sat Sep 27 12:20:25 PDT 2003 From: fromheader@example.com To: toheader@example.com Subject: Subject line EOF ) assert_equal(h.from,"fromline@example.com") assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal("toheader@example.com",h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Subject line",h["Subject"][0].contents) end def test_missing_fromline h=Gurgitate::Headers.new(<<'EOF' From: fromheader@example.com To: toheader@example.com Subject: Subject line EOF ) assert_equal('fromheader@example.com',h.from) assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal("toheader@example.com",h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Subject line",h["Subject"][0].contents) end def test_multiline_headers h=Gurgitate::Headers.new(<<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromheader@example.com To: toheader@example.com, nexttoheader@example.com Subject: Subject line EOF ) assert_equal(h.from,"fromline@example.com") assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal("toheader@example.com,\n nexttoheader@example.com",h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Subject line",h["Subject"][0].contents) end def test_multiline_headers_with_extra_colons h=Gurgitate::Headers.new(<<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromheader@example.com To: toheader@example.com, nexttoheader@example.com (The test: header) Subject: Subject line EOF ) assert_equal(h.from,"fromline@example.com") assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal("toheader@example.com,\n nexttoheader@example.com (The test: header)",h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Subject line",h["Subject"][0].contents) end def test_multiline_headers_with_various_levels_of_indentation h=Gurgitate::Headers.new(<<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromheader@example.com To: toheader@example.com, nexttoheader@example.com, thirdtoheader@example.com, fourthtoheader@example.com Subject: Subject line EOF ) assert_equal(h.from,"fromline@example.com") assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("fromheader@example.com",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal("toheader@example.com,\n nexttoheader@example.com,\n thirdtoheader@example.com,\n fourthtoheader@example.com",h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Subject line",h["Subject"][0].contents) end def test_a_header_that_actually_crashed_gurgitate h=Gurgitate::Headers.new(<<'EOF' Return-path: Received: from pd8mr3no.prod.shaw.ca (pd8mr3no-qfe2.prod.shaw.ca [10.0.144.160]) by l-daemon (iPlanet Messaging Server 5.2 HotFix 1.16 (built May 14 2003)) with ESMTP id <0HO6002FDGPREL@l-daemon> for dagbrown@shaw.ca; Tue, 11 Nov 2003 00:56:15 -0700 (MST) Received: from pd7mi4no.prod.shaw.ca ([10.0.149.117]) by l-daemon (iPlanet Messaging Server 5.2 HotFix 1.18 (built Jul 28 2003)) with ESMTP id <0HO60055LGPR40@l-daemon> for dagbrown@shaw.ca (ORCPT dagbrown@shaw.ca); Tue, 11 Nov 2003 00:56:15 -0700 (MST) Received: from venom.easydns.com (smtp.easyDNS.com [216.220.40.247]) by l-daemon (iPlanet Messaging Server 5.2 HotFix 1.18 (built Jul 28 2003)) with ESMTP id <0HO60079HGPR79@l-daemon> for dagbrown@shaw.ca; Tue, 11 Nov 2003 00:56:15 -0700 (MST) Received: from ohno.mrbill.net (ohno.mrbill.net [207.200.6.75]) by venom.easydns.com (Postfix) with ESMTP id D6493722BB for ; Tue, 11 Nov 2003 02:53:50 -0500 (EST) Received: by ohno.mrbill.net (Postfix) id ED0AD53380; Tue, 11 Nov 2003 01:56:13 -0600 (CST) Received: from mail.neurotica.com (neurotica.com [207.100.203.161]) by ohno.mrbill.net (Postfix) with ESMTP id 5CD465337F for ; Tue, 11 Nov 2003 01:56:13 -0600 (CST) Received: from mail.neurotica.com (localhost [127.0.0.1]) by mail.neurotica.com (Postfix) with ESMTP id CDAA2364C; Tue, 11 Nov 2003 02:56:03 -0500 (EST) Received: from smtpzilla5.xs4all.nl (smtpzilla5.xs4all.nl [194.109.127.141]) by mail.neurotica.com (Postfix) with ESMTP id B6A22361E for ; Tue, 11 Nov 2003 02:56:00 -0500 (EST) Received: from xs1.xs4all.nl (xs1.xs4all.nl [194.109.21.2]) by smtpzilla5.xs4all.nl (8.12.9/8.12.9) with ESMTP id hAB7u5ZZ042116 for ; Tue, 11 Nov 2003 08:56:05 +0100 (CET) Received: from xs1.xs4all.nl (wstan@localhost.xs4all.nl [127.0.0.1]) by xs1.xs4all.nl (8.12.10/8.12.9) with ESMTP id hAB7u5xE048677 for ; Tue, 11 Nov 2003 08:56:05 +0100 (CET envelope-from wstan@xs4all.nl) Received: (from wstan@localhost) by xs1.xs4all.nl (8.12.10/8.12.9/Submit) id hAB7u4sZ048676 for nifty@neurotica.com; Tue, 11 Nov 2003 08:56:04 +0100 (CET envelope-from wstan) Date: Tue, 11 Nov 2003 08:56:04 +0100 From: William Staniewicz Subject: Re: [nifty] Ping... In-reply-to: <9636B78C-140B-11D8-9EE6-003065D0C184@nimitzbrood.com> Sender: nifty-bounces@neurotica.com To: Nifty Cc: Errors-to: nifty-bounces@neurotica.com Reply-to: Nifty Message-id: <20031111075604.GE79497@xs4all.nl> MIME-version: 1.0 Content-type: text/plain; charset=us-ascii Content-disposition: inline Precedence: list X-BeenThere: nifty@mail.neurotica.com Delivered-to: dagbrown@mrbill.net Delivered-to: nifty@neurotica.com User-Agent: Mutt/1.4.1i X-Original-To: nifty@neurotica.com References: <9636B78C-140B-11D8-9EE6-003065D0C184@nimitzbrood.com> X-Mailman-Version: 2.1.2 List-Post: List-Subscribe: , List-Unsubscribe: , List-Help: List-Id: Nifty Original-recipient: rfc822;dagbrown@shaw.ca EOF ) assert_equal(h.from,"nifty-bounces@neurotica.com") assert_equal(1,h["From"].length) assert_equal("From",h["From"][0].name) assert_equal("William Staniewicz ",h["From"][0].contents) assert_equal(1,h["To"].length) assert_equal("To",h["To"][0].name) assert_equal('Nifty ',h["To"][0].contents) assert_equal(1,h["Subject"].length) assert_equal("Subject",h["Subject"][0].name) assert_equal("Re: [nifty] Ping...",h["Subject"][0].contents) end def test_another_crashy_set_of_headers h=Gurgitate::Headers.new(<<'EOF' From HEYITBLEWUP Fri Nov 21 14:41:08 PST 2003 Received: from unknown (harley.radius [192.168.0.123]) by yoda.radius with SMTP (Microsoft Exchange Internet Mail Service Version 5.5.2653.13) id LYN7YZKG; Wed, 9 Jul 2003 14:36:40 -0700 Subject: IAP password EOF ) assert_equal(h.from,"HEYITBLEWUP") assert_equal(nil,h["From"]) assert_equal("IAP password",h["Subject"][0].contents) end def test_fromline_no_hostname # illegal from line m=<<'EOF' From HEYITBLEWUP Sat Mar 27 16:02:12 PST 2004 Received: from ohno.mrbill.net (ohno.mrbill.net [207.200.6.75]) by lart.ca (Postfix) with ESMTP id A485F104CA9 for ; Sat, 27 Mar 2004 15:58:06 -0800 (PST) Received: by ohno.mrbill.net (Postfix) id 0D3423A289; Sat, 27 Mar 2004 17:58:42 -0600 (CST) Delivered-To: dagbrown@mrbill.net Received: from 66-168-59-126.jvl.wi.charter.com (66-168-59-126.jvl.wi.charter.com [66.168.59.126]) by ohno.mrbill.net (Postfix) with SMTP id 948BD3A288 for ; Sat, 27 Mar 2004 17:58:41 -0600 (CST) X-Message-Info: HOCBSQX Message-Id: <20040327235841.948BD3A288@ohno.mrbill.net> Date: Sat, 27 Mar 2004 17:58:41 -0600 (CST) From: ""@ To: undisclosed-recipients: ; EOF h=Gurgitate::Headers.new(m) assert_equal(%{""@},h["From"][0].contents) assert_equal(1,h["From"].length) end def test_editing_header m = <<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromline@example.com To: toline@example.com Subject: Subject line EOF h=Gurgitate::Headers.new(m) h["From"]="anotherfromline@example.com" assert_equal("anotherfromline@example.com",h["From"][0].contents, "From line correctly changed") assert_match(/^From: anotherfromline@example.com$/,h.to_s, "From line correctly turns up in finished product") end def test_editing_from m = <<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromline@example.com To: toline@example.com Subject: Subject line EOF h=Gurgitate::Headers.new(m) t=Time.new.to_s h.from="anotherfromline@example.com" assert_equal("anotherfromline@example.com",h.from, "Envelope from correctly changed") assert_match(/^From anotherfromline@example.com #{Regexp.escape(t)}/, h.to_mbox, "Envelope from changed in finished product") end def test_match_multiple_headers m = <<'EOF' From fromline@example.com Sat Oct 25 12:58:31 PDT 2003 From: fromline@example.com To: toline@example.com Subject: Subject line EOF h=Gurgitate::Headers.new(m) assert_equal(true,h["From","To"] =~ /fromline@example.com/, "headers contains fromline") assert_equal(true,h["From","To"] =~ /toline@example.com/, "headers contains toline") assert_equal(false,h["From","To"] =~ /nonexistent@example.com/, "headers do not contain nonexistent value") assert(!(h["Rabbit"] =~ /nonexistent/), "Asking for a nonexistent header") end def test_broken_spam m=<<'EOF' Return-Path: kirstenparsonsry@yahoo.com Delivery-Date: Fri May 21 19:42:02 PDT Return-Path: kirstenparsonsry@yahoo.com Delivery-Date: Fri May 21 17:39:51 2004 Return-Path: X-Original-To: dagbrown@lart.ca Delivered-To: dagbrown@lart.ca Received: from anest.co.jp (c-24-1-221-189.client.comcast.net [24.1.221.189]) by lart.ca (Postfix) with ESMTP id 05B7F5704 for ; Fri, 21 May 2004 17:39:51 -0700 (PDT) Message-ID: From: "Kirsten Parsons" To: dagbrown@lart.ca Subject: Congrats! Date: Fri, 21 May 2004 20:56:27 +0000 MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: base64 EOF h=Gurgitate::Headers.new(m) assert_equal(Gurgitate::Header.new("To","dagbrown@lart.ca").contents, h["To"][0].contents,"To header is as expected") assert_equal(false,h["To","Cc"] =~ /\blunar@lunar-linux.org\b/i, "There should be no Lunar Linux mailing list here") assert_equal(false,h["To"] =~ /\blunar@lunar-linux.org\b/i, "There should be no Lunar Linux mailing list in To line") assert(!(h["Cc"] =~ /\blunar@lunar-linux.org\b/i), "There should be no Lunar Linux mailing list in Cc line") end end class TC_Meddling_With_Headers < Test::Unit::TestCase def setup @message = <<'EOF' From: fromline@example.com To: toline@example.com Subject: Subject Line EOF @sender = "sender@example.com" @recipient = "recipient@example.com" @headers = Gurgitate::Headers.new @message, @sender, @recipient end def test_match assert @headers.match("From", /example.com/) end def test_nomatch assert !@headers.match("From", /lart.ca/) end def test_match_regex result = nil assert_nothing_raised do result = @headers.match "From", /example.com/ end assert result end def test_match_string result = nil assert_nothing_raised do assert result = @headers.match("From", "example.com") end assert result result = nil assert_nothing_raised do result = @headers.match("From", "e.ample.com") end assert !result end end