Browse files

Adding the gurgitate-mail files to rubyforge so that others may find …

…them.
  • Loading branch information...
0 parents commit e55fb7aad1820d0aecbb2fccb0538f121a5efa9e dagbrown committed Aug 19, 2003
Showing with 712 additions and 0 deletions.
  1. +33 −0 CHANGELOG
  2. +2 −0 INSTALL
  3. +57 −0 Makefile
  4. +1 −0 VERSION
  5. +304 −0 gurgitate-mail.RB
  6. +278 −0 gurgitate-mail.pod
  7. +8 −0 gurgitate.rb
  8. +29 −0 install.rb
33 CHANGELOG
@@ -0,0 +1,33 @@
+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
2 INSTALL
@@ -0,0 +1,2 @@
+To install gurgitate-mail, simply run the "install.rb" script:
+ ruby install.rb
57 Makefile
@@ -0,0 +1,57 @@
+TARGETS = gurgitate-mail.rb gurgitate-mail gurgitate-mail.html gurgitate-mail.man README
+
+TARBALL=$(shell echo gurgitate-mail-`cat VERSION`.tar.gz)
+WEBPAGE=$(HOME)/public_html/software/gurgitate-mail
+
+all: $(TARGETS)
+
+dist: tarball
+
+clean:
+ -rm -f $(TARGETS)
+ -rm -f pod2htm*~~
+
+tarball: $(TARGETS) INSTALL CHANGELOG
+ cd .. && tar zcvf $(TARBALL) \
+ gurgitate-mail/INSTALL \
+ gurgitate-mail/install.rb \
+ gurgitate-mail/gurgitate-mail \
+ gurgitate-mail/gurgitate-mail.rb \
+ gurgitate-mail/gurgitate-mail.html \
+ gurgitate-mail/gurgitate-mail.man \
+ gurgitate-mail/CHANGELOG \
+ gurgitate-mail/README
+
+gurgitate-mail.rb: gurgitate-mail.RB
+ ruby -w -c $< && cp $< $@
+
+gurgitate-mail: gurgitate.rb
+ ruby -w -c $< && cp $< $@
+ chmod +x gurgitate-mail
+
+gurgitate-mail.html: gurgitate-mail.pod
+ pod2html $< > $@
+
+gurgitate-mail.man: gurgitate-mail.pod
+ pod2man $< > $@
+
+README: gurgitate-mail.pod
+ pod2text $< > $@
+
+tag: VERSION
+ cvs update VERSION
+ @echo "Adding tag RELEASE_"`sed 's/\./_/g' VERSION`
+ cvs tag RELEASE_`sed 's/\./_/g' VERSION` .
+
+untag: VERSION
+ cvs update VERSION
+ @echo "Removing tag RELEASE_"`sed 's/\./_/g' VERSION`
+ cvs tag -d RELEASE_`sed 's/\./_/g' VERSION` .
+
+release: tag tarball
+ cp ../$(TARBALL) $(WEBPAGE)
+ chmod 644 $(WEBPAGE)/$(TARBALL)
+ cp CHANGELOG $(WEBPAGE)/CHANGELOG.txt
+ chmod 644 $(WEBPAGE)/CHANGELOG.txt
+ cp gurgitate-mail.html $(WEBPAGE)
+ chmod 644 $(WEBPAGE)/gurgitate-mail.html
1 VERSION
@@ -0,0 +1 @@
+1.1.1
304 gurgitate-mail.RB
@@ -0,0 +1,304 @@
+#------------------------------------------------------------------------
+# Mail filter package
+#------------------------------------------------------------------------
+
+require 'etc'
+
+#========================================================================
+# A little class for a single header
+#========================================================================
+class Header
+ attr_accessor :name
+ attr_accessor :contents
+
+ def initialize(header)
+ (@name,@contents)=header.split(/: /,2)
+ end
+
+ def matches (regex)
+ @contents =~ regex
+ end
+
+ def to_s
+ @name+": "+@contents
+ end
+end
+
+#========================================================================
+# A slightly bigger class for all a message's headers
+#========================================================================
+class Headers
+ def initialize(headertext)
+ @headers=Hash.new(nil)
+ @headertext=headertext
+
+ (unix_from,normal_headers)=headertext.split(/\n/,2);
+
+ # 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.
+ 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=""
+ end
+
+ @headertext.split(/\n/).each do |h|
+ header=Header.new(h)
+ @headers[header.name]=[] if @headers[header.name]==nil;
+ @headers[header.name].push(header)
+ end
+
+ # Okay, now worry 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:
+ if unix_from == "" then
+ fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+)/;
+ if self["Return-Path"] != nil then
+ fromregex.match(self["Return-Path"][0].contents);
+ else
+ fromregex.match(self["From"][0].contents);
+ 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.
+ if address_candidate == nil then
+ 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;
+ else
+ # 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
+ end
+ end
+
+ def [](name); return @headers[name]; end
+
+ def from
+ return @from
+ end
+
+ def match(name,regex)
+ ret=false
+ if(@headers[name]) then
+ @headers[name].each do |h|
+ ret |= h.matches(regex)
+ end
+ end
+ return ret
+ end
+
+ 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
+
+ def to_s
+ return @unix_from+"\n"+@headertext
+ end
+end
+
+#========================================================================
+# A class to deal with a message in its entirety
+#========================================================================
+class Mailmessage
+ attr_reader :headers;
+ attr_accessor :body;
+
+ def initialize(text)
+ (@headertext,@body)=text.split(/^$/,2)
+ fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<][<](.*@.*)[>]|([^ ]+@[^ ]+)/;
+ @headers=Headers.new(@headertext);
+ fromregex.match(@headers["From"][0].contents);
+ @from=$+
+ end
+
+ def header(name)
+ @headers[name].each do |h|
+ h.contents
+ end.join(", ");
+ end
+
+ # custom accessors
+ def from; @headers.from; end
+ def to_s; @headers.to_s+@body; end
+end
+
+#========================================================================
+# The actual gurgitator; reads a message and then can do other stuff
+# with it, like save to a mailbox or forward somewhere else.
+#========================================================================
+class Gurgitate < Mailmessage
+ include Etc
+ attr_writer :maildir, :logfile, :sendmail;
+ attr_reader :homedir;
+
+ # Constants
+ Spooldir="/var/spool/mail"
+ Spoolfile=Spooldir+"/"+Etc.getlogin()
+
+ # Set config params to defaults, read in mail message
+ def initialize(fh=nil)
+ @passwd=getpwnam(getlogin)
+ @homedir=@passwd.dir;
+ @maildir=@passwd.dir+"/Mail"
+ @logfile=@passwd.dir+"/.gurgitate.log"
+ @sendmail="/usr/lib/sendmail"
+ @actiontaken=false
+ if(fh)
+ super(fh.read)
+ end
+ end
+
+ # Read-only stuff
+ def spoolfile; Spoolfile; end
+
+ #--------------------------------------------------
+ # Save a message to a mailbox
+ # If the mailbox is of the form "=mailbox", it puts it into
+ # Maildir/mailbox.
+ # Otherwise, it puts it into the file you ask for.
+
+ def save(mailbox)
+ if mailbox[0,1]=='=' and @maildir != nil
+ mailbox["="]=@maildir+"/"
+ end
+
+ if mailbox[0,1] != '/'
+ log("Cannot save to relative filenames! Saving to spool file");
+ mailbox=spoolfile
+ end
+
+ log("Saving to mailbox "+mailbox)
+ begin
+ File.open(mailbox,"a") do |f|
+ f.flock(File::LOCK_EX)
+ # Do this all at once so that it doesn't write half a message
+ # if it's going to barf
+ # f.print "From "+self.from+" "+Time.new.to_s+"\n"
+ f.print self.to_s+"\n"
+ f.flock(File::LOCK_UN)
+ end
+ rescue SystemCallError
+ self.log "Gack! Something went wrong: "+$!
+ exit 75
+ end
+ end
+
+ def delete
+ end
+
+ #--------------------------------------------------
+ # Forwards the message to the address you ask for.
+ def forward(address)
+ self.log "Forwarding to "+address
+ IO.popen(@sendmail+" "+address,"w") do |f|
+ f.print(self.to_s)
+ end
+ end
+
+ #--------------------------------------------------
+ # Writes a log 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 a program
+ def pipe(program)
+ self.log "Piping through "+program
+ IO.popen(program,"w") do |f|
+ f.print(self.to_s)
+ end
+ return $?>>8
+ rescue SystemCallError
+ save(Spoolfile)
+ return -1
+ end
+
+ #--------------------------------------------------
+ # Pipes the message through a program, returns
+ # another Gurgitate object containing the output
+ def filter(program)
+ self.log "Filtering with "+program
+ IO.popen("-","w+") do |filter|
+ if filter.nil? then
+ exec(program)
+ else
+ if fork
+ filter.close_write
+ return Gurgitate.new(filter)
+ else
+ filter.close_read
+ filter.print(self.to_s)
+ filter.close
+ exit
+ end
+ end
+ end
+ rescue SystemCallError
+ save(Spoolfile)
+ return nil
+ end
+
+ #--------------------------------------------------
+ # Processes the mail filter
+ def process
+ configfilespec=homedir+"/.gurgitate-rules.rb"
+ if FileTest.exist?(configfilespec) and
+ FileTest.file?(configfilespec) and
+ FileTest.owned?(configfilespec) and
+ FileTest.readable?(configfilespec)
+ then
+ configfile=File.new(configfilespec)
+ rules=configfile.read
+ eval rules
+ save(spoolfile)
+ else
+ save(spoolfile)
+ end
+ rescue ScriptError
+ log "Couldn't load .gurgitate-rules: "+$!
+ save(spoolfile)
+ end
+end
278 gurgitate-mail.pod
@@ -0,0 +1,278 @@
+=head1 NAME
+
+gurgitate-mail - an easy-to-use mail filter
+
+=head1 SYNOPSIS
+
+gurgitate-mail
+
+=head1 DESCRIPTION
+
+C<gurgitate-mail> is a program which reads your mail and filters
+it according to the F<.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 F<.forward> file:
+
+ "|/path/to/gurgitate-mail"
+
+Or through your F<.procmailrc> file:
+
+ :0:
+ | /path/to/gurgitate-mail
+
+(Future versions will, hopefully, be usable as a local delivery
+agent; a drop-in replacement for "deliver" or "procmail".)
+
+=head1 CONFIGURATION FILE
+
+F<.gurgitate-rules> is a series of Ruby statements, with the
+following methods and variables available:
+
+=head2 Variables
+
+=over 5
+
+=item 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)
+
+=item headers
+
+This is an object containing the headers of the message. There
+are several methods that come with this object:
+
+=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
+C<gurgitate-mail>.
+
+=item maildir
+
+The directory which contains the folders, used by the C<save>
+method when you specify a folder as "=F<folder>" (like Elm).
+Defaults to "$HOME/Mail".
+
+=item homedir
+
+Your home directory. Read-only.
+
+=item logfile
+
+The location of the C<gurgitate-mail> logfile. If set to C<nil>,
+then no logging is done. Defaults to "$HOME/.gurgitate.log".
+
+=item sendmail
+
+The location of the C<sendmail> program. Used by the C<forward>
+method. Defaults to "/usr/lib/sendmail".
+
+=item spoolfile
+
+The location of the mail spool. Read-only.
+
+=back
+
+=head2 Methods
+
+=over 5
+
+=item matches(name(s),regex)
+
+Returns C<true> if the header C<name> matches the regular
+expression C<regex>. If C<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.
+
+=item from
+
+Returns the envelope "from" address of the email message. Note
+that this is the same as the bare "from".
+
+=item to_s
+
+As per Ruby convention, returns all the headers as a C<String> object.
+
+=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 C<maildir>. If you don't use the =F<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 C<spoolfile>.
+
+=item forward(address)
+
+This forwards the email message to another email address.
+
+=item pipe(program)
+
+This pipes the message through C<program>. C<pipe> returns the
+exit code of the program that the message was piped through.
+
+=item filter(program)
+
+This pipes the message through C<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.)
+
+=item return
+
+This tells C<gurgitate-mail> to stop processing the email message.
+If you don't use C<return>, then C<gurgitate-mail> will continue
+processing the same mail again with the next rule. If there isn't
+a C<return> at the end of F<gurgitate-rules.rb>, then
+C<gurgitate-mail> will save the email message in the normal mail
+spool.
+
+=item log(message)
+
+This writes a log message to the log file.
+
+=back
+
+=head1 SIMPLE EXAMPLES
+
+Here are some examples of C<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.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.
+
+=head1 ADVANCED EXAMPLES
+
+Here are some slightly more clever examples to give you an idea
+of what you can do with C<gurgitate-mail>. Let's suppose you have
+an email whitelist in a file called F<$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 L<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
+
+C<bogofilter -h> trains L<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 C<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
+
+C<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 C<bogofilter> for.
+
+=head1 AUTHOR
+
+Dave Brown <gurgitate-mail@dagbrown.com>
8 gurgitate.rb
@@ -0,0 +1,8 @@
+#------------------------------------------------------------------------
+# Mail filter invocation script
+#------------------------------------------------------------------------
+
+require "gurgitate-mail"
+
+message=Gurgitate.new(STDIN);
+message.process
29 install.rb
@@ -0,0 +1,29 @@
+#!/usr/bin/ruby -w
+
+require "rbconfig"
+require "ftools"
+
+include Config
+
+version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+sitedir = CONFIG["sitedir"]
+bindir = CONFIG["bindir"]
+dest = "#{sitedir}/#{version}"
+
+print "Installing gurgitate-mail.rb in #{dest}...\n"
+File.install("gurgitate-mail.rb", dest, 0644)
+
+print "Installing gurgitate-mail 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")

0 comments on commit e55fb7a

Please sign in to comment.