tumgreyspf-1.36/0000755000175000017500000000000011663450516012277 5ustar jafojafotumgreyspf-1.36/tumgreyspf.spec0000644000175000017500000001100111663450516015351 0ustar jafojafo%define name tumgreyspf %define version 1.36 %define release 1 %define prefix %{_prefix} Summary: Powerful, easy to use spam protection. Name: %{name} Version: %{version} Release: %{release} License: GPL Group: Applications/System URL: http://www.tummy.com/krud/ Source: %{name}-%{version}.tar.gz Packager: Sean Reifschneider BuildRoot: /var/tmp/%{name}-root Requires: python Requires: postfix >= 2.1 BuildArch: noarch %description This is tumgreyspf, an external policy checker for the postfix mail server. It can optionally greylist and/or use spfquery to check SPF records to determine if email should be accepted by your server. SPF is information published by the domain owner about what systems may legitimately send e-mail for the domain. Greylisting takes advantage of spam and viruses that do not follow the RFCs and retry deliveries on temporary failure. We use these checks as part of our mail system and have seen several orders of magnitude reduction in spam, lower system load, and few problems with legitimate mail getting blocked. It uses the file-system as it's database, no additional database is required to use it. %prep %setup %build %install [ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT" # make directories mkdir -p "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/ mkdir -p "$RPM_BUILD_ROOT"/var/lib/tumgreyspf/config mkdir -p "$RPM_BUILD_ROOT"/var/lib/tumgreyspf/data mkdir -p "$RPM_BUILD_ROOT"/usr/sbin mkdir -p "$RPM_BUILD_ROOT"/etc/cron.d mkdir -p $RPM_BUILD_ROOT/%{_mandir}/man8 # copy over files for file in tumgreyspf tumgreyspf-clean tumgreyspf-configtest \ tumgreyspf-install tumgreyspf-stat tumgreyspf-addip tumgreyspfsupp.py do cp "$file" "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/ done cp tumgreyspf.conf "$RPM_BUILD_ROOT"/var/lib/tumgreyspf/config/ cp __default__.dist "$RPM_BUILD_ROOT"/var/lib/tumgreyspf/config/__default__ cp tumgreyspf.8 $RPM_BUILD_ROOT/%{_mandir}/man8 # move external programs to /usr/sbin mv "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/tumgreyspf-configtest "$RPM_BUILD_ROOT"/usr/sbin mv "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/tumgreyspf-stat "$RPM_BUILD_ROOT"/usr/sbin mv "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/tumgreyspf-addip "$RPM_BUILD_ROOT"/usr/sbin # set up crontab echo '0 0 * * * nobody /usr/lib/tumgreyspf/tumgreyspf-clean' \ >"$RPM_BUILD_ROOT"/etc/cron.d/tumgreyspf # replace pieces in code that need to reflect new directories ( cd "$RPM_BUILD_ROOT"/usr/lib/tumgreyspf/ sed 's|^sys.path.append.*|sys.path.append("/usr/lib/tumgreyspf")|' \ tumgreyspf >tumgreyspf.new && \ cat tumgreyspf.new >tumgreyspf && \ rm -f tumgreyspf.new sed 's|^defaultConfigFilename.*|defaultConfigFilename = "/var/lib/tumgreyspf/config/tumgreyspf.conf"|' \ tumgreyspfsupp.py >tumgreyspfsupp.py.new && \ cat tumgreyspfsupp.py.new >tumgreyspfsupp.py && \ rm -f tumgreyspfsupp.py.new cd "$RPM_BUILD_ROOT"/usr/sbin/ sed 's|^sys.path.append.*|sys.path.append("/usr/lib/tumgreyspf")|' \ tumgreyspf-clean >tumgreyspf-clean.new && \ cat tumgreyspf-clean.new >tumgreyspf-clean && \ rm -f tumgreyspf-clean.new sed 's|^sys.path.append.*|sys.path.append("/usr/lib/tumgreyspf")|' \ tumgreyspf-stat >tumgreyspf-stat.new && \ cat tumgreyspf-stat.new >tumgreyspf-stat && \ rm -f tumgreyspf-stat.new sed 's|^my .base_dir*|my $base_dir = q(/var/lib/tumgreyspf/config/client_address);|' \ tumgreyspf-addip >tumgreyspf-addip.new && \ cat tumgreyspf-addip.new >tumgreyspf-addip && \ rm -f tumgreyspf-addip.new cd "$RPM_BUILD_ROOT"/var/lib/tumgreyspf/config/ sed 's|^spfqueryPath.*|spfqueryPath = "/usr/bin/spfquery"|' \ tumgreyspf.conf | \ sed 's|^greylistDir.*|greylistDir = "/var/lib/tumgreyspf/data"|' | \ sed 's|^configPath.*|configPath = "file:///var/lib/tumgreyspf/config"|' \ >tumgreyspf.conf.new && \ cat tumgreyspf.conf.new >tumgreyspf.conf && \ rm -f tumgreyspf.conf.new ) %clean [ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT" %files %defattr(755,root,root) /usr/lib/tumgreyspf /usr/sbin/* %dir /var/lib/tumgreyspf %dir /var/lib/tumgreyspf/config %config /var/lib/tumgreyspf/config/tumgreyspf.conf %config /var/lib/tumgreyspf/config/__default__ %attr(700,nobody,root) /var/lib/tumgreyspf/data %attr(644,root,root) /etc/cron.d/tumgreyspf %doc README README.QuickStart README.performance WHATSNEW TODO README-RPM %{_mandir}/man8/tumgreyspf* tumgreyspf-1.36/README.QuickStart0000644000175000017500000000111211663450516015243 0ustar jafojafoQuick Start Guide for tumgreyspf $Id: README.QuickStart,v 1.1 2004-08-23 02:24:17 jafo Exp $ ================================ If you have installed tumgreyspf from an RPM or Debian package, you should be up and running. You may wish to see the "TESTING" section of "README" for instructions on how to test the greylist/SPF setup. For more information on configuration, please see the "CONFIGURATION" section of "README". If you are not installing from RPM/Debian packages, please see the "QUICK-START INSTALL" section of "README" for instructions on manually installing tumgreyspf. tumgreyspf-1.36/README-RPM0000644000175000017500000000056711663450516013623 0ustar jafojafoOnce you install the RPM, you need to configure Postfix to use tumgreyspf. This is so that installing the RPM doesn't risk breaking an existing mail system. After installing the RPM, you will need to read the README and follow either the QUICKSTART INSTALL. Or, for more detailed installation instructions, go to the INSTALL section and start with the CONFIGURING POSTFIX. tumgreyspf-1.36/WHATSNEW0000644000175000017500000002325411663450516013470 0ustar jafojafo================================ Version 1.36 -- Thu Nov 24, 2011 New configuration setting "spfAcceptOnPermError" will accept e-mail from hosts that have invalid SPF records. This is probably the right thing to do, so defaulting to enabled. Contributed by Jesse Norell. Added a "COMMON PROBLEMS" section to README, with suggested text from George Sexton. Changed README to put tumgreyspf in recipient restrictions, not sender. Reported by George Sexton. Bug fixed in tumgreyspf-clean when using greylistIpOnly, it wasn't removing greylist entries. Reported by Michael Meelis and others. ================================ Version 1.35 -- Wed Jul 23, 2009 Fixed a bug caused when the SPF record has a NUL in it. Reported by Thomas Goirand ================================ Version 1.34 -- Wed Jun 10, 2009 Fixing a bug which caused many SPF return results to bypass greylisting. Identified by Tomasz 'urug' Olszewski ================================ Version 1.33 -- Sun Jul 20, 2008 Pretty print the time in tumgreyspf-stat, based on code by Luis Botero. Fixing a code bug causing error "global name 'Except' is not defined" Reported by Joe Lowe. ================================ Version 1.32 -- Thu Nov 29, 2007 Rephrasing part of the documentation per suggestion from martin f krafft. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=453370 ================================ Version 1.31 -- Thu Nov 01, 2007 Patch from Miklos HONTI to fix a bug in the SPF checker that would prevent it from continuing on if SPF returns "none". ================================ Version 1.30 -- Tue Oct 16, 2007 Fixing thinko in the patch in the last release. Submitted by Thomas Goirand ================================ Version 1.29 -- Wed Oct 10, 2007 Minimal man page contributed by Thomas Goirand. Add a header if the SPF check throws an exception for any reason. Suggested by Radoslaw Stachowiak If SPF returns "none" indicating no SPF record, allow checking to continue on to other checkers. For example, if you have it set to use "spf, greylist". Patch submitted by Paul Tiemann. ================================ Version 1.28 -- Wed Sep 12, 2007 Queue-id was not getting set for some paths through the code. Caused tracebacks in the mail log file. Reported by RStachowiak and George Sexton ================================ Version 1.27 -- Thu Jun 14, 2007 Patch from Scott Kitterman which changes the capitilization to hopefully prevent problems with future versions of SPF. ================================ Version 1.26 -- Sun Jun 10, 2007 Paul Tiemann reported problems with the location of SPF import and syslog logging. ================================ Version 1.25 -- Sat Jun 09, 2007 Patch from Scott Kitterman to use the pySPF v2.0 RFC-4408 check code. More patches from Scott Kitterman: Refactored SPF code for when using SPF2. Changed logging to match RFC-4408 SPF-Received terminology. Fixed processing to skip SPF for localhost connections to work for IPv6. Implemented prepending of SPF-Received: header. Changed skip local connections to use CIDR match rather than string. Log non-fail/error results to syslog Defer on temperror Reject on permerror Check HELO for null Sender (Mail From) Patch from Joshua J. Kugler to greylist only by IP address (not by IP+sender+recipient). Patch from Carl Brewer for "tumgreyspf-addip" to easily whitelist IP addresses. ================================ Version 1.24 -- Wed Nov 29, 2006 Patch from Scott Scriven to fix the tumgreyspf-stat+ignoreLastByte issues. ================================ Version 1.23 -- Tue Nov 28, 2006 README changes sent in by Leni Mayo and Radoslaw Stachowiak. Skip SPF lookups for mail originated from 127.*. Patch by Scott Kitterman Patch to tumgreyspf-stat to work with "ignoreLastByte", reported by Radoslaw Stachowiak and others. SPF reject message now has "SPF Reports:" prepended to it so that it's more obvious that the rejection is due to SPF. Based on suggestion by Radoslaw Stachowiak. Install script bug fixed, as reported by "Herman". ================================ Version 1.22 -- Tue Sep 04, 2006 tumgreyspf-clean will now expire initial entries after 12 hours if they haven't connected again. Patch by Jeff from Rockville. ================================ Version 1.21 -- Tue Aug 29, 2006 In the case of an SPF reject, return 550 instead of REJECT, in compliance with the RFCs. Found and patch provided by Scott Kitterman. Fixed a thinko and changed link to SPF site, patch by Scott Kitterman. Changing the documentation, removing "advanced" settings, based on questions by yann shukor. Made the OSError exception trap more picky. Patch by Mike Meyer. ================================ Version 1.20 -- Tue Jun 27, 2006 Fixing tumgreyspf-stat. Try making greylisting data directory several times if we get OSError. This helps work around a race condition. Reported by Mike Meyer. ================================ Version 1.19 -- Wed Jun 07, 2006 Mike Meyer noticed that unnecessary data was being written to the greylist directory, potentially using unnecessary data blocks. Corrected. ================================ Version 1.18 -- Tue May 30, 2006 Changed tumgreyspfsupp.py so that defaultConfigFilename is consistent with example settings in other files. This only impacts users who are not installing via the RPM. Reported by Luis Botero ================================ Version 1.17 -- Tue Mar 21, 2006 Changing the __default__.dist file to list SPF first, since the documentation recommends it be set that way. Suggested by Benjamin Derry. Changing the spec file so that the cron.d file is mode 644. Reported by Stephen Warren. ================================ Version 1.16 -- Fri Feb 24, 2006 1.15 had a bug in indentation. Ugh. Reported by Benjamin Derry. Adding a README-RPM with instructions for after the RPM install. Suggested by Benjamin Derry ================================ Version 1.15 -- Sat Feb 18, 2006 Adding a link to the greylisting URL in the initial greylist message. ================================ Version 1.14 -- Sun Feb 12, 2006 Fixing a bug with SPF checking and a traceback. Reported by Simon Bellwood. ================================ Version 1.13 -- Sun Feb 05, 2006 Another patch from Scott Scriven to fix the tumgreyspf-clean program for the "ignoreLastByte" patch from Thursday. ================================ Version 1.12 -- Thu Feb 02, 2006 A patch from Scott Scriven which implements a new setting, "ignoreLastByte". When set, the last byte of the IP address is ignored so that mail servers in a pool of the same /24 network block can more easily get mail through. This defaults to off. ================================ Version 1.11 -- Sat Oct 01, 2005 Adding documentation about file-system problems when using ext2/ext3 and how to avoid them. ================================ Version 1.10 -- Fri Sep 30, 2005 defaultSeedOnly was not implemented, Sean E. Millichamp provided a patch to implement it. Some SPF versions return "deny" instead of "fail". Sean E. Millichamp supplied a patch which treats both responses the same. ================================ Version 1.09 -- Wed Aug 31, 2005 Changing the install instructions to use different syntax for the main.cf file. (Reported by Ferdinand Schmid) Modifying the python version used by the programs. System was using "python2.2", changed to just use "python". (Reported by Ferdinand Schmid) ================================ Version 1.08 -- Wed Apr 27, 2005 Changed the regex used for processing the files, fixing problems with addresses that have "=" in them. Fix provided by Stephen Warren ================================ Version 1.07 -- Wed Apr 27, 2005 Debugging in tumgreyspf could cause tracebacks due to logging the wrong value. Fix provided by Stephen Warren. ================================ Version 1.06 -- Thu Apr 15, 2005 Changing the install script to indent second line of master.cf file (reported by Stephen Warren) Got a report from Stephen Warren that "tumgreyspf-install" does not work in a number of ways with his pre-existing postfix configuration files. I've changed the README to recommend doing a manual install instead of automatic. ================================ Version 1.04 -- Thu Apr 14, 2005 Changed the README to reflect GPL status. ================================ Version 1.03 -- Tue Mar 15, 2005 Added LICENSE file for GPL. ================================ Version 1.02 -- Wed Nov 10, 2004 Adding blackholing. Fixed a bunch of bugs in the install process and spec file which Kevin Fenzi found. Thanks. ================================ Version 1.01 -- Mon Aug 23, 2004 Fixing a bug in tumgreyspf-install related to updating the time_limit ================================ Version 1.00 -- Sun Aug 22, 2004 Extensive documentation. Added tumgreyspf-install. Added tumgreyspf-stat to show status of greylist data directory. Config file name can now be specified as argument to program. Split common routines out into tumgreyspfsupp. Added "tumgreyspf-clean". Added a README.performance file based on my testing. Added "tumgreyspf-configtest" program, which can be used to test a configuration file without having to wait for tumgreyspf to be fired off and log the config file failure. Added a master config file, /etc/tumgreyspf.conf No configuration of the main program is necessary now. Greylist data file ctime/atime is now updated when a new message comes in. Moving ip data from "data" into "data/client_address". Adding a log entry for when a message is accepted by greylist. tumgreyspf-1.36/tumgreyspf-install0000644000175000017500000001473711663450516016107 0ustar jafojafo#!/usr/bin/env python # # Set up postfix to use tumgreyspf import re, os, string, shutil, sys # default values postfixDir = '/etc/postfix' tumgreyspf_time_limit = 3600 tumgreyspf_user = 'nobody' tumgreyspf_path = '/usr/lib/tumgreyspf/tumgreyspf' G_verbose = 1 ##################################### def addPostfixMultiline(data, value): '''Add a value to an existing, possibly multi-line Postfix main.cf block. The modified block is returned.''' data = data[:] if len(data) == 1: data[0] = data[0][:-1] + ' ' + value + '\n' else: # find prefix prefix = ' ' for line in data[1:]: m = re.match(r'^(\s+)\S.*$', line) if m: prefix = m.group(1) break # add value data.append(prefix + value + '\n') return(data) ######################## # find path to postconf postconfPathList = ( '/usr/sbin/postconf', '/sbin/postconf', '/usr/bin/postconf', ) postconfPath = None for path in postconfPathList: if os.path.exists(path): postconfPath = path break ########################## # check required programs if not postconfPath: sys.stderr.write('Unable to find path to "postconf" program.\n') sys.exit(1) ################ # check version fp = os.popen('postconf mail_version', 'r') postfix_version = None for line in fp.readlines(): m = re.match(r'^mail_version\s*=\s*([\d.]+)\s*$', line) if not m: continue postfix_version = m.group(1) data = fp.read() fp.close() if not postfix_version: sys.stderr.write('Unable to determine postfix version.\n') sys.exit(1) # convert version into a list of integers and compare version_split = map(int, string.split(postfix_version, '.')) if version_split[0] < 2 or (version_split[0] == 2 and version_split[1] < 1): sys.stderr.write('tumgreyspf requires Postfix version 2.1 or higher.\n') sys.stderr.write(' Found version %s.\n' % postfix_version) sys.exit(1) # check existance of postfix directory if not os.path.exists(postfixDir): sys.stderr.write('Postfix does not seem to be in "%s".\n' % postfixDir) sys.exit(1) ####################################### # make backup of main.cf and master.cf masterFile = os.path.join(postfixDir, 'master.cf') masterBackupFile = os.path.join(postfixDir, 'master.cf.tumgreyspf') mainFile = os.path.join(postfixDir, 'main.cf') mainBackupFile = os.path.join(postfixDir, 'main.cf.tumgreyspf') if not os.path.exists(mainFile): sys.stderr.write('Could not find main.cf in "%s".\n' % mainFile) sys.exit(1) if not os.path.exists(masterFile): sys.stderr.write('Could not find master.cf in "%s".\n' % masterFile) sys.exit(1) if os.path.exists(masterBackupFile): sys.stdout.write('master.cf backup file "%s" already exists.\n' % masterBackupFile) sys.stdout.write(' Not making another backup.\n') else: shutil.copyfile(masterFile, masterBackupFile) if os.path.exists(mainBackupFile): sys.stdout.write('main.cf backup file "%s" already exists.\n' % mainBackupFile) sys.stdout.write(' Not making another backup.\n') else: shutil.copyfile(mainFile, mainBackupFile) ###################################################### # rewrite the smtpd_recipient_restrictions in main.cf fp = open(mainFile, 'r') mainData = fp.readlines() fp.close() # find smtpd_recipient_restrictions startLinenum = None endLinenum = None # find start of smtpd_recipient_restrictions mode = 'start' smtpd_recipient_restrictions = None for linenum in xrange(len(mainData)): line = mainData[linenum] if mode == 'start': if re.match(r'^smtpd_recipient_restrictions\s*=', line): startLinenum = linenum mode = 'end' elif mode == 'end': if re.match(r'^[^#\s].*', line): endLinenum = linenum smtpd_recipient_restrictions = mainData[startLinenum:endLinenum] break if startLinenum == None: startLinenum = len(mainData) endLinenum = len(mainData) smtpd_recipient_restrictions = [ 'smtpd_recipient_restrictions = \\n', ' reject_unauth_destination\n', ] if G_verbose: print 'No smtpd_recipient_restrictions found, adding.' # check for and add reject_unauth_destination found = 0 for line in smtpd_recipient_restrictions: # remove comments m = re.match(r'^(.*)#.*$', line) if m: line = m.group(1) if re.search(r'\breject_unauth_destination\b', line): found = 1 break if not found: if G_verbose: print 'Adding reject_unauth_destination to smtpd_recipient_restrictions' smtpd_recipient_restrictions = addPostfixMultiline( smtpd_recipient_restrictions, 'reject_unauth_destination') # check for and add check_policy_service found = 0 for line in smtpd_recipient_restrictions: # remove comments m = re.match(r'^(.*)#.*$', line) if m: line = m.group(1) if re.search(r'\bcheck_policy_service unix:private/tumgreyspf\b', line): found = 1 break if not found: if G_verbose: print 'Adding tumgreyspf policy to smtpd_recipient_restrictions' smtpd_recipient_restrictions = addPostfixMultiline( smtpd_recipient_restrictions, 'check_policy_service unix:private/tumgreyspf') # change the policy_time_limit found = 0 for linenum in xrange(len(mainData)): line = mainData[linenum] m = re.match(r'^tumgreyspf_time_limit\s*=\s*(\d+).*$', line) if m: if G_verbose: print 'Setting tumgreyspf_time_limit to %s' % tumgreyspf_time_limit mainData[linenum] = string.replace(line, m.group(1), str(tumgreyspf_time_limit)) found = 1 if not found: if G_verbose: print 'Adding tumgreyspf_time_limit line' mainData.append('\n') mainData.append('# increase the policy timeout for tumgreyspf\n') mainData.append('tumgreyspf_time_limit = %s\n' % tumgreyspf_time_limit) # update mainData with new settings mainData = (mainData[:startLinenum] + smtpd_recipient_restrictions + mainData[endLinenum:]) #################### # write new main.cf fp = open(mainFile, 'w') for line in mainData: fp.write(line) fp.close() ##################################### # add tumgreyspf policy to master.cf fp = open(masterFile, 'r') found = 0 for line in fp.readlines(): if re.match(r'tumgreyspf\s*', line): found = 1 fp.close() # append the data if not found: if G_verbose: print 'Adding tumgreyspf policy to master.cf' fp = open(masterFile, 'a') fp.write('\n') fp.write('# policy for tumgreyspf\n') fp.write('tumgreyspf unix - n n - - spawn\n') fp.write(' user=%s argv=%s\n' % ( tumgreyspf_user, tumgreyspf_path )) fp.close() ################## # restart postfix if os.path.exists('/etc/init.d/postfix'): if G_verbose: print 'Restarting postfix.' os.system('/etc/init.d/postfix restart') else: sys.stderr.write('Failed to restart postfix. Please restart manually.\n') sys.exit(1) tumgreyspf-1.36/tumgreyspf.80000644000175000017500000000212511663450516014575 0ustar jafojafo.TH tumgreyspf 8 .SH NAME tumgreyspf \- an external policy checker for the postfix mail server .SH SYNOPSIS .B tumgreyspf [ .I configfilename ] .SH DESCRIPTION Tumgreyspf can optionally greylist and/or use spfquery to check SPF records to determine if email should be accepted by your server. SPF is information published by the domain owner about what systems may legitimately send e-mail for the domain. Greylisting takes advantage of spam and viruses that do not follow the RFCs and retry deliveries on temporary failure. We use these checks as part of our mail system and have seen several orders of magnitude reduction in spam, lower system load, and few problems with legitimate mail getting blocked. Tumgreyspf uses the file-system as it's database, no additional database is required to use it. This man page is a place holder for whoever wants to write a doc to this. Feel free to send it to the upstream Sean Reifschneider or to me, the Debian maintainer, Thomas Goirand . .SH "SEE ALSO" Homepage: tumgreyspf-1.36/gentest0000755000175000017500000000104211663450516013673 0ustar jafojafo#!/usr/bin/env python # # Read a log file from stdin, generate output which can be passed # to tumgreyspf to test performance. import re, sys for line in sys.stdin.xreadlines(): m = re.match(r'^.* REMOTEIP="([\d.]+)".* HELO="([^"]+).* SENDER="([^"]+)".* ' r'RECIPIENT="([^"]+)".*$', line) if not m: continue sys.stdout.write('request=smtpd_access_policy\nprotocol_state=RCPT\nprotocol_name=SMTP\nclient_address=%s\nhelo_name=%s\nsender=%s\nrecipient=%s\nqueue_id=\nsize=0\nsasl_method=\nsasl_username=\nsasl_sender=\n\n' % m.groups()) tumgreyspf-1.36/README0000644000175000017500000003761611663450516013174 0ustar jafojafotumgreyspf README Sean Reifschneider Homepage: http://www.tummy.com/Community/software/tumgreyspf/ Code/bugfixes: https://github.com/linsomniac/tumgreyspf ============================================================= This is tumgreyspf, an external policy checker for the postfix mail server. It can optionally greylist and/or use spfquery to check SPF records to determine if email should be accepted by your server. It uses the file-system as it's database, no additional database is required to use it. LICENSE tumgreyspf is licensed under the GPL. BENEFITS High Accuracy SPF is information published by the domain owner about what systems may legitimately send e-mail for the domain. Greylisting takes advantage of spam and viruses that do not follow the RFCs and retry deliveries on temporary failure. We use these checks as part of our mail system and have seen several orders of magnitude reduction in spam, lower system load, and few problems with legitimate mail getting blocked. Low Maintenance tumgreyspf requires no regular attention from the administrator to remain effective. Easy Setup Installation should be as easy as installing an RPM or Debian package on your system. There are few additional requirements. Extensive time has been devoted to installation automation and documentation. REQUIREMENTS Python http://www.python.org/ Postfix 2.1 or above http://www.postfix.org/ Optional: spfquery or pyspf http://www.libspf2.org/ http://spf.pobox.com/downloads.html http://www.wayforward.net/spf/ NOTE BEFORE YOU USE TUMGREYSPF tumgreyspf stores the greylist data in the file-system using many small files. This has a few benefits, namely that you do not need to install or configure any database software. It also makes you immune to to database corruption issues that other greylist systems have. However, this does mean that if not configured properly you may experience extremely poor performance. There are details in one of my blog posts: http://www.tummy.com/journals/entries/jafo_20051001_003419 However, the short answer is that you need to be careful about blocking known bad recipient and sender addresses in Postfix before handing messages off to tumgreyspf, and you probably should configure SPF to be checked before greylisting. If you are going to be storing your tumgreyspf database on an "ext2" or "ext3" file-system, you have to be particularly careful about this problem. I have run a number of production e-mail servers using this with extremely good results and absolutely no problems, however I do acknowledge that there is a potential for problems. Read the above URL for more details on preventing these problems. Over the last 18 months that it's been in use, and more than a year that it's been publicly available, I've received many responses saying that it worked great, and one blog post reporting the above problems. tumgreyspf may not be for everyone, but many people do find it useful. QUICK-START INSTALL There is a script called "tumgreyspf-install" provided with this software. I have had a report that it didn't work, so I would recommend against running it, instead see the "INSTALL INSTRUCTIONS" section at the end of this document for manual installation instructions. The install process is fairly easy, requiring some simple changes to the Postfix configuration files. LOGGING tumgreyspf will log messages to syslog about it's activities. The "debugLevel" value in "tumgreyspf.conf" can be increased to get additional information to be logged. When set to a value of "0", only test results (greylist/SPF hits/misses) are logged. Look for "tumgreyspf" in your mail log files. TESTING The best way to test tumgreyspf is to simulate SMTP connections, then watch the logs and look in the ".../data/" directory for greylist settings. This testing probably needs to be done from a remote system. For example, suppose we have a machine "10.9.8.7" that we want to run tests against our mail server "10.1.2.3": Log into 10.9.8.7. Run "telnet 10.1.2.3 25" Type "helo example.com" Type "mail from: " Type "rcpt to: " Note that "user2@example.com" needs to be a valid local e-mail address in most cases, and that "user1@example.com" is subject to SPF blocking. The first time you do this, you should receive the response: 450 : Recipient address rejected: Service unavailable, greylisted. This indicates that the greylisting is working. Check the logs, you should see something similar to: Aug 22 19:52:49 mail tumgreyspf[12182]: Initial greylisting: REMOTEIP="10.9.8.7" HELO="example.com" SENDER="user1@example.com" RECIPIENT="user2@example.com" QUEUEID="" Aug 22 19:52:49 mail databytes[12184]: RCPT_INFO: REMOTEIP="10.9.8.7" HELO="example.com" SENDER="user1@example.com" RECIPIENT="user2@example.com" QUEUEID="" Aug 22 19:52:49 mail postfix/smtpd[11992]: NOQUEUE: reject: RCPT from testhost.example.com[10.9.8.7]: 450 : Recipient address rejected: Service unavailable, greylisted.; from= to= proto=SMTP helo= The "Initial greylisting" indicates that the record was not found in the database, and that a new entry was created. Now look in the greylist data for this entry: ls /path/to/data/client_address/10/9/8/7/greylist/user1@example.com/user2@example.com Wait 10 minutes (or whatever you set the greylisting time to) and try it again. This time, in response to your "rcpt to" line, you should get: 250 Ok or (if you have enabled SPF blocking for your domain): 554 : Recipient address rejected: Please see http://spf.pobox.com/why.html?sender=user2%40example.com&ip=10.9.8.7&receiver=spfquery The only way to get around the SPF block is to either disable SPF checking in tumgreyspf (perhaps for this IP only, see the "CONFIGURATION" section below), or change your SPF configuration so that it allows mail from your test machine. CONFIGURATION NOTE: After changing "tumgreyspf.conf", you should run "tumgreyspf-configtest" to ensure that it's correct. This only applies to changes made to the "tumgreyspf.conf" master configuration file. Configurations are processed from the top down, in the order specified by "OTHERCONFIGS". So, settings in a top-level __default__ file will be overridden if set in a configuration below that top level. There is the __default__ file at the top level that is used as a default for all decisions. If you wish to disable SPF or greylist for a specific IP/subnet/sender/recipient, you simply make a __default__ file in a subdirectory under config matching the entity you wish to match, with SPF or other checks disabled. For example, if you want to disable SPF queries for hosts in 192.168.10.0/24: mkdir /var/spool/tumgreyspf/config/client_address/192/168/10/ edit /var/spool/tumgreyspf/config/client_address/192/168/10/__default__ The __default__ file should contain: SPFSEEDONLY=0 GREYLISTTIME=300 CHECKERS= OTHERCONFIGS= Note that for a specific IP address, the last component is a file, having the same structure as the __default__. For example, to block the address "10.1.2.3", you would create a file named "3" under the directory ".../config/client_address/10/1/2". The above sets CHECKERS and OTHERCONFIGS to nothing, so for that subnet no checks are done. All other IP address blocks are still using the top level __default__ CONFIGURATION VALUES SPFSEEDONLY=1 will only check SPF, not use it for decisions. GREYLISTTIME is the number of seconds to wait before allowing a an incoming message. Unless you have a good reason for it, this should never be more than 3 hours or it may cause warnings about undeliverable e-mail to be sent. CHECKERS is one of the set of 'spf' and 'greylist'. This is a list of checks to perform. Note that they are done in the listed order. OTHERCONFIGS specifies which configurations will be used. Note that these configurations are read a maximum of once, and are applied in order. If another configuration changes this list, any configurations that are already done will be skipped. Allowed values are: client_address Look for configuration values based on the remote IP address. For example, if the remote host "10.9.8.7" is connecting, the following will be tried: .../config/client_address/10/__default__ .../config/client_address/10/9/__default__ .../config/client_address/10/9/8/__default__ .../config/client_address/10/9/8/7 envelope_sender Split the envelope sender (not the header "From" address) into "domain" and "local" parts, and look for a domain-specific configuration, or a configuration specific to a particular sender. So, if "user@example.com" sends a message, the following files would be tried: .../config/envelope_sender/example.com/__default__ .../config/envelope_sender/example.com/user Note that special characters other than @, _ (underscore), - (dash), . (dot), and + (plus) are escaped using "%DD" format, where "DD" is the hex value of the ASCII character. Also note that a leading "." in a domain or user is converted to "%2e", to prevent the confusion of "hidden files". envelope_recipient This is handled the same as envelope_sender, but is the envelope recipient. Note that this is not the value of the "To" header in the message, but the value in the envelope. GREYLISTEXPIREDAYS is a floating point number of days since receiving the last piece of e-mail after which a greylist entry will be expired. This value is used by "tumgreyspf-clean". INSTALL INSTRUCTIONS The fastest way to install tumgreyspf is to use the package for your system. This will use "tumgreyspf-install" to attempt to automatically configure postfix for tumgreyspf. However, it's recommended that you carefully review the Postfix configuration changes and verify that they are as you expect. INSTALLING THE SOFTWARE This does not need to be done if you've installed the RPM/Debian package. tumgreyspf uses two directories. One is for the main tumgreyspf code, and the other is for it's data/configuration. I call these directories "$TGSPROG" and "$TGSDATA" in the instructions below. Additionally, the user which tumgreyspf runs as is "$TGSUSER". Run the following commands: TGSPROG=/usr/local/lib/tumgreyspf TGSDATA=/var/local/lib/tumgreyspf TGSUSER=nobody # set up directories mkdir -p "$TGSPROG" "$TGSDATA"/config mkdir "$TGSDATA"/data chown -R nobody "$TGSDATA"/data cp __default__.dist "$TGSDATA"/config/__default__ # install programs cp tumgreyspf tumgreyspf-clean tumgreyspf-configtest "$TGSPROG" cp tumgreyspf-install tumgreyspf-stat tumgreyspfsupp.py "$TGSPROG" cp tumgreyspf.conf "$TGSDATA"/config/ # change permissions and ownership chown -R "$TGSUSER" "$TGSDATA" chown -R root "$TGSPROG" "$TGSDATA"/config chmod 700 "$TGSDATA"/data chmod -R 755 "$TGSDATA"/config If you have changed the values of TGSPROG or TGSDATA, you will need to change the the paths in the following files. In the .conf file, you will need to review the whole file, the other files have the required changes isolated to the top of the file: "$TGSDATA"/config/tumgreyspf.conf "$TGSPROG"/tumgreyspfsupp.py "$TGSPROG"/tumgreyspf "$TGSPROG"/tumgreyspf-clean "$TGSPROG"/tumgreyspf-stat CRONTAB WARNING: Make *SURE* you do this step, as not cleaning out the database may result in resource exhaustion in your file-system. Next, you will need to add a cron job which runs daily to clean out the the expired SPF entries. On many systems, there is a "/etc/cron.d" directory, and the following can be be used to add an entry: echo 0 0 * * * $TGSUSER $TGSPROG/tumgreyspf-clean \ >/etc/cron.d/tumgreyspf Otherwise, you will need to use "crontab -e -u $TGSUSER" to add the following entry: 0 0 * * * $TGSPROG/tumgreyspf-clean Note that you cannot use the literal "$TGSPROG", you will have to replace it with whatever the real value is. CONFIGURING POSTFIX WARNING: In these examples, you cannot use the literal "$TGS" variables. You will have to manually replace the appropriate values, they are simply there to mark where the changes need to be. Add to your postfix master.cf: tumgreyspf unix - n n - - spawn user=nobody argv=$TGSPROG/tumgreyspf Next, main.cf must be configured so that "smtpd_recipient_restrictions" includes a call to the tumgreyspf policy filter. If you already have a "smtpd_recipient_restrictions" line(s), you can add the following line anywhere after the line which reads "reject_unauth_destination". check_policy_service unix:private/tumgreyspf WARNING: It's very important that you have "reject_unauth_destination" before the "check_policy_service". If you do not, your system may be an open relay. So, for example, a minimal "smtpd_recipient_restrictions" may look like: smtpd_recipient_restrictions = \ reject_unauth_destination, \ check_policy_service unix:private/tumgreyspf Please consult the postfix documentation for more information on these and other settings you may wish to have in the "smtpd_recipient_restrictions" configuration. You will also need to have a line in the main.cf which reads: tumgreyspf_time_limit = 3600 SPF INSTALLATION NOTE: SPF is optional, but it's use, particularly it's use before greylisting, will help reduce spam and will reduce the size of the greylist database. This may prevent or lessen the problems mentioned in the "NOTE BEFORE YOU USE TUMGREYSPF" section. tumgreyspf can also use an external SPF program to do SPF lookups. You can use any of the following: Download libspf2 from http://www.libspf2.org/ untar and run "./configure; make", then copy "src/spfquery/spfquery_static" to "$TGSPROG". The Mail::SPF::Query Perl module includes a "spfquery" package that tumgreyspf can be used with. Once installed, change your tumgreyspf.conf file to list the path to "spfquery". Information on downloading this package is available from http://spf.pobox.com/downloads.html The Python pyspf package from http://www.wayforward.net/spf/ can also be used. If this is installed, tumgreyspf will automatically use it. COMMON PROBLEMS SPF checks need to be bypassed for relays for the domain, such as secondary MX servers. Putting an mx entry in your SPF TXT record is not sufficient to do this, as that only covers your *OUTGOING* e-mail. Incoming e-mail is controlled by the senders SPF record, which probably doesn't list your secondary MX hosts. :-) One way of bypassing this check would be to ensure that MX servers are listed in mynetworks, and that permit_mynetworks is ahead of the call to tumgreyspf. tumgreyspf-1.36/tumgreyspf-clean0000755000175000017500000000461411663450516015517 0ustar jafojafo#!/usr/bin/env python # # Walk through the greylist directory and clean out old entries, based on # the values in the config file. # # Copyright (c) 2004-2007, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # import os, re, string, syslog, sys, time sys.path.append('/usr/local/lib/tumgreyspf') import tumgreyspfsupp ################### def syslogprint(s): print s syslog.syslog = syslogprint ################################## def visit(config, dirname, fileList): ospathisfile = os.path.isfile ospathjoin = os.path.join base = config['greylistBasedir'] rx = re.compile(r'^/?(\d+)/(\d+)/(\d+)/((\d+)/)?greylist/(.*)$') rxIp = re.compile(r'^/?(\d+)/(\d+)/(\d+)/((\d+)/)?check_file') didUnlink = 0 for file in fileList: path = ospathjoin(dirname, file) if not ospathisfile(path): continue recipient = file relative = path[len(base):] if configGlobal['greylistByIPOnly'] > 0: m = rxIp.match(relative) else: m = rx.match(relative) if not m: print 'Unknown path "%s" found in greylist directory.' % relative continue # get IP information groups = m.groups() ipList = list(groups[:3]) if groups[3] != None: ipList.append(groups[4]) else: ipList.append('0') ip = string.join(ipList, '.') sender = groups[5] # look up expration day data = { 'envelope_sender' : tumgreyspfsupp.unquoteAddress(sender), 'envelope_recipient' : tumgreyspfsupp.unquoteAddress(recipient), 'client_address' : ip, } configData = tumgreyspfsupp.lookupConfig(config.get('configPath'), data, config.copy()) expireTime = time.time() - (configData['GREYLISTEXPIREDAYS'] * 86400) # check for expiration statData = os.stat(path) mtime = statData[8] ctime = statData[9] expiredAuth = ctime < mtime and (time.time() - mtime) > (12 * 3600) expiredAfterAuth = ctime < expireTime # check for expiration if expiredAuth or expiredAfterAuth: os.remove(path) didUnlink = 1 # remove this directory and it's parents if empty if didUnlink: newDirname = dirname while len(newDirname) > len(base): try: os.rmdir(newDirname) except OSError: break newDirname = os.path.dirname(newDirname) ############################ # main code config = tumgreyspfsupp.processConfigFile() greylistBasedir = os.path.join(config['greylistDir'], 'client_address') config['greylistBasedir'] = greylistBasedir os.path.walk(greylistBasedir, visit, config) tumgreyspf-1.36/tumgreyspf-configtest0000755000175000017500000000125111663450516016574 0ustar jafojafo#!/usr/bin/env python # vim: ts=3 sw=3 ai # # Test the configuration file, report any errors # # Copyright (c) 2004-2007, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # G_Id = '$Id: tumgreyspf-configtest,v 1.4 2007-06-10 01:11:11 jafo Exp $' import sys # check arguments if len(sys.argv) != 2: print 'ERROR: Configuration file must be specified as a command-line ' \ 'argument.' sys.exit(2) # test load of config try: execfile(sys.argv[1], {}, {}) except Exception, e: import traceback etype, value, tb = sys.exc_info() print ('Error reading config file "%s": %s' % ( sys.argv[1], sys.exc_info()[1] )) sys.exit(1) sys.exit(1) tumgreyspf-1.36/tumgreyspf.conf0000644000175000017500000000441111663450516015353 0ustar jafojafo# Ammount of debugging information logged. 0 logs no debugging messages # 4 includes all debug messages. debugLevel = 0 # If set to 1, no messages are rejected by greylisting. This allows a # greylist to be built up without blocking legitimate messages. Enable # this for a day or week if you want to make sure your most common # correspondants do not get greylisted. defaultSeedOnly = 0 # If not overridden in the directory-based configuration, this is the # amount of time of new greylist entries. defaultAllowTime = 600 # Path to the directory-based configuration. This can be used to override # the configuration values for certain remote hosts, senders or # recipients. configPath = 'file:///var/local/lib/tumgreyspf/config' # Directory where the greylist resides. greylistDir = '/var/local/lib/tumgreyspf/data' # Path to the program used for SPF checking. This can either be the # perl version available from http://www.openspf.com/ or the # "spfquery-static" program built from libspf2, also available from # http://www.openspf.com/ spfqueryPath = '/usr/local/lib/tumgreyspf/spfquery' # Directory where the blackhole information goes. "ips" sub-directory # contains IPs that have touched us with a bad address. "addresses" # sub-directory has a file per address named after the bad addresses. blackholeDir = '/var/lib/tumgreyspf/blackhole' # If set to 1, the last byte of the sender's IP address will be ignored. # So, if mail from 1.2.3.4 was delayed, then redelivered by 1.2.3.58, # the message would be accepted as if it came from the same server. # This allows pools of mail servers to appear as one, increasing # compatibility with large email services. ignoreLastByte = 1 # If set to one, message will only be greylisted by the IP of the # originating machine, not by IP, sender address, and reciever address # as is the default. This option honors the 'ingoreLastByte' option. greylistByIPOnly = 1 # If set to zero, permanent spf errors will reject the message. # If set to one, permanent spf errors do not reject the message. # Permanent errors are user errors in the domains SPF record # that cause SPF processing to fail: for example typos, recursive # inclusion, having two spf records for a domain, etc. # Default is 1 spfAcceptOnPermError = 1 tumgreyspf-1.36/TODO0000644000175000017500000000013011663450516012761 0ustar jafojafoDocumentation Something which generates stats and/or graphs of greylisting. Package it. tumgreyspf-1.36/README.performance0000644000175000017500000000252011663450516015456 0ustar jafojafoI have run tests of this code on two machines to determine the performance it is capable of. The first is my development laptop, a 1.2GHz Celeron (with cpufreq pushing it down to 800MHz), running on the JFS filesystem. The other is our mail server, a 2.66Ghz P4 running on the XFS filesystem running on a two-drive RAID-1 storage system. This RAID array was two 7200RPM Hitachi 80GB SATA drives using a 3ware controller. Both machines had the "safe writing" disabled for maillog in syslog.conf (by prefixing the maillog path name with a "-"). My test data was 50,756 greylist requests culled from the log files accumulated over a few days of actual traffic. The test used greylisting first, and then SPF if greylisting did not block. I believe that in most cases greylisting would be blocking. On the development laptop, it took 5 minutes 1 second, for an overall rate of 168 RCPTs per second. On the mail server, it took 2 minute 12 seconds, for an overall rate of 384 RCPTs per second. These both seemed to be using 100% CPU time. Obviously, with other mail processing going on there will be less CPU and disc bandwidth available for tumgreyspf. However, greylisting is blocking well over 80% of incoming messages right now on our server. Thus, there are around 5x fewer mesages being processed, leading to much more overall available resources. tumgreyspf-1.36/tumgreyspfsupp.py0000644000175000017500000002127211663450516015772 0ustar jafojafo#!/usr/bin/env python # # Copyright (c) 2004-2007, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved. S_rcsid = '$Id: tumgreyspfsupp.py,v 1.8 2007-06-10 01:11:11 jafo Exp $' import syslog, os, sys, string, re, time, popen2, urllib, stat # default values defaultConfigFilename = '/var/local/lib/tumgreyspf/config/tumgreyspf.conf' defaultConfigData = { 'debugLevel' : 0, 'defaultSeedOnly' : 0, 'defaultAllowTime' : 600, 'configPath' : 'file:///var/local/lib/tumgreyspf/config', 'greylistDir' : '/var/local/lib/tumgreyspf/data', 'blackholeDir' : '/var/local/lib/tumgreyspf/blackhole', 'spfqueryPath' : '/usr/local/lib/tumgreyspf/spfquery', 'ignoreLastByte' : 0, } ################################# class ConfigException(Exception): '''Exception raised when there's a configuration file error.''' pass ################################# def loadConfigFile(file, values): '''Load the specified config file if it exists, raise ValueError if there is an error in the config file. "values" is a dictionary of default config values. "values" is modified in place, and nothing is returned.''' if not os.path.exists(file): return try: execfile(file, {}, values) except Exception, e: import traceback etype, value, tb = sys.exc_info() raise ConfigException, ('Error reading config file "%s": %s' % ( file, sys.exc_info()[1] )) return() #################################################################### def processConfigFile(filename = None, config = None, useSyslog = 1, useStderr = 0): '''Load the specified config file, exit and log errors if it fails, otherwise return a config dictionary.''' import tumgreyspfsupp if config == None: config = tumgreyspfsupp.defaultConfigData if filename == None: filename = tumgreyspfsupp.defaultConfigFilename try: loadConfigFile(filename, config) except Exception, e: if useSyslog: syslog.syslog(e.args[0]) if useStderr: sys.stderr.write('%s\n' % e.args[0]) sys.exit(1) return(config) ################# class ExceptHook: def __init__(self, useSyslog = 1, useStderr = 0): self.useSyslog = useSyslog self.useStderr = useStderr def __call__(self, etype, evalue, etb): import traceback, string tb = traceback.format_exception(*(etype, evalue, etb)) tb = map(string.rstrip, tb) tb = string.join(tb, '\n') for line in string.split(tb, '\n'): if self.useSyslog: syslog.syslog(line) if self.useStderr: sys.stderr.write(line + '\n') #################### def setExceptHook(): sys.excepthook = ExceptHook(useSyslog = 1, useStderr = 1) #################### def quoteAddress(s): '''Quote an address so that it's safe to store in the file-system. Address can either be a domain name, or local part. Returns the quoted address.''' s = urllib.quote(s, '@_-+') if len(s) > 0 and s[0] == '.': s = '%2e' + s[1:] return(s) ###################### def unquoteAddress(s): '''Undo the quoting of an address. Returns the unquoted address.''' return(urllib.unquote(s)) ############################################################### commentRx = re.compile(r'^(.*)#.*$') def readConfigFile(path, configData = None, configGlobal = {}): '''Reads a configuration file from the specified path, merging it with the configuration data specified in configData. Returns a dictionary of name/value pairs based on configData and the values read from path.''' debugLevel = configGlobal.get('debugLevel', 0) if debugLevel >= 3: syslog.syslog('readConfigFile: Loading "%s"' % path) if configData == None: configData = {} nameConversion = { 'SPFSEEDONLY' : int, 'GREYLISTTIME' : int, 'CHECKERS' : str, 'OTHERCONFIGS' : str, 'GREYLISTEXPIREDAYS' : float, } # check to see if it's a file try: mode = os.stat(path)[0] except OSError, e: syslog.syslog('ERROR stating "%s": %s' % ( path, e.strerror )) return(configData) if not stat.S_ISREG(mode): syslog.syslog('ERROR: is not a file: "%s", mode=%s' % ( path, oct(mode) )) return(configData) # load file fp = open(path, 'r') while 1: line = fp.readline() if not line: break # parse line line = string.strip(string.split(line, '#', 1)[0]) if not line: continue data = map(string.strip, string.split(line, '=', 1)) if len(data) != 2: syslog.syslog('ERROR parsing line "%s" from file "%s"' % ( line, path )) continue name, value = data # check validity of name conversion = nameConversion.get(name) if conversion == None: syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path )) continue if debugLevel >= 4: syslog.syslog('readConfigFile: Found entry "%s=%s"' % ( name, value )) configData[name] = conversion(value) fp.close() return(configData) #################################################### def lookupConfig(configPath, msgData, configGlobal): '''Given a path, load the configuration as dictated by the msgData information. Returns a dictionary of name/value pairs.''' debugLevel = configGlobal.get('debugLevel', 0) # set up default config configData = { 'SPFSEEDONLY' : configGlobal.get('defaultSeedOnly'), 'GREYLISTTIME' : configGlobal.get('defaultAllowTime'), 'CHECKGREYLIST' : 1, 'CHECKSPF' : 1, 'OTHERCONFIGS' : 'envelope_sender,envelope_recipient', } # load directory-based config information if configPath[:8] == 'file:///': if debugLevel >= 3: syslog.syslog('lookupConfig: Starting file lookup from "%s"' % configPath) basePath = configPath[7:] configData = {} # load default config path = os.path.join(basePath, '__default__') if os.path.exists(path): if debugLevel >= 3: syslog.syslog('lookupConfig: Loading default config: "%s"' % path) configData = readConfigFile(path, configData, configGlobal) else: syslog.syslog(('lookupConfig: No default config found in "%s", ' 'this is probably an install problem.') % path) # load other configs from OTHERCONFIGS configsAlreadyLoaded = {} didLoad = 1 while didLoad: didLoad = 0 otherConfigs = string.split(configData.get('OTHERCONFIGS', ''), ',') if not otherConfigs or otherConfigs == ['']: break if debugLevel >= 3: syslog.syslog('lookupConfig: Starting load of configs: "%s"' % str(otherConfigs)) # SENDER/RECIPIENT for cfgType in otherConfigs: cfgType = string.strip(cfgType) # skip if already loaded if configsAlreadyLoaded.get(cfgType) != None: continue configsAlreadyLoaded[cfgType] = 1 didLoad = 1 if debugLevel >= 3: syslog.syslog('lookupConfig: Trying config "%s"' % cfgType) # SENDER/RECIPIENT if cfgType == 'envelope_sender' or cfgType == 'envelope_recipient': # get address if cfgType == 'envelope_sender': address = msgData.get('sender') else: address = msgData.get('recipient') if not address: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find %s' % cfgType) continue # split address into domain and local data = string.split(address, '@', 1) if len(data) != 2: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find %s address ' 'from "%s", skipping' % ( cfgType, address )) continue local = quoteAddress(data[0]) domain = quoteAddress(data[1]) # load configs path = os.path.join(basePath, cfgType) domainPath = os.path.join(path, domain, '__default__') localPath = os.path.join(path, domain, local) for name in ( domainPath, localPath ): if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % name) if os.path.exists(name): configData = readConfigFile(name, configData, configGlobal) # CLIENT IP ADDRESS elif cfgType == 'client_address': ip = msgData.get('client_address') if not ip: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find client ' 'address') else: path = basePath for name in [ 'client_address' ] \ + list(string.split(ip, '.')): path = os.path.join(path, name) defaultPath = os.path.join(path, '__default__') if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % defaultPath) if os.path.exists(defaultPath): configData = readConfigFile(defaultPath, configData, configGlobal) if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % path) if os.path.exists(path): configData = readConfigFile(path, configData, configGlobal) # unknown configuration type else: syslog.syslog('ERROR: Unknown configuration type: "%s"' % cfgType) # unkonwn config path else: syslog.syslog('ERROR: Unknown path type in: "%s", using defaults' % msgData) # return results return(configData) tumgreyspf-1.36/tumgreyspf-addip0000755000175000017500000000262111663450516015512 0ustar jafojafo#!/usr/bin/perl # # Whitelist an IP address for tumgreyspf. use strict; my $live = 1; my $base_dir = q(/var/local/lib/tumgreyspf/config/client_address/); my $conf_contents = q(SPFSEEDONLY=0 GREYLISTTIME=300 CHECKERS= OTHERCONFIGS=); my $conf_name = q(__default__); my $mkdir = q(/bin/mkdir); #print $conf_name."\n"; #print $conf_contents."\n"; while (<>) { chomp; my $dirtree = ""; if ($_ =~ /^(\d+)\.(\d+)\.(\d+).(\d+)/) { $dirtree = $base_dir.$1."/".$2."/".$3."/".$4."/"; } elsif ($_ =~ /^(\d+)\.(\d+)\.(\d+)/) { $dirtree = $base_dir.$1."/".$2."/".$3."/"; } elsif ($_ =~ /^(\d+)\.(\d+)/) { $dirtree = $base_dir.$1."/".$2."/"; } elsif ($_ =~ /^(\d+)/) { $dirtree = $base_dir.$1."/"; } if (($dirtree =~ /^$base_dir/) and ($live)) { run_it($mkdir." -p ".$dirtree."\n"); print "writing ".$dirtree.$conf_name."\n"; open (OUTFILE, ">".$dirtree.$conf_name) or die "Can't write ".$dirtree.$conf_name." : $!\n"; print OUTFILE $conf_contents; close (OUTFILE); } else { print "not running : ".$mkdir." -p ".$dirtree."\n"; print "not writing : ".$dirtree.$conf_name."\n"; } } ########################################################################## sub run_it { my ($command) = @_; print $command."\n"; if ($live) { open (COMMAND, "$command 2>&1 |") or die "Can't run $command : $!\n"; while () { print; } close (COMMAND); } } tumgreyspf-1.36/tumgreyspf-test.conf0000644000175000017500000000257411663450516016340 0ustar jafojafo# Ammount of debugging information logged. 0 logs no debugging messages # 4 includes all debug messages. debugLevel = 0 # If set to 1, no messages are rejected by greylisting. This allows a # greylist to be built up without blocking legitimate messages. Enable # this for a day or week if you want to make sure your most common # correspondants do not get greylisted. defaultSeedOnly = 0 # If not overridden in the directory-based configuration, this is the # amount of time of new greylist entries. defaultAllowTime = 600 # Path to the directory-based configuration. This can be used to override # the configuration values for certain remote hosts, senders or # recipients. configPath = 'file:///home/jafo/projects/tumgreyspf/test/config' # Directory where the greylist resides. greylistDir = '/home/jafo/projects/tumgreyspf/test/data' # Path to the program used for greylist checking. This can either be the # perl version available from http://spf.pobox.com/ or the # "spfquery-static" program built from libspf2, also available from # http://spf.pobox.com/ spfqueryPath = '/usr/local/lib/postfix/spfquery' # Directory where the blackhole information goes. "ips" sub-directory # contains IPs that have touched us with a bad address. "addresses" # sub-directory has a file per address named after the bad addresses. blackholeDir = '/usr/local/lib/tumgreyspf/blackhole', tumgreyspf-1.36/__default__.dist0000644000175000017500000000037311663450516015407 0ustar jafojafoSPFSEEDONLY = 0 GREYLISTTIME = 600 CHECKERS = spf,greylist OTHERCONFIGS = client_address,envelope_sender,envelope_recipient # The number of days after which, if no messages have come in, we will # drop the greylist entry. GREYLISTEXPIREDAYS = 10.0 tumgreyspf-1.36/tumgreyspf-stat0000755000175000017500000000504311663450516015405 0ustar jafojafo#!/usr/bin/env python # # Display information about the entries in the greylist. # # Copyright (c) 2004-2007, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # import os, re, string, syslog, sys, time sys.path.append('/usr/local/lib/tumgreyspf') import tumgreyspfsupp ################### def syslogprint(s): print s syslog.syslog = syslogprint ################################## def visit(config, dirname, fileList): ospathisfile = os.path.isfile ospathjoin = os.path.join base = config['greylistBasedir'] rx = re.compile(r'^/?(\d+)/(\d+)/(\d+)/(\d+)/greylist/(.*)$') if config['ignoreLastByte'] > 0: rx = re.compile(r'^/?(\d+)/(\d+)/(\d+)/greylist/(.*)$') didUnlink = 0 for file in fileList: path = ospathjoin(dirname, file) if not ospathisfile(path): continue recipient = file relative = dirname[len(base):] m = rx.match(relative) if not m: print 'Unknown path "%s" found in greylist directory.' % relative continue ip = string.join(m.groups()[:-1], '.') sender = m.groups()[-1] # look up expration day data = { 'envelope_sender' : tumgreyspfsupp.unquoteAddress(sender), 'envelope_recipient' : tumgreyspfsupp.unquoteAddress(recipient), 'client_address' : ip, } configData = tumgreyspfsupp.lookupConfig(config.get('configPath'), data, config.copy()) expireTime = time.time() - (configData['GREYLISTEXPIREDAYS'] * 86400) # check statData = os.stat(path) mtime = statData[8] ctime = statData[9] now = time.time() # status information stats = '' if ctime < mtime: stats = stats + 'Blocked,' if mtime > now: stats = stats + 'Pending,' if stats: stats = ' (%s)' % stats[:-1] def prettyseconds(seconds): for prettySeconds, prettyDescription in ( ( 86400, 'd' ), ( 3600, 'h' ), ( 60, 'm' ), ): if seconds > prettySeconds: return('%d%s' % ( seconds / prettySeconds, prettyDescription )) return('%ss' % seconds) # display information print ('IP=%s SENDER=%s RECIPIENT=%s STARTS=%s LAST=%s EXPIRESIN=%s%s' % ( ip, sender, recipient, prettyseconds(now - mtime), prettyseconds(now - ctime), prettyseconds(int(expireTime - now)), stats )) ############################ # main code config = tumgreyspfsupp.processConfigFile() greylistBasedir = os.path.join(config['greylistDir'], 'client_address') config['greylistBasedir'] = greylistBasedir try: os.path.walk(greylistBasedir, visit, config) # ignore interrupts and errors writing stdout except IOError, e: if e.errno != 32: raise except KeyboardInterrupt: pass tumgreyspf-1.36/tumgreyspf0000755000175000017500000003204011663450516014431 0ustar jafojafo#!/usr/bin/env python # vim: ts=3 sw=3 ai # # Log information about incoming SMTP connections. # # Copyright (c) 2004-2007, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # S_rcsid = '$Id: tumgreyspf,v 1.29 2007-10-08 00:39:37 jafo Exp $' import syslog, os, sys, string, re, time, popen2, urllib, stat, errno, socket import spf sys.path.append('/usr/local/lib/tumgreyspf') import tumgreyspfsupp syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, syslog.LOG_MAIL) tumgreyspfsupp.setExceptHook() ############################################# def cidrmatch(connectip, ipaddrs, n): """Match connect IP against a list of other IP addresses. From pyspf.""" try: if connectip.count(':'): MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL connectip = spf.inet_pton(connectip) for arg in ipaddrs: ipaddrs[ipaddrs.index(arg)] = spf.inet_pton(arg) bin = spf.bin2long6 else: MASK = 0xFFFFFFFFL bin = spf.addr2bin c = ~(MASK >> n) & MASK & bin(connectip) for ip in [bin(ip) for ip in ipaddrs]: if c == ~(MASK >> n) & MASK & ip: return True except socket.error: pass return False def parse_cidr(cidr_ip): """Breaks CIDR notation into a (address,cidr,cidr6) tuple. The cidr defaults to 32 if not present. Derived from pyspf""" import re RE_DUAL_CIDR = re.compile(r'//(0|[1-9]\d*)$') RE_CIDR = re.compile(r'/(0|[1-9]\d*)$') a = RE_DUAL_CIDR.split(cidr_ip) if len(a) == 3: cidr_ip, cidr6 = a[0], int(a[1]) else: cidr6 = None a = RE_CIDR.split(cidr_ip) if len(a) == 3: cidr_ip, cidr = a[0], int(a[1]) else: cidr = None b = cidr_ip.split(':', 1) if len(b) < 2: return cidr_ip, cidr return a[0], cidr6 ############################################# def spfcheck(data, configData, configGlobal): #{{{1 debugLevel = configGlobal.get('debugLevel', 0) queue_id = ('QUEUE_ID="%s"' % ( data.get('queue_id', ''))) ip = data.get('client_address') if ip == None: if debugLevel: syslog.syslog('spfcheck: No client address, exiting') return(( None, None )) # Do not check SPF for localhost addresses - add to skip addresses to # skip SPF for internal networks if desired. skip_addresses = ['127.0.0.0/8', '::ffff:127.0.0.0//104', '::1//128',] for cidr in skip_addresses: parsed_address = parse_cidr(cidr) good_ip = [parsed_address[0],] if cidrmatch(ip, good_ip, int(parsed_address[1])): return (( None, 'SPF check N/A for local connections' )) sender = data.get('sender') helo = data.get('helo_name') if not sender and not helo: if debugLevel: syslog.syslog('spfcheck: No sender or helo, exiting') return(( None, None )) # if no helo name sent, use domain from sender if not helo: foo = string.split(sender, '@', 1) if len(foo) < 2: helo = 'unknown' else: helo = foo[1] # start query spfResult = None spfReason = None if hasattr(spf, 'check2'): # use the pySPF 2.0/RFC 4408 interface try: ret = spf.check2(i = ip, s = sender, h = helo) except Exception, e: header = 'TumGreySPF-Warning: SPF Check failed: %s' % str(e) return(( 'prepend', header )) spfReason = repr(str(string.strip(ret[1]))) spfResult = string.strip(ret[0]) spfResult = spfResult.lower().capitalize() if spfResult == 'None': # allow checking to continue on to other checkers if no SPF return(( None, None )) if sender: identity = 'identity=mailfrom; ' else: identity = 'identity=helo; ' spfDetail = (identity + 'client-ip=%s; helo=%s; envelope-from=%s; ' 'receiver=%s; ' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), )) syslog.syslog('%s: %s; %s' % ( spfReason, queue_id, spfDetail )) if spfResult == 'Fail': return(( 'reject', '%s SPF Reports: %s' % ( queue_id, spfReason ))) spfAcceptOnPermError = configGlobal.get('spfAcceptOnPermError', 1) if spfResult == 'Permerror' and not spfAcceptOnPermError: return(( 'reject', '%s SPF Reports: %s' % ( queue_id, spfReason ))) if spfResult == 'Temperror': return(( 'defer', '%s SPF Reports: %s' % ( queue_id, spfReason ))) header = ('Received-SPF: '+ spfResult + ' (' + spfReason + ') ' + spfDetail) return(( 'prepend', header )) else: # fall back to older pre-RFC interface try: ret = spf.check(i = ip, s = sender, h = helo) except Exception, e: header = 'TumGreySPF-Warning: SPF Check failed: %s' % str(e) return(( 'prepend', header )) spfResult = string.strip(ret[0]) spfReason = repr(str(string.strip(ret[2]))) # try spfquery if not spfResult: # check for spfquery spfqueryPath = configGlobal['spfqueryPath'] if not os.path.exists(spfqueryPath): if debugLevel: syslog.syslog('spfcheck: No spfquery at "%s", exiting' % spfqueryPath) return(( None, None )) # open connection to spfquery fpIn, fpOut = popen2.popen2('%s -file -' % spfqueryPath) fpOut.write('%s %s %s\n' % ( ip, sender, helo )) fpOut.close() spfData = fpIn.readlines() fpIn.close() if debugLevel: syslog.syslog('spfcheck: spfquery result: "%s"' % str(spfData)) spfResult = string.strip(spfData[0]) spfReason = repr(str(string.strip(spfData[1]))) # read result if spfResult == 'fail' or spfResult == 'deny': syslog.syslog('SPF fail: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" %s REASON="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), queue_id, spfReason ) ) return(( 'reject', '%s SPF Reports: %s' % ( queue_id, spfReason ) )) if debugLevel: syslog.syslog('spfcheck: pyspf result: "%s"' % str(ret)) return(( None, None )) ################################################## def greylistcheck(data, configData, configGlobal): #{{{1 greylistDir = configGlobal['greylistDir'] ip = data.get('client_address') if ip == None: return(( None, None )) ipBytes = string.split(ip, '.') if configGlobal['ignoreLastByte'] > 0: ipBytes = ipBytes[:-1] ipPath = string.join(ipBytes, '/') if configGlobal['greylistByIPOnly'] > 0: dir = os.path.join(greylistDir, ipPath) path = os.path.join(dir, 'check_file') else: sender = data.get('sender') recipient = data.get('recipient') if not sender or not recipient: return(( None, None )) sender = tumgreyspfsupp.quoteAddress(sender) recipient = tumgreyspfsupp.quoteAddress(recipient) dir = os.path.join(greylistDir, 'client_address', ipPath, 'greylist', sender) path = os.path.join(dir, recipient) allowTime = configData.get('GREYLISTTIME', 600) if not os.path.exists(path): if not os.path.exists(dir): # if multiple messages come in at once # it can cause multiple makedirs for i in xrange(10): try: os.makedirs(dir) break except OSError, msg: if msg.errno != errno.EEXIST: raise time.sleep(1) # still didn't succeed if not os.path.exists(dir): syslog.syslog(('ERROR: Could not create directory after ' '10 seconds: "%s"') % dir) return(( 'defer', 'Service unavailable, error creating data ' 'directory. See /var/log/maillog for more information.' )) # create file open(path, 'w').close() now = time.time() mtime = now + allowTime os.utime(path, ( now, mtime )) if configGlobal.get('defaultSeedOnly'): syslog.syslog( 'Training greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( None, None )) syslog.syslog('Initial greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'defer', 'Service unavailable, greylisted ' '(http://projects.puremagic.com/greylisting/).' )) # is it time to allow yet mtime = os.stat(path)[8] now = time.time() os.utime(path, ( now, mtime )) if mtime > now and not configGlobal.get('defaultSeedOnly'): syslog.syslog('Pending greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'defer', 'Service unavailable, greylisted.' )) syslog.syslog('Allowed greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( None, None )) ################################################### def blackholecheck(data, configData, configGlobal): #{{{1 blackholeDir = configGlobal['blackholeDir'] ip = data.get('client_address') if ip == None: return(( None, None )) ipPath = string.join(string.split(ip, '.'), '/') dir = os.path.join(blackholeDir, 'ips', ipPath) recipient = data.get('recipient') if not recipient: return(( None, None )) recipient = tumgreyspfsupp.quoteAddress(recipient) # add blackhole recipientPath = os.path.join(blackholeDir, 'addresses', recipient) if os.path.exists(recipientPath): if not os.path.exists(dir): os.path.makedirs(dir) # check for existing blackhole entry if os.path.exists(dir): syslog.syslog('Blackholed: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'reject', 'Service unavailable, blackholed.' )) return(( None, None )) ################### # load config file {{{1 configFile = tumgreyspfsupp.defaultConfigFilename if len(sys.argv) > 1: if sys.argv[1] in ( '-?', '--help', '-h' ): print 'usage: tumgreyspf []' sys.exit(1) configFile = sys.argv[1] configGlobal = tumgreyspfsupp.processConfigFile(filename = configFile) # loop reading data {{{1 debugLevel = configGlobal.get('debugLevel', 0) if debugLevel >= 2: syslog.syslog('Starting') instance_list = [] data = {} lineRx = re.compile(r'^\s*([^=\s]+)\s*=(.*)$') while 1: line = sys.stdin.readline() if not line: break line = string.rstrip(line) if debugLevel >= 4: syslog.syslog('Read line: "%s"' % line) # end of entry {{{2 if not line: if debugLevel >= 4: syslog.syslog('Found the end of entry') configData = tumgreyspfsupp.lookupConfig(configGlobal.get('configPath'), data, configGlobal) if debugLevel >= 2: syslog.syslog('Config: %s' % str(configData)) # run the checkers {{{3 checkerValue = None checkerReason = None for checkerType in string.split(configData.get('CHECKERS', ''), ','): checkerType = string.strip(checkerType) if checkerType == 'greylist': checkerValue, checkerReason = greylistcheck(data, configData, configGlobal) if checkerValue != None: break elif checkerType == 'spf': checkerValue, checkerReason = spfcheck(data, configData, configGlobal) if configData.get('SPFSEEDONLY', 0): checkerValue = None checkerReason = None if checkerValue != None and checkerValue != 'prepend': break elif checkerType == 'blackhole': checkerValue, checkerReason = blackholecheck(data, configData, configGlobal) if checkerValue != None: break # handle results {{{3 if checkerValue == 'reject': sys.stdout.write('action=550 %s\n\n' % checkerReason) elif checkerValue == 'prepend': instance = data.get('instance') # The following if is only needed for testing. Postfix # will always provide instance. if not instance: import random instance = str(int(random.random()*100000)) # This is to prevent multiple headers being prepended # for multi-recipient mail. found_instance = instance_list.count(instance) if found_instance == 0: sys.stdout.write('action=prepend %s\n\n' % checkerReason) instance_list.append(instance) else: sys.stdout.write('action=dunno\n\n') elif checkerValue == 'defer': sys.stdout.write('action=defer_if_permit %s\n\n' % checkerReason) else: sys.stdout.write('action=dunno\n\n') # end of record {{{3 sys.stdout.flush() data = {} continue # parse line {{{2 m = lineRx.match(line) if not m: syslog.syslog('ERROR: Could not match line "%s"' % line) continue # save the string {{{2 key = m.group(1) value = m.group(2) if key not in [ 'protocol_state', 'protocol_name', 'queue_id' ]: value = string.lower(value) data[key] = value tumgreyspf-1.36/LICENSE0000644000175000017500000004330211663450516013306 0ustar jafojafo GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.