#!/usr/bin/python

## policyd-greylist : v0.1

## http://devsec.org/software/policyd-greylist/

## policyd-greylist is a postfix policy daemon written in python that can
## be used to greylist unseen ip/mailfrom/rcptto 3-tuples.
##
## It can be used by running it like this:
##
##   tcpsvd -l0 -c1 -E 127.0.0.1 8895 ./policyd-greylist
##
## ..and then adding it to your postfix main.cf config like this:
##
##   smtpd_recipient_restrictions = ... , inet:127.0.0.1:8895 , ...
##
## - Thor Kooda
##   2008-04-11


import os
import sys
import re
import stat
import hashlib
import time


# set defaults..
settings = {
	"PROGNAME" : "policyd-greylist",
	"CACHEDIR" : "/dev/shm/policyd-greylist",
	"SECONDS" : 60 * 120,
}

def info( msg, desc="info" ):
	print >>sys.stderr, "%s: %s: pid %d : %s" % ( settings["PROGNAME"], desc, os.getpid(), msg )
	sys.stderr.flush()

def warn( msg ):
	info( msg, "warn" )

def bail( act, infostr="" ):
	extra = ""
	if infostr:
		extra = " : info=%s" % infostr
	info( "%s: action=%s%s" % ( args, act, extra ) )
	sys.stderr.flush()
	sys.stdout.write( "action=%s\n\n" % act )
	sys.stdout.flush()
	sys.exit( 0 )


# optionally override defaults
for s in settings.keys():
	e = os.getenv( s )
	if e:
		settings[ s ] = e

var_sender = False
var_recipt = False
var_ip = False
args = ""
line = sys.stdin.readline()
while line:
	if len( line ) < 2:
		break
	line = line.rstrip( "\n" )
	args += line + ", "
	## tkooda : 2008-04-23 : FIXME: check line length before referencing it
	if line[:7] == "sender=":
		var_sender = line[7:].lower()
	elif line[:10] == "recipient=":
		var_recipt = line[10:].lower()
	elif line[:15] == "client_address=":
		var_ip = line[15:].lower()
	line = sys.stdin.readline()

# validate args ..
if not var_sender:
   bail( "dunno", "missing_sender" )
if not var_recipt:
   bail( "dunno", "missing_recipient" )

if "/" in var_recipt: # dosn't catch unicode chars?? -use regex-valid instead?
   bail( "dunno", "invalid_recipient" )

ud = var_recipt.split( "@" )

if len( ud ) != 2:
   bail( "dunno", "recipient_has_multiple_users" )

var_recipt_user = ud[0]
var_recipt_domain = ud[1]

h = hashlib.sha1( "%s:%s:%s" % ( var_ip, var_sender, var_recipt ) ).hexdigest()

path_hash_dir = os.path.join( settings[ "CACHEDIR" ], h[0], h[1] )
path_hash_file = os.path.join( path_hash_dir, h )

# make parent cache dirs (must be initial greylisting)..
if not os.path.isdir( path_hash_dir ):
	try:
		os.makedirs( path_hash_dir )
	except:
		bail( "dunno", "makedirs_failed: %s" % path_hash_dir )

# new initial greylisting..
if not os.path.isfile( path_hash_file ):
	try:
		fd = os.open( path_hash_file, os.O_CREAT )
		os.close( fd )
	except:
		bail( "dunno", "create_failed: %s" % path_hash_file )
	bail( "defer", "greylisted: initial : hash=%s" % h )
	# XXX FIXME: set 4xx string to indicate greylisting (for at least abuse/postmaster/mailer-daemon)

# hash file already exists, fetch it's atime/mtime to decide..
try:
	s = os.stat( path_hash_file )
	time_first = s[stat.ST_MTIME]
	time_last = s[stat.ST_ATIME]
except:
	bail( "dunno", "stat_failed: %s" % path_hash_file )

now = int( time.time() )

# indicate recent attempt by setting atime ("greylist 'pass' keepalive")..
try:
	os.utime( path_hash_file, ( now, time_first ) )
except:
	bail( "dunno", "utime_failed: %s, hash=%s" % ( path_hash_file, h ) )

# greylist if not old enough..
if time_first + int( settings[ "SECONDS" ] ) > now:
	bail( "defer", "greylisted: first=%d, last=%d, remaining=%d, hash=%s" \
		  % ( time_first, time_last, time_first + int( settings[ "SECONDS" ] ) - now, h ) )
	# XXX FIXME: set 4xx string to indicate greylisting (for at least abuse/postmaster/mailer-daemon)

# don't greylist this message..
bail( "dunno", "passed-greylisting: first=%d, last=%d, hash=%s" % ( time_first, time_last, h ) )

