postgrey-1.36/0000755000175000017500000000000012571400066011474 5ustar dwsdwspostgrey-1.36/policy-test0000755000175000017500000001404212252310311013664 0ustar dwsdws#!/usr/bin/perl -w # policy-test: Postfix Policy Daemon Testing Tool # Copyright (c) 2007 Open Systems AG, Switzerland # released under the GNU General Public License # see the documentation with 'perldoc policy-test' use strict; use Getopt::Long 2.25 qw(:config posix_default no_ignore_case); use Pod::Usage; use Parse::Syslog; use IO::Socket::INET; my $VERSION = '0.1'; my $requests; # total number of requests done my $policy_service; my %opt = (); sub log_to_policy($) { my ($file) = @_; my $parser = Parse::Syslog->new($file); my %state; while(my $sl = $parser->next) { $sl->{program} =~ /^postfix\/(.*)/ or next; my $pprogram = $1; if($sl->{text} =~ /^NOQUEUE/) { $sl->{text} =~ m{ ^NOQUEUE:\ reject: # NOQUEUE: reject: \ RCPT\ from # RCPT from \ ([^[]+) # mail.onezot.com \[([^]]*)\] # [64.38.44.154] :.*?; # : 450 4.1.8 }x or do { warn "can't parse: $sl->{text}\n"; next; }; my ($name, $addr, $other) = ($1, $2, $3); my ($sender, $recipient); $other =~ /\bfrom=<([^>]*)>/ and $sender=$1; $other =~ /\bto=<([^>]*)>/ and $recipient=$1; my %attrs = ( time => $sl->{timestamp}, client_name => $name, client_address => $addr, sender => $sender, recipient => $recipient ); feed_policy(\%attrs); } elsif($sl->{text} =~ /^([0-9A-F]+):(.*)/) { my ($queue_id, $data) = ($1, $2); if($data =~ / client=([^[]+)\[([^]]+)\]/) { $state{$queue_id}{time}=$sl->{timestamp}; $state{$queue_id}{client_name}=$1; $state{$queue_id}{client_address}=$2; } defined $state{$queue_id}{time} or next; if($data =~ / from=<([^>]*)>/) { $state{$queue_id}{sender}=$1; } if($data =~ /to=<([^>]*)>/) { $state{$queue_id}{recipient}=$1; } if(defined $state{$queue_id}{sender} and defined $state{$queue_id}{recipient}) { feed_policy($state{$queue_id}); delete($state{$queue_id}); } } } } my $socket; sub feed_policy($) { my ($data) = @_; $requests++; if(not defined $socket) { if($policy_service =~ /^inet:(.*):(\d+)$/) { $socket = new IO::Socket::INET( PeerAddr => $1, PeerPort => $2, Proto => 'tcp', ) or die "Can't connect to $1:$2: $!\n"; } elsif($policy_service =~ /^unix:(.*)$/) { $socket = new IO::Socket::UNIX($1) or die "Can't open $1: $1:$2: $!\n"; } } my $request = "request=smtpd_access_policy\n"; $request .= "protocol_state=RCPT\n"; $request .= "protocol_name=SMTP\n"; for my $k (keys %$data) { my $val = $data->{$k}; $k = 'policy_test_time' if $k eq 'time'; $request .= "$k=$val\n"; } $request .= "\n"; print $socket $request; print $request if $opt{verbose}; my $line; do { $line = <$socket>; print $line if $opt{verbose} and $line ne "\n"; } while($line ne "\n"); print "\n" if $opt{verbose}; } sub main() { # parse options GetOptions(\%opt, 'help|h', 'man', 'version', 'verbose|v') or exit(1); if($opt{help}) { pod2usage(1) } if($opt{man}) { pod2usage(-exitstatus => 0, -verbose => 2) } if($opt{version}) { print "postgrey $VERSION\n"; exit(0) } $policy_service = shift @ARGV; defined $policy_service and $policy_service =~ /^(unix:\S+|inet:\S+:\d+)$/ or pod2usage(1); my $start_time = time; log_to_policy('-'); my $elapsed = time-$start_time; my $rate = $elapsed != 0 ? "(". $requests / $elapsed . " r/s)" : ''; print "$requests requests in ${elapsed}s $rate\n"; } main; __END__ =head1 NAME policy-test - Postfix Policy Daemon Testing Tool =head1 SYNOPSIS B [I...] unix:/file/path B [I...] inet:hostname:port -h, --help display this help and exit --version output version information and exit -v, --verbose increase verbosity level =head1 DESCRIPTION policy-test is a script that converts Postfix log entries on the standard input into requests to the Postfix policy daemon given as the first argument. It can be used for: =over 4 =item Testing the performance of a policy daemon =item Pre-seeding a Greylist policy daemon with data from the logs =back Note that this program is still in its very early stages of development and does only support a small subset of the normal attributes used in the Postfix policy delegation protocol. It basically supports the bare minimum to make it work with Postgrey. Also note that a non-standard attribute is being generated: policy_test_time contains the Unix timestamp of when the client connected. =head1 COPYRIGHT Copyright (c) 2007 by Open Systems AG. All rights reserved. =head1 LICENSE 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., 675 Mass Ave, Cambridge, MA 02139, USA. =head1 AUTHOR Sdavid@schweikert.chE> =cut # vim: sw=4 et postgrey-1.36/README0000644000175000017500000000156012554253307012363 0ustar dwsdws Postgrey - a Postfix policy server for greylisting ================================================== Requirements ------------ - Perl >= 5.6.0 - Net::Server (Perl Module) - IO::Multiplex (Perl Module) - BerkeleyDB (Perl Module) - Berkeley DB >= 4.1 (Library) - Digest::SHA (Perl Module, only for --privacy option) - NetAddr::IP Documentation ------------- See POD documentation in postgrey. Execute: perldoc postgrey See also Postgrey's homepage: http://postgrey.schweikert.ch/ Mailing-List and getting Help ----------------------------- There is a mailing-list for the discussion of postgrey, where you can also ask for help in case of trouble. To subscribe, send a mail with subject 'subscribe' to: postgrey-request@list.ee.ethz.ch There is also a mailing-list archive, where you might find answers: http://lists.ee.ethz.ch/wws/arc/postgrey postgrey-1.36/Changes0000644000175000017500000002422512571377711013006 0ustar dwsdws* 2015-09-01: version 1.36 - improved support for IPv6 addresses: new --ipv4cidr and --ipv6cidr options for proper subnetting of IPv6 (Håvard Moen, BenediktS) - updated whitelist - user-settable unix socket permission: --socketmode (Teubel György) - init script fixes (Sacha Ott) * 2014-06-11: version 1.35 - use just 'postgrey' as process name, instead of '/usr/sbin/postgrey', because Linux tools are limited to 15 characters (#5) - Make postgrey work with Perl 5.18 (Yasuhiro KIMURA, #4) - updated whitelist * 2011-05-04: version 1.34 - gracefully handle future timestamps in the database (Andreas Hoedle) - replaced obsolete Digest::SHA1 with Digest::SHA (Salvatore Bonaccorso) - updated whitelist * 2010-05-04: version 1.33 - fix warning with IPv6 address (Edwin Kremer) - added --x-greylist-header option (Guido Leisker) - contrib/postgrey.init: new LSB-compliant init script by Adrian von Bidder (Debian) - contrib/postgreyreport: fix POD error (Christian Perrier) - contrib/postgreyreport: added --tab and --show_time options (Leonard den Ottolander) - updated whitelist * 2008-07-22: version 1.32 - fixed recipients whitelisting (David Carrel) - added --syslog-facility option (Peter Rabbitson) - add support for BATV (Edwin Groothuis) - updated whitelist * 2007-09-06: version 1.31 - support CIDR-style addresses in the client whitelist (Claudio Strizzolo) - improve logging of unresolveable hosts (Adrian von Bidder, Heiko Schlichting) - updated whitelist - fix unix socket permission issues (Martin F Krafft, Adrian von Bidder, Leos Bitto, Debian bug #376910) - fix regexps for matching hosts in whitelists (Antonello Nocchi) - do maintenance after the current request and not before (Clifton Royston) * 2007-08-02: version 1.30 - implemented --listen-queue-size option (you usually don't need to set it except for big sites). - small fix to the 'native' syslogging implemented in 1.29 * 2007-07-23: version 1.29 - workaround for a possible crash with Sys::Syslog < 0.15 when syslog messages were generated during syslog file rotation - use the 'native' logging socket with Sys::Syslog >= 0.15 (Philipp Koller) * 2007-06-21: version 1.28 - improved logging (postgrey is now more verbose, use the new --quiet option if that bothers you) - improved Exim support (see Debian bug #380257) - workaround for a bug in Exchange dealing with temporary failures (use DSN code 4.2.0 instead of the default 4.7.1) - added 'policy-test' script for testing postgrey's performance or pre-seeding the database - fix logging of delayed, unresolveable clients in syslog (Andreas Metzler) - more descriptive error message on DB failure (Adrian von Bidder) - updated whitelist - new homepage: http://postgrey.schweikert.ch/ * 2006-07-17: version 1.27 - fix socket permissions with Net::Server >= 0.94 (Leos Bitto) * 2006-07-12: version 1.26 - added support for Exim (Guy Antony Halse) - greatly improve cleanup speed with explicit transactions (Maeda Atusi) * 2006-06-29: version 1.25 - updated whitelist - bugfix: --privacy was not working - change default greylist-text not to include the number of seconds left, since it seems that spammers are misusing it. - added --hostname option (Maarten de Vries) * 2006-01-16: version 1.24 - updated whitelist with many new entries (Vito Robar) - added --privacy option (Micah Anderson) - don't use DB_TXN_NOSYNC to workaround an apparent bug in Berkeley-DB (Nick Moffitt) - fix use of unitialized value (Bjoern A. Zeeb) - Use 'permit_mynetworks' in the documentation (Keith Lofstrom) * 2005-11-23: version 1.23 - fix compatibility with Berkeley-DB < 4.1 - fix crash in the database-cleanup routine (Vito Robar) - updated whitelist - don't be pedantic about wrong reverse-dns entries: it doesn't really help and it could affect legitimate mail servers (Andreas Hoedle) * 2005-11-11: version 1.22 - compatibility for log() of Net::Server 0.88 - don't enter triplets for auto-whitelisted clients (reduce size of main db) - updated whitelist - replace log() with mylog() so that we don't need to worry about format-string problems again - add support for IPv6 in whitelists (Adrian Knoth) - the --dbdir option was not working because of taint-mode (Sven Mueller) - allow comments on the same line (Nigel Gorry) * 2005-04-14: version 1.21 Security: this release fixes a remotely exploitable DoS vulnerability (CVE-2005-1127) - fix crash with '%' in sender addresses (Stefan Schmidt) - fix other users of unchecked strings with syslog/printf (Peter Bieringer) - run in tainted mode -T (Peter Bieringer) (version 1.19 and 1.20 were released on the same day with the above fixes) * 2005-03-07: version 1.18 - correctly set the locale to C so that no internationalized 8bit-text is put in the headers (Bernhard Weisshuhn) - syslog delays (when a X-Greylist header is added) - bugfix: sometimes the retry-window was not properly enforced - bugfix: argument parsing in postgreyreport (MJH) - include the recipient-domain in the default --greylist-text so that the concerned mail-server can be determined and a contact-address can be given on the help page - better documentation for --greylist-text - just warn if DB_AUTO_COMMIT is not available because an older berkeley-db is used (Adrian 'Dagurashibanipal' von Bidder) * 2004-12-14: version 1.17 - added --auto-whitelist-clients option (Andreas Hoedle) it is turned on by default, to disable it specify --auto-whitelist-clients=0 - removed "transition code" of version 1.14 - command-line as process name instead of just 'perl' (Pierre Thierry) - add option --group or set group to 'nogroup' (Adrian 'Dagurashibanipal' von Bidder) * 2004-09-08: version 1.16 - increase max-time to 35, so that monthly newsletter are not greylisted (Stephen Gildea) - set LC_ALL=C so that no localized text is output at the SMTP level (Michal Trojnara) - add a "help" URL to the Greylisted for ... message (Steve Traugott) - updated whitelist * 2004-08-18: version 1.15 - fixed --retry-window with hours (Tom Baker) - update postgreyreport to version 1.14.2 (Tom Baker) - update whitelist * 2004-07-12: version 1.14 - lookup-by-subnet is default. "transition code" has been implemented, so that also a lookup with the unstripped IP will be tried. That way you shouldn't loose any data from the database when moving to version 1.14. Use --lookup-by-host for the old behaviour. All "address pool" entries are removed from the whitelist... - loosen "IP in reverse name" test (Matt Egan) - changed default --max-age to 30, since the db should shrink a lot because of the --retry-window functionality (you might want to db_dump/db_load your db to make it really shrink on disk) - another Perl 5.8.0 workaround - implemented --greylist-text (Tom Baker) - replaced X-Postgrey header with a X-Greylist header which is more similar to what greylist-milter does (Stephen Gildea) - implemented --greylist-action (Stephen Gildea) - updated postgreyreport (Tom Baker) - update whitelist * 2004-07-05: version 1.13 - strip sender address extension (everything after '+'), so that more VERP addresses are substituted to a unique string. (Stephane Bortzmeyer, Santiago Vila) - update whitelist - allow more than one --whitelist-xy option (Xavier Le Vourch) - implemented --retry-window (Tom Baker) - reload whitelists on HUP - add postmaster and abuse to recipients whitelist - add X-Postgrey header to delayed mails * 2004-07-02: version 1.12 - support IPs in the postgrey_whitelist_clients file (Tom Baker) - new version of postgreyreport (Tom Baker) - update whitelist - workaround Perl 5.8.0 bug (Matt Egan) * 2004-06-29: version 1.11 - support for regular expressions in whitelists - update whitelist (Duncan Hill) * 2004-06-26: version 1.10 - fix syntax error in code for --lookup-by-subnet (Matt Egan) * 2004-06-25: version 1.9 - case-insensitive whitelists - fix permissions of --unix socket (Arnaud Launay) * 2004-06-24: version 1.8 - postfix-style syntax for whitelists - add .0 on --lookup-by-subnet (Tom Baker) * 2004-06-24: version 1.7 - bugfix: wrong syslog call ('warn' instead of 'warning') - update whitelist - allow input with cr-lf lines for testing (Tom Baker) - implemented --lookup-by-subnet (Tom Baker) - use 'unix' syslog socket on non-solaris platforms (Matt Egan) - implemented whitelist support from postgrey itself (instead of using postfix restrictions: easier to setup) - included contrib/postgrey-report by Tom Baker * 2004-06-11: version 1.6 - rewritten verp substitution code - when doing keys maintenance: report about how many keys there where before and after. - update whitelist * 2004-06-03: version 1.5 - default delay is now 300 seconds instead of 600 (received reports of mailers that try again the first time after 8 minutes and it doesn't seem to matter right now for filtering spam: they never come again) - report in the log the real time that is left, after which the triplet will be accepted - traverse the database for cleaning old entries only during the night and only once per day - more efficient traversal of database for cleaning (Matthew Reimer) - fix syslogging of warnings and fatal errors * 2004-05-24: version 1.4 - log removal was not working (need to call txn_checkpoint first) * 2004-05-22: version 1.3 - PID file generation with --pidfile (Ralf Engelschall) - fix opening of unix socket (Ralf Engelschall) * 2004-05-21: version 1.2 - create files readable only by the user (umask 077) - logging bugfixes - compatibility with older Berkeley DBs (3.x should work) * 2004-05-21: version 1.1 - use log_archive method every hour instead of DB_LOG_AUTOREMOVE - cosmetic fixes (Ralf Hildebrandt) * 2004-05-20: version 1.0 - initial public release Note: The names in braces do usually give credit where the idea comes from and not who did implement it (which is mostly me) postgrey-1.36/postgrey_whitelist_recipients0000644000175000017500000000027412571400066017617 0ustar dwsdws# postgrey whitelist for mail recipients # -------------------------------------- # put this file in /etc/postfix or specify its path # with --whitelist-recipients=xxx postmaster@ abuse@ postgrey-1.36/postgrey0000755000175000017500000011463212571400056013304 0ustar dwsdws#!/usr/bin/perl -T -w # Postgrey: a Postfix Greylisting Policy Server # Copyright (c) 2004-2007 ETH Zurich # Copyright (c) 2007 Open Systems AG, Switzerland # released under the GNU General Public License # see the documentation with 'perldoc postgrey' package postgrey; use strict; use Pod::Usage; use Getopt::Long 2.25 qw(:config posix_default no_ignore_case); use NetAddr::IP; use Net::Server; # used only to find out which version we use use Net::Server::Multiplex; use BerkeleyDB; use Fcntl ':flock'; # import LOCK_* constants use Sys::Hostname; use Sys::Syslog; # used only to find out which version we use use POSIX qw(strftime setlocale LC_ALL); use vars qw(@ISA); @ISA = qw(Net::Server::Multiplex); my $VERSION = '1.36'; my $DEFAULT_DBDIR = '/var/spool/postfix/postgrey'; my $CONFIG_DIR = '/etc/postfix'; sub cidr_parse($) { defined $_[0] or return undef; $_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/ or return undef; $1 < 256 and $2 < 256 and $3 < 256 and $4 < 256 and $5 <= 32 and $5 > 0 or return undef; my $net = ($1<<24)+($2<<16)+($3<<8)+$4; my $mask = ~((1<<(32-$5))-1); return ($net & $mask, $mask); } sub cidr_match($$$) { my ($net, $mask, $addr) = @_; return undef unless defined $net and defined $mask and defined $addr; return undef if ($addr =~ /:.*:/); # ignore IPv6 addresses if($addr =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { $addr = ($1<<24)+($2<<16)+($3<<8)+$4; } return ($addr & $mask) == $net; } sub read_clients_whitelists($) { my ($self) = @_; my @whitelist_clients = (); my @whitelist_ips = (); my @whitelist_cidr = (); for my $f (@{$self->{postgrey}{whitelist_clients_files}}) { if(open(CLIENTS, $f)) { while() { s/#.*$//; s/^\s+//; s/\s+$//; next if $_ eq ''; if(/^\/(\S+)\/$/) { # regular expression push @whitelist_clients, qr{$1}i; } elsif(/^\d{1,3}(?:\.\d{1,3}){0,3}$/) { # IP address or part of it push @whitelist_ips, qr{^\Q$_\E\b}; } elsif(/^.*\:.*\:.*$/) { # IPv6? push @whitelist_ips, qr{^\Q$_\E\b}; } elsif(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/) { my ($net, $mask) = cidr_parse($_); if(defined $mask) { push @whitelist_cidr, [ $net, $mask ]; } else { warn "$f line $.: invalid cidr address\n"; } } # note: we had ^[^\s\/]+$ but it triggers a bug in perl 5.8.0 elsif(/^\S+$/) { push @whitelist_clients, qr{(?:^|\.)\Q$_\E$}i; } else { warn "$f line $.: doesn't look like a hostname\n"; } } } else { # do not warn about .local file: maybe the user just doesn't have one warn "can't open $f: $!\n" unless $f =~ /\.local$/; } close(CLIENTS); } $self->{postgrey}{whitelist_clients} = \@whitelist_clients; $self->{postgrey}{whitelist_ips} = \@whitelist_ips; $self->{postgrey}{whitelist_cidr} = \@whitelist_cidr; } sub read_recipients_whitelists($) { my ($self) = @_; my @whitelist_recipients = (); for my $f (@{$self->{postgrey}{whitelist_recipients_files}}) { if(open(RECIPIENTS, $f)) { while() { s/#.*$//; s/^\s+//; s/\s+$//; next if $_ eq ''; my ($user, $domain) = split(/\@/, $_, 2); if(/^\/(\S+)\/$/) { # regular expression push @whitelist_recipients, qr{$1}i; } elsif(!/^\S+$/) { warn "$f line $.: doesn't look like an address\n"; } # postfix access(5) syntax: elsif(defined $domain and $domain ne '') { # user@domain (match also user+extension@domain) push @whitelist_recipients, qr{^\Q$user\E(?:\+[^@]+)?\@\Q$domain\E$}i; } elsif(defined $domain) { # user@ push @whitelist_recipients, qr{^\Q$user\E(?:\+[^@]+)?\@}i; } else { # domain ($user is the domain) push @whitelist_recipients, qr{(?:\@|\.)\Q$user\E$}i; } } } else { # do not warn about .local file: maybe the user just doesn't have one warn "can't open $f: $!\n" unless $f =~ /\.local$/; } close(RECIPIENTS); } $self->{postgrey}{whitelist_recipients} = \@whitelist_recipients; } sub do_sender_substitutions($$) { my ($self, $addr) = @_; my ($user, $domain) = split(/@/, $addr, 2); defined $domain or return $addr; # BATV is defined as prvs=tag-val=loc-core@, but sometimes shows up # as prvs=loc-core=tag-val@... if ($user =~ /^prvs=/) { my @a = split(/=/, $user); if ($#a == 2) { if ($a[1] =~ /^[0-9a-z]{10}/ && $a[2] !~ /^[0-9a-z]{10}/) { $user = $a[2]; } elsif ($a[2] =~ /^[0-9a-z]{10}/ && $a[1] !~ /^[0-9a-z]{10}/) { $user = $a[1]; } else { # throw a coin, pick the standard $user = $a[2]; } } } # strip extension, used sometimes for mailing-list VERP $user =~ s/\+.*//; # replace numbers in VERP addresses with '#' so that # we don't create a new key for each mail $user =~ s/\b\d+\b/#/g; return "$user\@$domain"; } # split network and host part and return both sub do_client_substitutions($$$) { my ($self, $ip, $revdns) = @_; if($self->{postgrey}{lookup_by_host}) { return ($ip, undef); } my $ipaddr; if($ip =~ /\./) { # IPv4 my @ip=split(/\./, $ip); return ($ip, undef) unless defined $ip[3]; # skip if it contains the last two IP numbers in the hostname # (we assume it is a pool of dialup addresses of a provider) return ($ip, undef) if $revdns =~ /$ip[2]/ and $revdns =~ /$ip[3]/; $ipaddr = NetAddr::IP->new($ip, $self->{postgrey}{ipv4cidr}); } elsif($ip =~ /:/) { # IPv6 $ipaddr = NetAddr::IP->new($ip, $self->{postgrey}{ipv6cidr}); } else { warn "doesn't look like an IP address: $ipaddr\n"; return ($ip, undef); } return ($ipaddr->network, undef); } sub mylog($$$) { my ($self, $level, $string) = @_; $string =~ s/\%/%%/g; # for Net::Server <= 0.87 if(!defined $Sys::Syslog::VERSION or $Sys::Syslog::VERSION lt '0.15' or !defined $Net::Server::VERSION or $Net::Server::VERSION lt '0.94') { # Workaround for a crash when syslog daemon is temporarily not # present (for example on syslog rotation). # Note that this is not necessary with Sys::Syslog >= 0.15 and # Net::Server >= 0.94 thanks to the nofatal Option. eval { local $SIG{"__DIE__"} = sub { }; $self->log($level, $string); }; } else { $self->log($level, $string); } } sub mylog_action($$$;$$) { my ($self, $attr, $action, $reason, $additional_info) = @_; my $str; $str .= $attr->{queue_id} . ': ' if $attr->{queue_id}; my @info = ("action=$action"); push @info, "reason=$reason" if defined $reason; push @info, $additional_info if defined $additional_info; for my $a (qw(client_name client_address sender recipient)) { push @info, "$a=$attr->{$a}" if $attr->{$a}; } $str .= join(', ', @info); $self->mylog(2, $str); } sub do_maintenance($$) { my ($self, $now) = @_; my $db = $self->{postgrey}{db}; my $db_env = $self->{postgrey}{db_env}; # remove old db logs $self->mylog(1, "cleaning up old logs..."); $db_env->txn_checkpoint(0, 0) and warn "can't checkpoint !? $BerkeleyDB::Error"; for my $l ($db_env->log_archive(DB_ARCH_ABS)) { $self->mylog(1, "rm $l"); unlink $l or warn "can't remove db log $l: $!\n"; } # remove old keys # this is very expensive: We might refuse to speak to postfix for too # long, after which clients will start getting "450 Server configuration # problem" errors... do it only during the night and only if at least one # day has passed my $hour = (localtime($now))[2]; if($hour > 1 and $hour < 7 and $now - $self->{postgrey}{last_maint_keys} >= 82800) { $self->mylog(1, "cleaning up old entries..."); my $max_age = $self->{postgrey}{max_age}; my $retry_window = $self->{postgrey}{retry_window}; my ($nr_keys_before, $nr_keys_after) = (0,0); # note: deleteing hash elements in a 'each'-loop isn't supported # that's why we first put them in @old_keys and then delete them my @old_keys = (); while (my ($key, $value) = each %$db) { $nr_keys_before++; my ($first, $last) = split(/,/,$value); if(not defined $last) { # shouldn't happen push @old_keys, $key; } elsif($now - $last > $max_age) { # last-seen passed max-age push @old_keys, $key; } elsif($last-$first < $self->{postgrey}{delay} and $now-$last > $retry_window) { # no successful entry yet and last seen passed retry-window push @old_keys, $key; } else { $nr_keys_after++; } } my $db_obj = $self->{postgrey}{db_obj}; my $txn = $db_env->txn_begin(); $db_obj->Txn($txn); for my $key (@old_keys) { delete $db->{$key}; } $txn->txn_commit(); $self->mylog(1, "cleaning main database finished. before: $nr_keys_before, after: $nr_keys_after"); if($self->{postgrey}{awl_clients}) { # cleanup clients auto-whitelist database my $cawl_db = $self->{postgrey}{db_cawl}; ($nr_keys_before, $nr_keys_after) = (0, 0); $nr_keys_after=0; my @old_keys_cawl = (); while (my ($key, $value) = each %$cawl_db) { my $cawl_last_seen = (split(/,/, $value))[1]; $nr_keys_before++; if($now - $cawl_last_seen > $max_age) { push @old_keys_cawl, $key; } else { $nr_keys_after++; } } my $db_cawl_obj = $self->{postgrey}{db_cawl_obj}; $txn = $db_env->txn_begin(); $db_cawl_obj->Txn($txn); for my $key (@old_keys_cawl) { delete $cawl_db->{$key}; } $txn->txn_commit(); $self->mylog(1, "cleaning clients database finished. before: $nr_keys_before, after: $nr_keys_after"); } $self->{postgrey}{last_maint_keys}=$now; } } sub is_new_instance($$) { my ($self, $inst) = @_; return 1 if not defined $inst; # in case the 'instance' parameter # was not supplied by the client (Exim) # we keep a list of the last 20 "instances", which identify unique messages # so that for example we only put one X-Greylist header per message. $self->{postgrey}{instances} = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] unless defined $self->{postgrey}{instances}; my $i = $self->{postgrey}{instances}; return 0 if scalar grep { $_ eq $inst } @$i; # put new value into the array unshift @$i, $inst; pop @$i; return 1; } # main routine: based on attributes specified as argument, return policy decision sub smtpd_access_policy($$) { my ($self, $now, $attr) = @_; my $db = $self->{postgrey}{db}; # This attribute occurs in connections from the policy-test script, # not in a regular postfix query connection if(defined $attr->{policy_test_time}) { $now = $attr->{policy_test_time} } # whitelists for my $w (@{$self->{postgrey}{whitelist_clients}}) { if($attr->{client_name} =~ $w) { $self->mylog_action($attr, 'pass', 'client whitelist'); return 'DUNNO'; } } for my $w (@{$self->{postgrey}{whitelist_ips}}) { if($attr->{client_address} =~ $w) { $self->mylog_action($attr, 'pass', 'client whitelist'); return 'DUNNO'; } } for my $w (@{$self->{postgrey}{whitelist_cidr}}) { if(cidr_match($w->[0], $w->[1], $attr->{client_address})) { $self->mylog_action($attr, 'pass', 'client whitelist'); return 'DUNNO'; } } for my $w (@{$self->{postgrey}{whitelist_recipients}}) { if($attr->{recipient} =~ $w) { $self->mylog_action($attr, 'pass', 'recipient whitelist'); return 'DUNNO'; } } # auto whitelist clients (see below for explanation) my ($cawl_db, $cawl_key, $cawl_count, $cawl_last); if($self->{postgrey}{awl_clients}) { $cawl_db = $self->{postgrey}{db_cawl}; $cawl_key = $attr->{client_address}; if ($self->{postgrey}{privacy}) { $cawl_key = Digest::SHA::sha1_hex($cawl_key); } my $cawl_val = $cawl_db->{$cawl_key}; ($cawl_count, $cawl_last) = split(/,/,$cawl_val) if defined $cawl_val; # whitelist if count is enough if(defined $cawl_count and $cawl_count >= $self->{postgrey}{awl_clients}) { if(($now >= $cawl_last+3600) or ($cawl_last > $now)) { $cawl_count++; # for statistics $cawl_db->{$cawl_key}=$cawl_count.','.$now; } $self->mylog_action($attr, 'pass', 'client AWL'); return 'DUNNO'; } } # lookup my $sender = $self->do_sender_substitutions($attr->{sender}); my ($client_net, $client_host) = $self->do_client_substitutions($attr->{client_address}, $attr->{client_name}); my $key = lc "$client_net/$sender/$attr->{recipient}"; if ($self->{postgrey}{privacy}) { $key = Digest::SHA::sha1_hex($key); } my $val = $db->{$key}; my $first; my $last_was_successful=0; if(defined $val) { my $last; ($first, $last) = split(/,/,$val); # find out if the last time was unsuccessful, so that we can add a header # to say how much had to be waited if($last - $first >= $self->{postgrey}{delay}) { $last_was_successful=1; } else { # discard stored first-seen if it is the first retrial and # it is beyond the retry_window $first = $now if $now-$first > $self->{postgrey}{retry_window}; } # test for invalid first-seen date in the future if($first > $now) { $self->mylog(1, "correcting date for first seen in the future!"); $first = $now; } } else { $first = $now; } # update (put as last element stripped host-part if it was stripped) if(defined $client_host) { $db->{$key}="$first,$now,$client_host"; } else { $db->{$key}="$first,$now"; } my $diff = $self->{postgrey}{delay} - ($now - $first); # auto whitelist clients # algorithm: # - on successful entry in the greylist db of a triplet: # - client not whitelisted yet? -> increase count, whitelist if count > 10 or so # - client whitelisted already? -> update last-seen timestamp if($self->{postgrey}{awl_clients}) { # greylisting succeeded if($diff <= 0 and !$last_was_successful) { # enough time has passed (record only one attempt per hour) if(! defined $cawl_last or $now >= $cawl_last + 3600) { # ok, increase count $cawl_count++; $cawl_db->{$cawl_key}=$cawl_count.','.$now; my $client = $attr->{client_name} ? $attr->{client_name}.'['.$attr->{client_address}.']' : $attr->{client_address}; $self->mylog(1, "whitelisted: $client") if $cawl_count==$self->{postgrey}{awl_clients}; } } } # not enough waited? -> greylist if ($diff > 0 ) { my $msg = $self->{postgrey}{greylist_text}; # Workaround for an Exchange bug related to Greylisting: # use DSN 4.2.0 instead of the default 4.7.1. This works # only with Postfix 2.3 though and we know that Postfix >= 2.3 # always defines the etrn_domain attribute (is this really true and # guaranteed in future versions? I don't know...) $msg = '4.2.0 ' . $msg if defined $attr->{etrn_domain} and $msg !~ /^\d/; $msg =~ s/\%s/$diff/; my $recip_domain = $attr->{recipient}; $recip_domain =~ s/.*\@//; $msg =~ s/\%r/$recip_domain/; $self->mylog_action($attr, 'greylist', $now != $first ? "early-retry (${diff}s missing)" : "new" ); return "$self->{postgrey}{greylist_action} $msg"; } # X-Greylist header: if(!$last_was_successful and $self->is_new_instance($attr->{instance})) { # syslog my $client = ($attr->{client_name} and $attr->{client_name} ne 'unknown') ? $attr->{client_name} : $attr->{client_address}; # add X-Greylist header my $date = strftime("%a, %d %b %Y %T %Z", localtime); my $delay = $now-$first; $self->mylog_action($attr, 'pass', 'triplet found', 'delay='.($delay)); my $msg = $self->{postgrey}{x_greylist_header}; $msg =~ s/\%t/$delay/; $msg =~ s/\%v/$VERSION/; $msg =~ s/\%d/$date/; $msg =~ s/\%h/$self->{postgrey}{hostname}/; return 'PREPEND ' . $msg; } $self->mylog_action($attr, 'pass', 'triplet found'); return 'DUNNO'; } sub main() { # save arguments for Net:Server HUP restart my @ARGV_saved = @ARGV; # do not output any localized texts! setlocale(LC_ALL, 'C'); # parse options my %opt = (); GetOptions(\%opt, 'help|h', 'man', 'version', 'noaction|no-action|n', 'verbose|v', 'quiet|q', 'daemonize|d', 'unix|u=s', 'inet|i=s', 'socketmode=s', 'user=s', 'group=s', 'dbdir=s', 'pidfile=s', 'delay=i', 'max-age=i', 'lookup-by-subnet', 'lookup-by-host', 'ipv4cidr=i', 'ipv6cidr=i', 'auto-whitelist-clients:s', 'whitelist-clients=s@', 'whitelist-recipients=s@', 'syslogfacility|syslog-facility|facility=s', 'retry-window=s', 'greylist-action=s', 'greylist-text=s', 'privacy', 'hostname=s', 'exim', 'listen-queue-size=i', 'x-greylist-header=s', ) or exit(1); # note: lookup-by-subnet can be given for compatibility, but it is default # so do not do nothing with it... # note: auto-whitelist-clients:s and not auto-whitelist-clients:n so that # we can differentiate between --auto-whitelist-clients=0 and # auto-whitelist-clients if($opt{help}) { pod2usage(1) } if($opt{man}) { pod2usage(-exitstatus => 0, -verbose => 2) } if($opt{version}) { print "postgrey $VERSION\n"; exit(0) } if($opt{noaction}) { die "ERROR: don't know how to \"no-action\".\n" } defined $opt{unix} or defined $opt{inet} or die "ERROR: --unix or --inet must be specified\n"; # bind only localhost if no host is specified if(defined $opt{inet} and $opt{inet}=~/^\d+$/) { $opt{inet} = "localhost:$opt{inet}"; } # retry window my $retry_window = 24*3600*2; # default: 2 days if(defined $opt{'retry-window'}) { if($opt{'retry-window'} =~ /^(\d+)h$/i) { $retry_window = $1 * 3600; } elsif($opt{'retry-window'} =~ /^\d+$/) { $retry_window = $opt{'retry-window'} * 24 * 3600; } else { die "ERROR: --retry-window must be either a number of days or a number\n", " followed by 'h' for hours ('6h' for example).\n"; } } # untaint what is given on --dbdir. It is not security sensitive since # it is provided by the admin if($opt{dbdir}) { $opt{dbdir} =~ /^(.*)$/; $opt{dbdir} = $1; } # untaint what is given on --pidfile. It is not security sensitive since # it is provided by the admin if($opt{pidfile}) { $opt{pidfile} =~ /^(.*)$/; $opt{pidfile} = $1; } # untaint what is given on --inet. It is not security sensitive since # it is provided by the admin if($opt{inet}) { $opt{inet} =~ /^(.*)$/; $opt{inet} = $1; } # untaint what is given on --socketmode. It is not security sensitive since # it is provided by the admin if($opt{socketmode}) { $opt{socketmode} =~ /^(.*)$/; $opt{socketmode} = $1; } # determine proper "logsock" for Sys::Syslog my $syslog_logsock; if(defined $Sys::Syslog::VERSION and $Sys::Syslog::VERSION ge '0.15' and defined $Net::Server::VERSION and $Net::Server::VERSION ge '0.97') { # use 'native' when Sys::Syslog >= 0.15 $syslog_logsock = 'native'; } elsif($^O eq 'solaris') { # 'stream' is broken and 'unix' doesn't work on Solaris: only 'inet' # seems to be useable with Sys::Syslog < 0.15 $syslog_logsock = 'inet'; } else { $syslog_logsock = 'unix'; } # Workaround: Net::Server doesn't allow for a value of 'listen' higher than 999 if(defined $opt{'listen-queue-size'} and $opt{'listen-queue-size'} > 999) { $opt{'listen-queue-size'} = 999; } # create Net::Server object and run it my $server = bless { server => { commandline => [ 'postgrey', @ARGV_saved ], port => [ $opt{inet} ? $opt{inet} : $opt{unix}."|unix" ], proto => $opt{inet} ? 'tcp' : 'unix', socketmode => $opt{socketmode} || '0666', user => $opt{user} || 'postgrey', group => $opt{group} || 'nogroup', dbdir => $opt{dbdir} || $DEFAULT_DBDIR, setsid => $opt{daemonize} ? 1 : undef, pid_file => $opt{daemonize} ? $opt{pidfile} : undef, log_level => $opt{quiet} ? 1 : ($opt{verbose} ? 3 : 2), log_file => $opt{daemonize} ? 'Sys::Syslog' : undef, syslog_logsock => $syslog_logsock, syslog_facility => $opt{syslogfacility} || 'mail', syslog_ident => 'postgrey', listen => $opt{'listen-queue-size'} ? $opt{'listen-queue-size'} : undef, }, postgrey => { delay => $opt{delay} || 300, max_age => $opt{'max-age'} || 35, last_maint => time, last_maint_keys => 0, # do it on the first night lookup_by_host => $opt{'lookup-by-host'}, ipv4cidr => $opt{'ipv4cidr'} || 24, ipv6cidr => $opt{'ipv6cidr'} || 64, awl_clients => defined $opt{'auto-whitelist-clients'} ? ($opt{'auto-whitelist-clients'} ne '' ? $opt{'auto-whitelist-clients'} : 5) : 5, retry_window => $retry_window, greylist_action => $opt{'greylist-action'} || 'DEFER_IF_PERMIT', greylist_text => $opt{'greylist-text'} || 'Greylisted, see http://postgrey.schweikert.ch/help/%r.html', whitelist_clients_files => $opt{'whitelist-clients'} || [ "$CONFIG_DIR/postgrey_whitelist_clients" , "$CONFIG_DIR/postgrey_whitelist_clients.local" ], whitelist_recipients_files => $opt{'whitelist-recipients'} || [ "$CONFIG_DIR/postgrey_whitelist_recipients" ], privacy => defined $opt{'privacy'}, hostname => defined $opt{hostname} ? $opt{hostname} : hostname, exim => defined $opt{'exim'}, x_greylist_header => $opt{'x-greylist-header'} || 'X-Greylist: delayed %t seconds by postgrey-%v at %h; %d', }, }, 'postgrey'; # max_age is in days $server->{postgrey}{max_age}*=3600*24; # read whitelist $server->read_clients_whitelists(); $server->read_recipients_whitelists(); # --privacy requires Digest::SHA if($opt{'privacy'}) { require Digest::SHA; } $0 = join(' ', @{$server->{server}{commandline}}); $server->run; # shouldn't get here $server->mylog(1, "Exiting!"); exit 1; } ##### Net::Server::Multiplex methods: # reload whitelists on HUP sub sig_hup { my $self = shift; $self->mylog(1, "HUP received: reloading whitelists..."); $self->read_clients_whitelists(); $self->read_recipients_whitelists(); } sub post_bind_hook() { my ($self) = @_; # set unix socket permissions if($self->{server}{port}[0] =~ /^(.*)\|unix$/) { chmod oct($self->{server}{socketmode}), $1; } } sub pre_loop_hook() { my ($self) = @_; # be sure to put in syslog any warnings / fatal errors if($self->{server}{log_file} eq 'Sys::Syslog') { $SIG{__WARN__} = sub { Sys::Syslog::syslog('warning', '%s', "WARNING: $_[0]") }; $SIG{__DIE__} = sub { Sys::Syslog::syslog('crit', '%s', "FATAL: $_[0]"); die @_; }; } # write files with mode 600 umask 0077; # ensure that only one instance of postgrey is running my $lock = "$self->{server}{dbdir}/postgrey.lock"; open(LOCK, ">>$lock") or die "ERROR: can't open lock file: $lock\n"; flock(LOCK, LOCK_EX|LOCK_NB) or die "ERROR: locked: $lock\n"; # my $setflags = DB_TXN_NOSYNC; my $setflags = 0; # see http://bugs.debian.org/334430 if($BerkeleyDB::db_version >= 4.1) { $setflags |= BerkeleyDB::DB_AUTO_COMMIT; } else { warn "disabling DB_AUTO_COMMIT because you are using BerkeleyDB version $BerkeleyDB::db_version. Version 4.1 is required for DB_AUTO_COMMIT. You might have problems in case of system failures to recover the database.\n"; } # open database with transactions, logging and auto-commit to make it as # safe as possible. Note that locking is not required since only one # process is running $self->{postgrey}{db_env} = BerkeleyDB::Env->new( -Home => $self->{server}{dbdir}, -Flags => DB_CREATE|DB_RECOVER|DB_INIT_TXN|DB_INIT_MPOOL|DB_INIT_LOG, -SetFlags => $setflags, ) or die "ERROR: can't create DB environment: $! (" . "dbdir: ".$self->{server}{dbdir}." uid/gid: $<,$>)\n"; $self->{postgrey}{db_obj} = tie(%{$self->{postgrey}{db}}, 'BerkeleyDB::Btree', -Filename => 'postgrey.db', -Flags => DB_CREATE, -Env => $self->{postgrey}{db_env} ) or die "ERROR: can't create database $self->{server}{dbdir}/postgrey.db: $!\n"; if($self->{postgrey}{awl_clients}) { $self->{postgrey}{db_cawl_obj} = tie(%{$self->{postgrey}{db_cawl}}, 'BerkeleyDB::Btree', -Filename => 'postgrey_clients.db', -Flags => DB_CREATE, -Env => $self->{postgrey}{db_env} ) or die "ERROR: can't create database $self->{server}{dbdir}/postgrey_clients.db: $!\n"; } } sub mux_input() { my ($self, $mux, $fh, $in_ref) = @_; defined $self->{postgrey_attr} or $self->{postgrey_attr} = {}; my $attr = $self->{postgrey_attr}; # consume entire lines while ($$in_ref =~ s/^([^\r\n]*)\r?\n//) { next unless defined $1; my $in = $1; if($in =~ /([^=]+)=(.*)/) { # read attributes $attr->{substr($1, 0, 512)} = substr($2, 0, 512); } elsif($in eq '') { defined $attr->{request} or $attr->{request}=''; if($attr->{request} ne 'smtpd_access_policy') { $self->{net_server}->mylog(0, "unrecognized request type: '$attr->{request}'"); } else { my $now = time; # decide my $action = $self->{net_server}->smtpd_access_policy($now, $attr); # give answer print $fh "action=$action\n\n"; # attempt maintenance if one hour has passed since the last one my $server = $self->{net_server}; if($server->{postgrey}{last_maint} && $now-$server->{postgrey}{last_maint} >= 3600) { $server->{postgrey}{last_maint} = $now; $server->do_maintenance($now); } # close the filehandle if --exim is set if ($self->{net_server}->{postgrey}{exim}) { close($fh); last; } } $self->{postgrey_attr} = {}; } else { $self->{net_server}->mylog(1, "ignoring garbage: <".substr($in, 0, 100).">"); } } } sub fatal_hook() { my ($self, $error, $package, $file, $line) = @_; # Net::Server calls $self->server_close but, unfortunately, # it does exit(0) (with Net::Server 0.97)... # It is however useful for init-script to detect non-zero exit codes die('ERROR: ' . $error); } main; __END__ =head1 NAME postgrey - Postfix Greylisting Policy Server =head1 SYNOPSIS B [I...] -h, --help display this help and exit --version output version information and exit -v, --verbose increase verbosity level --syslog-facility Syslog facility to use (default mail) -q, --quiet decrease verbosity level -u, --unix=PATH listen on unix socket PATH --socketmode=MODE unix socket permission (default 0666) -i, --inet=[HOST:]PORT listen on PORT, localhost if HOST is not specified -d, --daemonize run in the background --pidfile=PATH put daemon pid into this file --user=USER run as USER (default: postgrey) --group=GROUP run as group GROUP (default: nogroup) --dbdir=PATH put db files in PATH (default: /var/spool/postfix/postgrey) --delay=N greylist for N seconds (default: 300) --max-age=N delete entries older than N days since the last time that they have been seen (default: 35) --retry-window=N allow only N days for the first retrial (default: 2) append 'h' if you want to specify it in hours --greylist-action=A if greylisted, return A to Postfix (default: DEFER_IF_PERMIT) --greylist-text=TXT response when a mail is greylisted (default: Greylisted + help url, see below) --lookup-by-subnet strip the last N bits from IP addresses, determined by ipv4cidr and ipv6cidr (default) --ipv4cidr=N What cidr to use for the subnet on IPv4 addresses when using lookup-by-subnet (default: 24) --ipv6cidr=N What cidr to use for the subnet on IPv6 addresses when using lookup-by-subnet (default: 64) --lookup-by-host do not strip the last 8 bits from IP addresses --privacy store data using one-way hash functions --hostname=NAME set the hostname (default: `hostname`) --exim don't reuse a socket for more than one query (exim compatible) --whitelist-clients=FILE default: /etc/postfix/postgrey_whitelist_clients --whitelist-recipients=FILE default: /etc/postfix/postgrey_whitelist_recipients --auto-whitelist-clients=N whitelist host after first successful delivery N is the minimal count of mails before a client is whitelisted (turned on by default with value 5) specify N=0 to disable. --listen-queue-size=N allow for N waiting connections to our socket --x-greylist-header=TXT header when a mail was delayed by greylisting default: X-Greylist: delayed seconds by postgrey- at ; Note that the --whitelist-x options can be specified multiple times, and that per default /etc/postfix/postgrey_whitelist_clients.local is also read, so that you can put there local entries. =head1 DESCRIPTION Postgrey is a Postfix policy server implementing greylisting. When a request for delivery of a mail is received by Postfix via SMTP, the triplet C / C / C is built. If it is the first time that this triplet is seen, or if the triplet was first seen less than I seconds (300 is the default), then the mail gets rejected with a temporary error. Hopefully spammers or viruses will not try again later, as it is however required per RFC. Note that you shouldn't use the --lookup-by-host option unless you know what you are doing: there are a lot of mail servers that use a pool of addresses to send emails, so that they can change IP every time they try again. That's why without this option postgrey will strip the last byte of the IP address when doing lookups in the database. =head2 Installation =over 4 =item * Create a C user and the directory where to put the database I (default: C) =item * Write an init script to start postgrey at boot and start it. Like this for example: postgrey --inet=10023 -d F in the postgrey source distribution includes a LSB-compliant init script by Adrian von Bidder for the Debian system. =item * Put something like this in /etc/main.cf: smtpd_recipient_restrictions = permit_mynetworks ... reject_unauth_destination check_policy_service inet:127.0.0.1:10023 =item * Install the provided postgrey_whitelist_clients and postgrey_whitelist_recipients in /etc/postfix. =item * Put in /etc/postfix/postgrey_whitelist_recipients users that do not want greylisting. =back =head2 Whitelists Whitelists allow you to specify client addresses or recipient address, for which no greylisting should be done. Per default postgrey will read the following files: /etc/postfix/postgrey_whitelist_clients /etc/postfix/postgrey_whitelist_clients.local /etc/postfix/postgrey_whitelist_recipients You can specify alternative paths with the --whitelist-x options. Postgrey whitelists follow similar syntax rules as Postfix access tables. The following can be specified for B: =over 10 =item domain.addr C domain and subdomains. =item name@ C and extended addresses C. =item name@domain.addr C and extended addresses. =item /regexp/ anything that matches C (the full address is matched). =back The following can be specified for B: =over 10 =item domain.addr C domain and subdomains. =item IP1.IP2.IP3.IP4 IP address IP1.IP2.IP3.IP4. You can also leave off one number, in which case only the first specified numbers will be checked. =item IP1.IP2.IP3.IP4/MASK CIDR-syle network. Example: 192.168.1.0/24 =item /regexp/ anything that matches C (the full address is matched). =back =head2 Auto-whitelisting clients With the option --auto-whitelist-clients a client IP address will be automatically whitelisted if the following conditions are met: =over 4 =item * At least 5 successfull attempts of delivering a mail (after greylisting was done). That number can be changed by specifying a number after the --auto-whitelist-clients argument. Only one attempt per hour counts. =item * The client was last seen before --max-age days (35 per default). =back =head2 Greylist Action To set the action to be returned to postfix when a message fails postgrey's tests and should be deferred, use the --greylist-action=ACTION option. By default, postgrey returns DEFER_IF_PERMIT, which causes postfix to check the rest of the restrictions and defer the message only if it would otherwise be accepted. A delay action of 451 causes postfix to always defer the message with an SMTP reply code of 451 (temp fail). See the postfix manual page access(5) for a discussion of the actions allowed. =head2 Greylist Text When a message is greylisted, an error message like this will be sent at the SMTP-level: Greylisted, see http://postgrey.schweikert.ch/help/example.com.html Usually no user should see that error message and the idea of that URL is to provide some help to system administrators seeing that message or users of broken mail clients which try to send mails directly and get a greylisting error. Note that the default help-URL contains the original recipient domain (example.com), so that domain-specific help can be presented to the user (on the default page it is said to contact postmaster@example.com) You can change the text (and URL) with the B<--greylist-text> parameter. The following special variables will be replaced in the text: =over 4 =item %s How many seconds left until the greylisting is over (300). =item %r Mail-domain of the recipient (example.com). =back =head2 Greylist Header When a message is greylisted, an additional header can be prepended to the header section of the mail: X-Greylist: delayed %t seconds by postgrey-%v at %h; %d You can change the text with the B<--x-greylist-header> parameter. The following special variables will be replaced in the text: =over 4 =item %t How many seconds the mail has been delayed due to greylisting. =item %v The version of postgrey. =item %d The date. =item %h The host. =back =head2 Privacy The --privacy option enable the use of a SHA1 hash function to store IPs and emails in the greylisting database. This will defeat straight forward attempts to retrieve mail user behaviours. =head2 SEE ALSO See L for a description of what greylisting is and L for a description of how Postfix policy servers work. =head1 COPYRIGHT Copyright (c) 2004-2007 by ETH Zurich. All rights reserved. Copyright (c) 2007 by Open Systems AG. All rights reserved. =head1 LICENSE 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., 675 Mass Ave, Cambridge, MA 02139, USA. =head1 AUTHOR Sdavid@schweikert.chE> =cut # vi: sw=4 et postgrey-1.36/contrib/0000755000175000017500000000000012571400066013134 5ustar dwsdwspostgrey-1.36/contrib/postgreyreport0000755000175000017500000005746712252310311016202 0ustar dwsdws#!/usr/bin/perl # postgreyreport by tbaker@bakerfl.org # bits and peices of code taken from postgrey 1.11 ( http://isg.ee.ethz.ch/tools/postgrey/ ) package postgreyreport; use strict; use BerkeleyDB; use Getopt::Long 2.25 qw(:config posix_default no_ignore_case); use Net::Server::Daemonize qw( get_uid get_gid set_uid set_gid ); use Pod::Usage; #use Net::RBLClient; my $VERSION='1.14.3 (20100321)'; # used in maillog processing my $RE_revdns_ip = qr/ ([^\[\s]+)\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]/; # ptr[1.2.3.4] my $RE_reject = qr/reject: /; my $RE_triplet = qr/$RE_revdns_ip: 450 .+from=<([^>]+)> to=<([^>]+)>/; my $dns; my %dns_cache; # used for --check_sender my $rbl = undef; # Net::RBLClient object select((select(STDOUT), $| = 1)[0]); # Unbuffer standard output. # default options, override via command line my %opt = ( user => 'postgrey', dbdir => '/var/spool/postfix/postgrey', delay => 300, return_string => 'Greylisted', # match on this string check_sender => '', # = mx,a,mx/24,a/24 # todo=spf - uses Net::DNS show_tries => 0, # number of greylist attempts within --delay separate_by_subnet => '', # if not blank output this string for every new /24 separate_by_ip => '', # if not blank output this string for every new IP single_line => 1, # output everything on a single line? (grouping enabled if false ) tab => 0, # use tabs as separators, not spaces (only in single line mode) show_time => 0, # show entry time in maillog skip_dnsbl => [], # list of DNSBL servers to check and skip reporting for skip_clients => [], # files of clients to skip reporting skip_pool => 0, # skip entries that appear to be a provider pool (last 2 ips in ptr) match_clients => [], # files of ONLY clients to report on v => 0, # verbose? used mainly for script debugging debug_db => 0, # output time() values from btree db debug_re => '', # but only for these hosts (separate by commas ) ); # start here sub main { GetOptions(\%opt, 'help|h', 'version', 'man', 'delay=s', 'user|u=s', 'dbdir=s', 'debug_db', 'debug_re=s', 'v+', 'return_string|greylist-text=s', 'show_tries', 'check_sender=s', 'separate_by_subnet=s', 'separate_by_ip=s', 'single_line!', 'tab', 'show_time', 'skip_dnsbl=s@','skip_clients=s@', 'match_clients=s@', 'skip_pool', ) or exit(1); if($opt{help}) { pod2usage(1) } if($opt{man}) { pod2usage(-exitstatus => 0, -verbose => 2) } if ($opt{version}) { print "postgreyreport $VERSION\n"; exit(0) } if (scalar(@{$opt{skip_dnsbl}}) > 0) { require Net::RBLClient; $rbl = Net::RBLClient->new ( lists => $opt{skip_dnsbl} ); } setup_debug(); # display key/value pairs from db read_client_files(); postgrey_fatal_report(); # do the work } ####################################################### # postgrey_fatal(): report on all fatal triplets # sub postgrey_fatal_report() { umask 0077; # mode 600 my %triplets; # hash of all triplets we will look at drop_priv($opt{user}); # change UID to 'postgrey' # convert --check_sender into hash: opt{do_checks}{VAL} if ($opt{check_sender}) { use Net::DNS; $dns = Net::DNS::Resolver->new; $opt{check_sender} = lc $opt{check_sender}; foreach my $check ( split(/,/,$opt{check_sender}) ) { $opt{do_checks}{$check}=1; print "Enabling Check: opt{do_checks}{$check} \n" if ($opt{v}); } } my $db = setup_dbm($opt{dbdir}); # connect to BerkeleyDB my @greyfatal = find_and_sort_fatal( \%{$db}, \%triplets ); # read STDIN and sort the fatal triplets # foreach: loop through (sorted) fatal triplets and display to STDOUT my ($last_ip,$last_subnet); # define now $opt{separate_by_ip} =~ s|\\n|\n|g; # do it once before the for loop $opt{separate_by_subnet} =~ s|\\n|\n|g; # "" foreach my $key (@greyfatal) { my ($ip,$sender,$recipient) = split(/\//,$key); # separate the triplet my $revdns = $triplets{$key}{revdns}; # we saved revdns during maillog parse, so we dont have to look it up # --check_sender=mx,mx/24,a,a/24 # dns lookups from Net::DNS are cached and only performed once per sender's @domain my $check_sender = ''; if ( $opt{do_checks}{mx} and check_sender_mx( $sender,$ip,'mx') ) { $check_sender='MX'; } elsif ( $opt{do_checks}{'mx/24'} and check_sender_mx( $sender,$ip,'mx/24') ) { $check_sender='MX/24'; } elsif ( $opt{do_checks}{a} and check_sender_a( $sender,$ip,'a') ) { $check_sender='A'; } elsif ( $opt{do_checks}{'a/24'} and check_sender_a( $sender,$ip,'a/24') ) { $check_sender='A/24'; } # if separate_by_ip or separate_by_subnet display configured text if ($last_subnet eq $triplets{$key}{subnet}) { print "$opt{separate_by_ip}" if ( ($last_ip ne $ip) and $opt{separate_by_ip}) ; } else { if ( $opt{separate_by_subnet} ) { print $opt{separate_by_subnet}; } elsif ( $opt{separate_by_ip} ) { print $opt{separate_by_ip}; } } # display output on single line or multi-line if ($opt{single_line}) { if ($opt{tab}) { printf "%s\t", $triplets{$key}{entrytime} if($opt{show_time}) ; printf "%s\t", $triplets{$key}{counter} if($opt{show_tries}) ; printf "%s\t", $check_sender if($opt{check_sender}) ; printf "%s\t", $ip ; printf "%s\t", $revdns ; printf "%s\t", $sender ; } else { printf "%s ", $triplets{$key}{entrytime} if($opt{show_time}) ; printf "%s ", $triplets{$key}{counter} if($opt{show_tries}) ; printf "%5s ", $check_sender if($opt{check_sender}) ; printf "%15s ", $ip ; printf "%s ", $revdns ; printf "%s ", $sender ; } printf "%s\n", $recipient; ; } else { ### multi-line ## only output PTR - IP if its a new IP (grouping) printf "%-77s ", $revdns if($last_ip ne $ip) ; printf "%15s" , $ip if($last_ip ne $ip) ; print "\n" if($last_ip ne $ip) ; ## always output the new pairs MX/A? (sender/recipient) # if sender was from MX or A of above IP printf "%5s " , $check_sender if($opt{check_sender}) ; printf " ", $check_sender if(! $opt{check_sender}); # tries or blank space printf " %2s ", $triplets{$key}{counter} if($opt{show_tries}) ; print " " if(! $opt{show_tries}) ; # sender - recipient printf " %40s ", $sender ; printf " %40s ", $recipient ; print "\n" ; } ($last_ip, $last_subnet) = ($ip, $triplets{$key}{subnet}); # save for next iteration } } ##################################################################### # find_and_sort_fatal( \%db, \%triplets ) # read STDIN (maillog) and remember any 4xx greylisted log entries # return array of fatal triplets (ip/sender/recipient) sorted by ip sub find_and_sort_fatal { my ($db, $triplets) = @_; # while(<>): STDIN is maillog.0, looking at reject: 4xx greylist entries and remembering all triplets MAILLOG: while (<>) { next unless (/$RE_reject/o); # only look at reject: lines next unless (/$opt{return_string}/o); # only look at greylisted lines next unless (/$RE_triplet/o); # extract the triplet my ($revdns,$ipaddr,$sender,$recipient) = ($1,$2,$3,$4); my @ip = split(/\./, $ipaddr); $sender = do_sender_substitutions($sender); my ($subnet) = do_client_substitutions($ipaddr,$revdns); # 1.2.3.0 my $key = lc "$ipaddr/$sender/$recipient"; # postgrey key my $subkey = lc "$subnet/$sender/$recipient"; # subnet key 1.2.3.0/sender/recipient # if we are wanting to dump first,last out of the db do it before we determine if its fatal if ( is_debug_host($revdns) ) { foreach my $testkey ( @{[$key,$subkey]} ) { my ($tfirst, $tlast) = split(/,/,$db->{$testkey}); my $tdiff = $tlast - $tfirst; print "$testkey : $db->{$testkey} = " .$tdiff . "s \n"; } } # if --match_clients was specified on command line then move on to the next line unless a match is found if ( scalar(@{$opt{match_clients}}) > 0 ) { next unless ( find_in_array($ipaddr, $opt{MATCH_CLIENT_IPS}) or find_in_array($revdns, $opt{MATCH_CLIENT_PTR}) ); } # if --skip_clients was specified on command line, skip to next line if a match is found next if ( find_in_array($ipaddr, $opt{SKIP_CLIENT_IPS}) or find_in_array($revdns, $opt{SKIP_CLIENT_PTR}) ); # if --skip_pool then if last 2 ips are in ptr skip to next line next if ( $opt{skip_pool} and defined $ip[3] and $revdns =~ /$ip[2]/ and $revdns =~ /$ip[3]/ ); # check the db, proceed if the triplet was fatal next MAILLOG unless is_fatal_triplet($db, $key, $subkey); # if --skip_dnsbl then do RBL lookups (slow!) if ( defined $rbl ) { $rbl->lookup($ipaddr); my @listed = $rbl->listed_by; next if ( scalar(@listed) > 0 ); } # we made it past all the filtering checks, remember the triplet as fatal $triplets->{$key}{counter}++; # increase counter for this triplet $triplets->{$key}{revdns}=$revdns; # save its ptr for later use $triplets->{$key}{ipaddr}=$ipaddr; # save IP in easy to access form $triplets->{$key}{subnet}=$subnet; # save subnet in easy to access form $triplets->{$key}{subkey}=$subkey; # save key in subnet form $triplets->{$key}{entrytime}=substr($_,0,15); } die "Debugging DB active, report shutdown" if ($opt{debug_db}); # don't do anything other than spit out key pairs and stop my @greyfatal = keys %{ $triplets }; # create an array containing all triplets in form: ip/sender/recipient # sort fatal triplets by IP address @greyfatal = sort { pack('C4' => $a =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) cmp pack('C4' => $b =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) } @greyfatal; return @greyfatal; } sub find_in_array($$) { my ($var, $patterns) = @_; for my $w (@{$patterns}) { return 1 if $var =~ $w; } return 0; } sub is_fatal_triplet($$$) { my ($db, $key, $subkey) = @_; my ($lapsed_ip, $lapsed_subnet) = (undef,undef); # try lookup by key if ( $db->{$key} =~ /,/ ) { my ($tfirst,$tlast) = split(/,/,$db->{$key}); # time_first_seen,time_last_seen $lapsed_ip = $tlast - $tfirst; # difference is time lapsed } # try subnet lookup if ( $db->{$subkey} =~ /,/ ) { my ($tfirst,$tlast) = split(/,/,$db->{$subkey}); # time_first_seen,time_last_seen $lapsed_subnet = $tlast - $tfirst; } if ( ( defined $lapsed_ip or defined $lapsed_subnet ) and (!( ($lapsed_ip >= $opt{delay} ) or ($lapsed_subnet >= $opt{delay}) ) ) ) { #push (@greyfatal, $key); # if lapsed time less than --delay, then it was a fatal triplet return 1; } elsif (( ! defined $lapsed_ip ) and ( ! defined $lapsed_subnet )) { #push (@greyfatal, $key); # if neither is found in the db it must have been removed. return 1; } return 0; } ########################################################################### # check_sender_mx(sender, ip, subnet) # subnet='' or '/24' # return true if ip is in MX list for sender domain (or /24 if specified) # enable via --check_sender=mx or --check_sender=mx,mx/24 sub check_sender_mx($$$) { my ($sender, $ip, $subnet) = @_; my ($user, $hostname) = split(/\@/,$sender); my @iplist; if ( $dns_cache{$hostname}{mx} ) { @iplist = @{$dns_cache{$hostname}{mx}}; # use the cache for MX records } else { my @mxr = mx($dns, $hostname); # no cache existed, call out to Net::DNS # mx records if ($#mxr >= 0) { foreach my $mxrr (@mxr) { # print "MX for $hostname: ". $mxrr->exchange . "\n"; my $ipquery = $dns->search($mxrr->exchange); if ($ipquery) { foreach my $iprr ($ipquery->answer) { next unless ($iprr->type eq "A"); # print " IP=" . $iprr->address . "\n"; push (@iplist, $iprr->address); } } } } if ( $#iplist < 0 ) { push (@iplist, '0.0.0.0'); } # cache ip of all zero's so we dont keep calling net::dns if nothing is returned $dns_cache{$hostname}{mx} = [ @iplist ]; # cache the array IPs of the MX records into an hash location. } $subnet =~ s/^mx//i; return check_sender_ip_vs_list($ip, $subnet, \@iplist); } ########################################################################### # check_sender_a(sender, ip, subnet) # subnet='' or '/24' # return true if ip is in A record for sender domain (or /24 if specified) # enable via --check_sender=a or --check_sender=a,24 sub check_sender_a($$$) { my ($sender, $ip, $subnet) = @_; my ($user, $hostname) = split(/\@/,$sender); my @iplist; if ( $dns_cache{$hostname}{a} ) { @iplist = @{$dns_cache{$hostname}{a}}; # use the cache'd A records } else { my $ipquery = $dns->search($hostname); # no cache existed, call out to Net::DNS if ($ipquery) { foreach my $iprr ($ipquery->answer) { next unless ($iprr->type eq "A"); # print " IP=" . $iprr->address . "\n"; push (@iplist, $iprr->address); } } if ( $#iplist < 0 ) { push (@iplist, '0.0.0.0'); } # cache ip of all zero's so we dont keep calling net::dns if nothing is returned $dns_cache{$hostname}{a} = [ @iplist ]; # cache the array IPs of the A records into an hash location. } $subnet =~ s/^a//i; return check_sender_ip_vs_list($ip, $subnet, \@iplist); } ################################################### # used by check_sender_mx and check_sender_a # return true if IP is in list # if /24 then return true if first 3 octets match sub check_sender_ip_vs_list($$$) { my ($client_ip, $match, $iplist) = @_; foreach my $ipaddr ( @{$iplist} ) { return 1 if ($client_ip eq $ipaddr); return 0 if (! $match eq '/24'); $client_ip =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.)/; my $client_classaddr = $1; $ipaddr =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.)/; my $ipaddr_classaddr = $1; return 1 if ( $client_classaddr eq $ipaddr_classaddr ); } return 0 } ######################################### # drop_priv(username) # code from Net::Server sub drop_priv { my ($user) = @_; ### drop privileges eval{ if( $user ne $> ){ # print "Setting uid to \"$user\"\n"; set_uid( $user ); } }; if( $@ ){ if( $> == 0 ){ die $@; } elsif( $< == 0){ # print "NOTICE: Effective UID changed, but Real UID is 0: $@\n"; }else{ print $@."\n"; } } } ###########################################3 # setup_dbm(dbdir) # connect to BerkeleyDB *READ_ONLY*, return reference to db hash sub setup_dbm { my ($dbdir) = @_; my %db; tie(%db, 'BerkeleyDB::Btree', -Filename => "$dbdir/postgrey.db", -Flags => DB_RDONLY, ) or die "ERROR: can't find database $dbdir/postgrey.db: $!\n"; return \%db; } # from postgrey 1.14 http://isg.ee.ethz.ch/tools/postgrey/ sub do_sender_substitutions($) { my ($addr) = @_; my ($user, $domain) = split(/@/, $addr, 2); defined $domain or return $addr; # strip extension, used sometimes for mailing-list VERP $user =~ s/\+.*//; # replace numbers in VERP addresses with '#' so that # we don't create a new key for each mail $user =~ s/\b\d+\b/#/g; return "$user\@$domain"; } # from postgrey 1.14 http://isg.ee.ethz.ch/tools/postgrey/ sub do_client_substitutions($$) { my ($ip, $revdns) = @_; # --lookup-by-subnet: return ($ip, undef) if $revdns eq 'unknown'; my @ip=split(/\./, $ip); return ($ip, undef) unless defined $ip[3]; # skip if it contains the last two IP numbers in the hostname # (we assume it is a pool of dialup addresses of a provider) return ($ip, undef) if $revdns =~ /$ip[2]/ and $revdns =~ /$ip[3]/; return (join('.', @ip[0..2], '0'), $ip[3]); } ## used code from postgrey for read_client_whitelists() to import client files sub read_client_files() { my @skip_client_ips; my @skip_client_ptr; my @match_client_ips; my @match_client_ptr; for my $f (@{$opt{'skip_clients'}}) { if(open(CLIENTS, $f)) { while() { s/^\s+//; s/\s+$//; next if $_ eq '' or /^#/; if(/^\/(\S+)\/$/) { # regular expression push @skip_client_ptr, qr{$1}i; } elsif(/^\d{1,3}(?:\.\d{1,3}){0,3}$/) { # IP address or part of it push @skip_client_ips, qr{^$_}; } # note: we had ^[^\s\/]+$ but it triggers a bug in perl 5.8.0 elsif(/^\S+$/) { push @skip_client_ptr, qr{\Q$_\E$}i; } else { warn "WARNING: $f line $.: doesn't look like a hostname\n"; } } } } $opt{SKIP_CLIENT_PTR} = \@skip_client_ptr; $opt{SKIP_CLIENT_IPS} = \@skip_client_ips; for my $f (@{$opt{'match_clients'}}) { if(open(CLIENTS, $f)) { while() { s/^\s+//; s/\s+$//; next if $_ eq '' or /^#/; if(/^\/(\S+)\/$/) { # regular expression push @match_client_ptr, qr{$1}i; } elsif(/^\d{1,3}(?:\.\d{1,3}){0,3}$/) { # IP address or part of it push @match_client_ips, qr{^$_}; } # note: we had ^[^\s\/]+$ but it triggers a bug in perl 5.8.0 elsif(/^\S+$/) { push @match_client_ptr, qr{\Q$_\E$}i; } else { warn "WARNING: $f line $.: doesn't look like a hostname\n"; } } } } $opt{MATCH_CLIENT_PTR} = \@match_client_ptr; $opt{MATCH_CLIENT_IPS} = \@match_client_ips; } sub setup_debug() { if ($opt{debug_db} or $opt{search_db}) { die "\nDebugging_DB Activated, but no matching RE's defined. use --debug_re also! \n " if (! $opt{debug_re} ); print "\nDebugging_DB Active, Displaying hosting matching REs: "; foreach my $RE ( split(/,/,$opt{debug_re}) ) { print "$RE ; "; push ( @{ $opt{debug_RE} }, qr/$RE/i ); } print "\n\n"; } } sub is_debug_host($) { my ($host) = @_; foreach my $RE ( @{$opt{debug_RE}} ) { return 1 if ($host =~ /$RE/); } return 0; } main(); exit 0; __END__ =head1 NAME postgreyreport - Fatal report for Postfix Greylisting Policy Server =head1 SYNOPSIS B [I...] -h, --help display this help and exit --version display version and exit --user=USER run as USER (default: postgrey) --dbdir=PATH find db files in PATH (default: /var/spool/postfix/postgrey) --delay=N report triplets that did not try again after N seconds (default: 300) --greylist-text=TXT text to match on for greylist maillog lines --skip_pool Skip report for 'subscriber pools' ( last 2 octets of IP found in PTR name ) --skip_dnsbl=RBL RBL server to query and skip reporting for any listed hosts (SLOW!!) --skip_clients=FILE PTR or IP or REGEXP of clients to skip in report --match_clients=FILE *ONLY* report if fatal *AND* PTR/IP of client matches --show_tries display the number of attempts failed triplets made in first column --show_time show entry time in maillog (single line only) --tab use tabs as separators for easy cut(1)ting --nosingle_line display sender/recipients grouped by ptr - ip --separate_by_subnet=TXT display TXT for every new /24 (ex: "=================\n" ) --separate_by_ip=TXT display TXT for every new IP (ex: "\n") --check_sender=LIST one or more of: mx,mx/24,a,a/24 does DNS/A lookups for sender @domain and compares sending IP if match displays "MX" "A" or "MX/24" or "A/24" depending on LIST Note that --(skip|match)_clients can be specified multiple times and there are no default files. Same rules apply as postgrey's --whitelist-clients, see postgrey doc for more info. --skip_dnsbl can also be specified multiple times to query multiple DNSBL servers. =head1 DESCRIPTION postgreyreport opens postgrey.db as read-only; reads a maillog via STDIN, extracts the triplets for any Greylisted lines and looks them up in postgrey.db. if the difference in first and last time seen is less than --delay=N then the triplet is considered fatal and displayed to STDOUT The report sorts by client IP address =head2 Note: unless you are using --lookup_by_subnet or excluding all known MTA pools you will likely have false fatal reports for "BigISPs". A message that was tried from every IP in SMTP pool before making it through will show up in the report for all of the attempted source IPs =head2 USAGE It is best to run postgreyreport against a maillog that is at least several hours old (yesterdays?) ( you be the judge on how old is acceptable ). if you run the report against a live maillog you are not giving legit MTA's enough time to try again and you will have lots of inaccurate information. =over =item * Ex usage: zcat /var/log/maillog.0.gz | ./postgreyreport [options] > postgreyreport.log or zcat /var/log/maillog.0.gz | \ ./postgreyreport --nosingle_line --check_sender=mx,a \ --separate_by_subnet=":==================\n" # 94 "=" total, some were omitted for clarity =item * Ex Output: ( POD wrapping will mess this up, view source ) :============================================================================================ unknown 4.29.43.31 marissa_mcclendonuu@abit.com.tw user1@recipient1.com jake_meyerdt@ali.com.tw user2@recipient1.com jenny_banks_sh@translate.ru user1@recipient2.com rvazquezpo@ali.com.tw user3@recipient1.com aep@notimexico.com user2@recipient1.com brittneystanley_ei@cetra.org.tw user2@recipient1.com brendasheehan_cw@lib.ru user2@recipient1.com :============================================================================================ lsanca1-ar5-127-189.biz.dsl.gtei.net 4.33.127.189 A fokkensr@lsanca1-ar5-127-189.biz.dsl.gtei.net user2@recipient1.com cyxlfrfwciercu@publicist.com user3@recipient4.com :============================================================================================ smtpout.mac.com 17.250.248.83 do_not_reply@apple.com user4@recipient5.com smtpout.mac.com 17.250.248.88 MX legituser@mac.com user6@recipient7.com :============================================================================================ =back =head1 HISTORY B<1.14.3 20100321> =over 4 Some additions, Leonard den Ottolander New option: --tab Use tabs as separator in single line mode New option: --show_time Show entry time in maillog in single line mode =back B<1.14.2 20040715> =over 4 BUGFIX: (automatic) lookup-by-subnet support was broken, fixed. BUGFIX: corrected a few spelling errors new Option: --skip_pool Skip report for 'subscriber pools' =back B<1.14.1 20040712> =over 4 Changed --return-string to --greylist-text to match postgrey new Option: --skip_clients=FILE new Option: --match_clients=FILE new Option: --skip_dnsbl=RBL.DNS.NAME All 3 of the new options can be specified multiple times. Updated do_*_subsititions again to match postgrey =back B<1.11.1 20040701> =over 4 missing keys from DB are considered fatal triplets and included in report Changed --delay testing from "greater than" to "greater than or equal to" Fixed --help and --man switches Removed setuid Notice =back B<1.6.4 20040618> =over 4 Initial Public Version (postgrey/contrib) =back =head1 AUTHOR Stbaker@bakerfl.orgE> =cut postgrey-1.36/contrib/postgrey.init0000644000175000017500000000712612554251301015700 0ustar dwsdws#! /bin/sh # # postgrey start/stop the postgrey greylisting deamon for postfix # (priority should be smaller than that of postfix) # # Author: (c)2004-2006 Adrian von Bidder # Based on Debian sarge's 'skeleton' example # Distribute and/or modify at will. # # Version: $Id: postgrey.init 1436 2006-12-07 07:15:03Z avbidder $ # ### BEGIN INIT INFO # Provides: postgrey # Required-Start: $syslog $local_fs $remote_fs # Required-Stop: $syslog $local_fs $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start/stop the postgrey daemon ### END INIT INFO set -e PATH=/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/sbin/postgrey DAEMON_NAME=postgrey DESC="postfix greylisting daemon" DAEMON_USER=postgrey PIDFILE=/var/run/$DAEMON_NAME.pid SCRIPTNAME=/etc/init.d/$DAEMON_NAME # Gracefully exit if the package has been removed. test -x $DAEMON || exit 0 . /lib/lsb/init-functions # Read config file if it is present. if [ -r /etc/default/$DAEMON_NAME ] then . /etc/default/$DAEMON_NAME fi POSTGREY_OPTS="--pidfile=$PIDFILE --daemonize $POSTGREY_OPTS" if [ -z "$POSTGREY_TEXT" ]; then POSTGREY_TEXT_OPT="" else POSTGREY_TEXT_OPT="--greylist-text=$POSTGREY_TEXT" fi ret=0 do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ $POSTGREY_OPTS "$POSTGREY_TEXT_OPT" \ || return 2 } do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --user $DAEMON_USER --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --user $DAEMON_USER --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$DAEMON_NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$DAEMON_NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; reload|force-reload) [ "$VERBOSE" != no ] && log_daemon_msg "Reloading $DESC" "$DAEMON_NAME" do_reload case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; restart) do_stop do_start ;; status) status_of_proc -p $PIDFILE $DAEMON "$DAEMON_NAME" 2>/dev/null ret=$? ;; *) echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload|status}" >&2 exit 1 ;; esac exit $ret postgrey-1.36/postgrey_whitelist_clients0000644000175000017500000002147412571400066017120 0ustar dwsdws# postgrey whitelist for mail client hostnames # -------------------------------------------- # put this file in /etc/postfix or specify its path # with --whitelist-clients=xxx # # postgrey version: 1.36, build date: 2015-09-01 # greylisting.org: Southwest Airlines (unique sender, no retry) southwest.com # greylisting.org: isp.belgacom.be (wierd retry pattern) isp.belgacom.be # greylisting.org: Ameritrade (no retry) ameritradeinfo.com # greylisting.org: Amazon.com (unique sender with letters) amazon.com # 2004-05-20: Linux kernel mailing-list (unique sender with letters) vger.kernel.org # 2004-06-02: karger.ch, no retry karger.ch # 2004-06-02: lilys.ch, (slow: 4 hours) server-x001.hostpoint.ch # 2004-06-09: roche.com (no retry) gw.bas.roche.com # 2004-06-09: newsletter (no retry) mail.hhlaw.com # 2004-06-09: no retry (reported by Ralph Hildebrandt) prd051.appliedbiosystems.com # 2004-06-17: swissre.com (no retry) swissre.com # 2004-06-17: dowjones.com newsletter (unique sender with letters) returns.dowjones.com # 2004-06-18: switch.ch (works but personnel is confused by the error) domin.switch.ch # 2004-06-23: accor-hotels.com (slow: 6 hours) accor-hotels.com # 2004-06-29: rr.com (no retry, reported by Duncan Hill) /^ms-smtp.*\.rr\.com$/ # 2004-06-29: cox.net (no retry, reported by Duncan Hill) /^lake.*mta.*\.cox\.net$/ # 2004-06-29: motorola.com (no retry) mot.com # 2004-07-01: nic.fr (address verification, reported by Arnaud Launay) nic.fr # 2004-07-01: verizon.net (address verification, reported by Bill Moran and Eric, adapted by Adam C. Mathews) /^s[cv]\d+pub\.verizon\.net$/ # 2004-07-02: cs.columbia.edu (no retry) cs.columbia.edu # 2004-07-02: papersinvited.com (no retry) 66.216.126.174 # 2004-07-02: telekom.de (slow: 6 hours) /^mail\d+\.telekom\.de$/ # 2004-07-04: tiscali.dk (slow: 12 hours, reported by Klaus Alexander Seistrup) /^smtp\d+\.tiscali\.dk$/ # 2004-07-04: freshmeat.net (address verification) freshmeat.net # 2004-07-11: zd-swx.com (unique sender with letters, reported by Bill Landry) zd-swx.com # 2004-07-11: lockergnome.wc09.net (unique sender with letters, reported by Bill Landry) lockergnome.wc09.net # 2004-07-19: mxlogic.net (no retry, reported by Eric) p01m168.mxlogic.net p02m169.mxlogic.net # 2004-09-08: intel.com (pool on different subnets) /^fmr\d+\.intel\.com$/ # 2004-09-17: cox-internet.com (no retry, reported by Rod Roark) /^fe\d+\.cox-internet\.com$/ # 2004-10-11: logismata.ch (no retry) logismata.ch # 2004-11-25: brief.cw.reum.de (no retry, reported by Manuel Oetiker) brief.cw.reum.de # 2004-12-03: ingeno.ch (no retry) qmail.ingeno.ch # 2004-12-06: rein.ch (no retry) mail1.thurweb.ch # 2005-01-26: tu-ilmenau.de (no retry) piggy.rz.tu-ilmenau.de # 2005-04-06: polymed.ch (no retry) mail.polymed.ch # 2005-06-08: hu-berlin.de (slow: 6 hours, reported by Joachim Schoenberg) rz.hu-berlin.de # 2005-06-17: gmail.com (big pool, reported by Beat Mueller) proxy.gmail.com # 2005-06-23: cacert.org (address verification, reported by Martin Lohmeier) cacert.org # 2005-07-27: polytech.univ-mrs.fr (no retry, reported by Giovanni Mandorino) polytech.univ-mrs.fr # 2005-08-05: gnu.org (address verification, reported by Martin Lohmeier) gnu.org # 2005-08-17: ciphirelabs.com (needs fast responses, reported by Sven Mueller) cs.ciphire.net # 2005-11-11: lufthansa (no retry, reported by Peter Bieringer) /^gateway\d+\.np4\.de$/ # 2005-11-23: arcor-online.net (slow: 12 hours, reported by Bernd Zeimetz) /^mail-in-\d+\.arcor-online\.net$/ # 2005-12-29: netsolmail.com (no retry, reported by Gareth Greenaway) netsolmail.com # mail.likopris.si (no retry, reported by Vito Robar) 193.77.153.67 # jcsw.nato.int (several servers, no retry, reported by Vito Robar) 195.235.39 # tesla.vtszg.hr (no retry, reported by Vito Robar) tesla.vtszg.hr # mailgw*.iai.co.il (pool of several servers, reported by Vito Robar) /^mailgw.*\.iai\.co\.il$/ # gw.stud-serv-mb.si (no retry, reported by Vito Robar) gw.stud-serv-mb.si # mail.commandtech.com (no retry, reported by Vito Robar) 216.238.112.99 # duropack.co.at (no retry, reported by Vito Robar) 193.81.20.195 # mail.esimit-tech.si (no retry, reported by Vito Robar) 193.77.126.208 # mail.resotel.be (ocasionally no retry, reported by Vito Robar) 80.200.249.216 # mail2.alliancefr.be (ocasionally no retry, reported by Vito Robar) mail2.alliancefr.be # webserver.turboinstitut.si (no retry, reported by Vito Robar) webserver.turboinstitut.si # mil.be (pool of different servers, reported by Vito Robar) 193.191.218.141 193.191.218.142 193.191.218.143 194.7.234.141 194.7.234.142 194.7.234.143 # mail*.usafisnews.org (no retry, reported by Vito Robar) /^mail\d+\.usafisnews\.org$/ # odk.fdv.uni-lj.si (no retry, reported by Vito Robar) /^odk.fdv.uni-lj.si$/ # rak-gentoo-1.nameserver.de (no retry, reported by Vito Robar) rak-gentoo-1.nameserver.de # dars.si (ocasionally no retry, reported by Vito Robar) mx.dars.si # cosis.si (no retry, reported by Vito Robar) 213.143.66.210 # mta?.siol.net (sometimes no or slow retry; they use intermail, reported by Vito Robar) /^mta[12].siol.net$/ # pim-N-N.quickinspirationsmail.com (unique sender, reported by Vito Robar) /^pim-\d+-\d+\.quickinspirationsmail\.com$/ # flymonarch (no retry, reported by Marko Djukic) flymonarch.com # wxs.nl (no retry, reported by Johannes Fehr) /^p?smtp.*\.wxs\.nl$/ # ibm.com (big pool, reported by Casey Peel) ibm.com # messagelabs.com (big pool, reported by John Tobin) messagelabs.com # ptb.de (slow, reported by Joachim Schoenberg) berlin.ptb.de # registrarmail.net (unique sender names, reported by Simon Waters) registrarmail.net # google.com (big pool, reported by Matthias Dyer, Martin Toft) google.com # orange.fr (big pool, reported by Loïc Le Loarer) /^smtp\d+\.orange\.fr$/ # citigroup.com (slow retry, reported by Michael Monnerie) /^smtp\d+.citigroup.com$/ # cruisingclub.ch (no retry) mail.ccs-cruising.ch # digg.com (no retry, Debian #406774) diggstage01.digg.com # liberal.ca (retries only during 270 seconds, Debian #406774) smtp.liberal.ca # pi.ws (pool + long retry, Debian #409851) /^mail[12]\.pi\.ws$/ # rambler.ru (big pool, reported by Michael Monnerie) rambler.ru # free.fr (big pool, reported by Denis Sacchet) /^smtp[0-9]+-g[0-9]+\.free\.fr$/ /^postfix[0-9]+-g[0-9]+\.free\.fr$/ # thehartford.com (pool + long retry, reported by Jacob Leifman) /^netmail\d+\.thehartford\.com$/ # abb.com (only one retry, reported by Roman Plessl) /^nse\d+\.abb\.com$/ # 2007-07-27: sourceforge.net (sender verification) lists.sourceforge.net # 2007-08-06: polytec.de (no retry, reported by Patrick McLean) polytec.de # 2007-09-06: qualiflow.com (no retry, reported by Alex Beckert) /^mail\d+\.msg\.oleane\.net$/ # 2007-09-07: nrl.navy.mil (no retry, reported by Axel Beckert) nrl.navy.mil # 2007-10-18: aliplast.com (long retry, reported by Johannes Feigl) mail.aliplast.com # 2007-10-18: inode.at (long retry, reported by Johannes Feigl) /^mx\d+\..*\.inode\.at$/ # 2008-02-01: bol.com (no retry, reported by Frank Breedijk) /^.*?.server.arvato-systems.de$/ # 2008-06-05: registeredsite.com (no retry, reported by Fred Kilbourn) /^(?:mail|fallback-mx)\d+.atl.registeredsite.com$/ # 2008-07-17: mahidol.ac.th (no retry, reported by Alex Beckert) saturn.mahidol.ac.th # 2008-07-18: ebay.com (big pool, reported by Peter Samuelson) ebay.com # 2008-07-22: yahoo.com (big pool, reported by Juan Alonso) yahoo.com # 2008-11-07: facebook (no retry, reported by Tim Freeman) /^outmail\d+\.sctm\.tfbnw\.net$/ # 2009-02-10: server14.cyon.ch (long retry, reported by Alex Beckert) server14.cyon.ch # 2009-08-19: 126.com (big pool) /^m\d+-\d+\.126\.com$/ # 2010-01-08: tifr.res.in (no retry, reported by Alex Beckert) home.theory.tifr.res.in # 2010-01-08: 1blu.de (long retry, reported by Alex Beckert) ms4-1.1blu.de # 2010-03-17: chello.at (big pool, reported by Jan-willem van Eys) /^viefep\d+-int\.chello\.at$/ # 2010-05-31: nic.nu (long retry, reported by Ivan Sie) mx.nic.nu # 2010-06-10: Microsoft servers (long/no retry, reported by Roy McMorran) bigfish.com frontbridge.com microsoft.com # 2010-06-18: Google/Postini (big pool, reported by Warren Trakman) postini.com # 2011-02-04: evanzo-server.de (no retry, reported by Andre Hoepner) /^mx.*\.evanzo-server\.de$/ # 2011-05-02: upcmail.net (big pool, reported by Michael Monnerie) upcmail.net # 2013-12-18: orange.fr (big pool, reported by fulax) /^smtp\d+\.smtpout\.orange\.fr$/ # 2014-01-29: gmx/web.de/1&1 (long retry, reported by Axel Beckert) mout-xforward.gmx.net mout-xforward.web.de mout-xforward.kundenserver.de mout-xforward.perfora.net # 2014-02-01: startcom.org (long retry, reported by jweiher) gateway.startcom.org # 2014-12-18: mail.ru (retries from fallback*.mail.ru, reported by Andriy Yurchuk) /^fallback\d+\.mail\.ru$/ # French tax authority, no retry dgfip.finances.gouv.fr # 2015-06-10: magisto.com (requested by postmaster) /^o\d+\.ntdc\.magisto\.com$/ # 2015-07-23: outlook.com (github #20) outlook.com # 2015-08-19 (the retrying is failing) mail.alibaba.com postgrey-1.36/README.exim0000644000175000017500000000566012252310311013311 0ustar dwsdwsUsing Postgrey with Exim ------------------------ Whilst Postgrey is designed to work with the Postfix MTA it is possible to get it to work with Exim. This isn't the only way to get greylisting to work under Exim[1], and it might not even be the easiest. Postgrey does, however, have some advantages over the alternatives that may be useful depending on your situation. In order to get Postgrey and Exim to cooperate we need to get Exim to implement enough of Postfix's access policy delegation protocol[2] to be able to talk to the Postgrey daemon, which we can do in an acl_smtp_rcpt ACL in Exim's config file: defer log_message = greylisted host $sender_host_address set acl_m0 = request=smtpd_access_policy\nprotocol_state=RCPT\nprotocol_name=${uc:$received_protocol}\nhelo_name=$sender_helo_name\nclient_address=$sender_host_address\nclient_name=$sender_host_name\nsender=$sender_address\nrecipient=$local_part@$domain\n\n set acl_m0 = ${sg{${readsocket{/var/run/postgrey}{$acl_m0}{5s}{}{action=DUNNO}}}{action=}{}} message = ${sg{$acl_m0}{^\\w+\\s*}{}} condition = ${if eq{${uc:${substr{0}{5}{$acl_m0}}}}{DEFER}{true}{false}} Whilst this ACL looks complicated, all it really does tells Exim to defer incoming e-mail if it is greylisted and has no effect once the greylisting period is over. It fails safe in the sense that it'll pass all e-mail without deferring it if it can't talk to Postgrey (the hard-coded "action=DUNNO"). It's also worth noting that it isn't a full implementation of Postfix's policy protocol -- it is just enough to get the greylisting job done in a reasonably sane way. You'll obviously need to adjust the above ACL to match your setup. In particular you might need to change the socket path from /var/run/postgrey. You should also check that you're not already using $acl_m0 for something else (you can change all occurrences of $acl_m0 to any unused variable between $acl_m1 and $acl_m9 if you are). Postfix's protocol allows for a policy server to prepend a header on delivered e-mail, and Postgrey uses this to notify users of how long mail was delayed for. If you want to make use of this feature, you need the following ACL somewhere under your defer one: warn message = ${sg{$acl_m0}{^\\w+\\s*}{}} condition = ${if eq{${uc:${substr{0}{7}{$acl_m0}}}}{PREPEND}{true}{false}} Once you've changed your Exim config, you'll also need to set up and configure Postgrey. You do this as you would for Postfix, except that you need to add the --exim option to the postgrey command line. This alters the behavior of Postgrey slightly and makes it close the connection as Exim's readsocket expansion expects. You can test your new configuration using Exim's -bh command line option to emulate SMTP sessions from a variety of different places. -- - Guy Antony Halse [1] http://www.greylisting.org/implementations/exim.shtml [2] http://www.postfix.org/SMTPD_POLICY_README.html postgrey-1.36/COPYING0000644000175000017500000004307012252310311012520 0ustar dwsdws GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, 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) 19yy 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., 675 Mass Ave, Cambridge, MA 02139, 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) 19yy 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.