#!/usr/bin/perl
###########################################################
# txt2rhsbl
#
# Converts local host/domain blacklist to DNS RHSBL zone file
#
# Available from: <http://www.postconf.com/docs/txt2rhsbl>
# See also: <http://www.postconf.com/docs/txt2rbl>
###########################################################
## $SRCTXT="/etc/spamdomains";        ## sender RHSBL
$SRCTXT="/etc/spamhosts";             ## client RHSBL
$DSTDB="/var/named/db.example.com.rhsbl";
$ZONE='rhsbl.example.com';
$MESSAGE="per <http://rhsbl.example.com/>";
$ZONENS='ns.example.com';
$ZONEADMIN='hostmaster.example.com';
$TTL=86400;
$SERIALNO=20000730;
@WHITELIST=('localhost','example.com','yahoo.com','google.com');
###########################################################
# to do: increment serial# for slave servers,
#        test zone file with nslint,
#        warning msg if duplicate, ...
###########################################################
$TMPDB="$DSTDB.tmp";

if ( ! -w $DSTDB ) {
	 print "  ERROR: $DSTDB not found or not writeable.\n";
	 exit 1;
} elsif ( ! -s $SRCTXT ) {
	print "  ERROR: missing or empty $SRCTXT.\n";
	exit 1;
}

if ( -f $TMPDB ) {
	print "  Lock ($TMPDB) found, sleeping 60 seconds.\n";
	sleep 60;
	for ( $i=1 ; $i<5 ; $i++ ) {
		if ( -f $TMPDB ) {
			print "  Lock ($TMPDB) found, sleeping another 60 seconds.\n";
			sleep 60;
		}
	}
	if ( -f $TMPDB )  {
		print "  Lock ($TMPDB) still found after 5 minutes, giving up.\n";
		exec `logger "  Lock ($TMPDB) still found after 5 minutes, giving up.\n"`;
		exit 1;
	}
}

sub Terminate() {
	close($SRCTXT);
	close($TMPDB);
	unlink($TMPDB);
	die("$_[0]");
}
$SIG{ INT } = \&Terminate;
$SIG{ KILL } = \&Terminate;
$SIG{ TERM } = \&Terminate;
$SIG{ QUIT } = \&Terminate;

print "Updating $ZONE from $SRCTXT to $DSTDB...\n";

open(TMPDB,">$TMPDB") || \&Terminate("  Cannot create $TMPDB\n");

#### write zone file header ####
print TMPDB <<"TAG";
\$TTL $TTL
@ IN SOA $ZONENS. $ZONEADMIN. (
	$SERIALNO 86400 43200 604800 $TTL )

	IN NS ${ZONENS}.

example.tld            A    127.0.0.2
example.tld            TXT  "$MESSAGE for testing"
*.example.tld          A    127.0.0.2
*.example.tld          TXT  "$MESSAGE for testing"
example.com            A    127.0.0.2
example.com            TXT  "$MESSAGE for testing"
*.example.com          A    127.0.0.2
*.example.com          TXT  "$MESSAGE for testing"
example.net            A    127.0.0.2
example.net            TXT  "$MESSAGE for testing"
*.example.net          A    127.0.0.2
*.example.net          TXT  "$MESSAGE for testing"
example.org            A    127.0.0.2
example.org            TXT  "$MESSAGE for testing"
*.example.org          A    127.0.0.2
*.example.org          TXT  "$MESSAGE for testing"

TAG

open(SRCTXT, $SRCTXT)|| \&Terminate("  Cannot read $SRCTXT\n");
DOMAIN: foreach (<SRCTXT>) {

	#### parse source file ####
	next DOMAIN if /(^#|^$|^\s)/;   # ignore comments, nulls, start with space/tab
	next DOMAIN if /\s(HOLD|DISCARD|WARN|LOCAL)/; # skip local tags
	@line = split(/\s/, $_);        # strip comments/tags
	tr/A-Z/a-z/;                    # to lower case
	$_ = $line[0];
	chomp;
	s/^\.+//;                       # strip leading dots
	s/\.+$//;                       # and trailing dots

	#### validate FQDN syntax ####
	if ( /[^a-z0-9-\.]/ || /\.-/ || /-\./ || /---/ || /\.\./ || /^-/ || /-$/ ) {
		#### illegal chars, sequential dots/dashes, leading/trailing dashes
		print "  REJECT:  $_  is not a valid host or domain name.\n";
		next DOMAIN
	}

	#### check whitelist ####
	$checkdom = $_;
	foreach $whitelisted ( @WHITELIST ) {
		chomp $whitelisted;
		if ( $checkdom =~ /^$whitelisted$/ ) {
			print "  REJECT:  $checkdom  is whitelisted.\n";
			next DOMAIN
		} # perl bug, -w may generate "panic: pp_iter"
	}

	#### cull duplicates ####
	$index{$_}++;
}
foreach (sort keys (%index) ) {
	#### domain and subdomain A and TXT record ####
	printf TMPDB ("%-42s A   127.0.0.2\n", $_);
	printf TMPDB ("%-42s TXT \"$MESSAGE\"\n", $_);
	printf TMPDB ("*.%-40s A   127.0.0.2\n", $_);
	printf TMPDB ("*.%-40s TXT \"$MESSAGE\"\n", $_);
}

close(SRCTXT);
close(TMPDB);
rename $TMPDB, $DSTDB || \&Terminate("  Cannot write $DSTDB\n");
exec `/usr/bin/killall -1 named`;
