#!/usr/bin/python2.2
#
#   monouso.py v0.3
#   e-mail usa e getta *senza* database sql
#   piuttosto semplice infatti.
#
#   E' in grado di generare indirizzi usa-e-getta su un dominio
#   virtuale, utilizzando tre mappe di lookup per postfix:
#
#    block
#	contiene gli indirizzi gia' usati, quelli che possono
#	quindi essere bloccati in smtpd_recipient_restrictions
#	(il che dovrebbe permettere, in abbinamento all'opzione
#	smtpd_delay_reject=no di postfix, di respingere lo spam
#	senza neanche trasferirlo sulla rete)
#
#    virtual
#	che mappa gli indirizzi fwd-codice@dominio.virtuale in
#	fwd-codice sulla macchina locale (da aggiungere a virtual_maps)
#
#    aliases
#	tabella che mappa tutti gli indirizzi attualmente validi
#	verso questo programma.
#
#   La separazione virtual/aliases e' dovuta al fatto che in postfix
#   e' impossibile redirigere la posta attraverso virtual_maps in
#   un comando (con |).
#
#
#   (c)2003 a.p.m. ale@incal.net
#


import random, dbhash, time, re
import os, sys, fcntl, email
from email.Utils import getaddresses

# dominio degli indirizzi usa e getta
domain = 'incal.net'

# base di tutti i percorsi seguenti
# (la barra in fondo e' importante)
base = '/work/monouso/'

# file con gli indirizzi da bloccare in postfix
# (in genere in smtpd_recipient_restrictions)
postfix_block_file = base + 'block'

# file con gli alias attivi
postfix_alias_file = base + 'aliases'

# file con la mappa per il dominio virtuale
postfix_virtual_file = base + 'virtual'

# file di log (se non e' impostato il logging non
# viene effettuato)
log_file = base + 'log'

# file del database
database_file = base + 'mono_db'

# indirizzo collegato a /dev/null
noreply_address = 'nobody@incal.net'

# collocazione di sendmail
sendmail_path = '/usr/sbin/sendmail'

# percorso completo di questo programma
if sys.argv[0][0:1] == '/':
    program_name = sys.argv[0]
else:
    program_name = os.path.join(os.getcwd(),sys.argv[0])



# operazioni sui file
def file_append(file, line):
    fp = open(file, 'a')
    fcntl.lockf(fp, fcntl.LOCK_EX)
    fp.write(line)
    fcntl.lockf(fp, fcntl.LOCK_UN)
    fp.close()

def file_remove(file, prefix):
    fp = open(postfix_alias_file, 'r+')
    fcntl.lockf(fp, fcntl.LOCK_EX)
    content = fp.read()
    output = re.compile('^%s.*$' % prefix, re.MULTILINE).sub('', content)
    fp.seek(0)
    fp.truncate(0)
    fp.write(output)
    fcntl.lockf(fp, fcntl.LOCK_UN)
    fp.close()


# blocca un indirizzo
def add_block(code):
    file_append(postfix_block_file,
                "fwd-%s@%s      REJECT\n" % (code, domain))

# rimuove un alias
def del_alias(code):
    file_remove(postfix_alias_file, 'fwd-' + code)
    file_remove(postfix_virtual_file, 'fwd-' + code)

# aggiunge un alias
def add_alias(code, alias):
    file_append(postfix_alias_file,
                "fwd-%s      |%s\n" % (code, program_name))
    file_append(postfix_virtual_file,
                "fwd-%s@%s      fwd-%s\n" % (code, domain, code))
    dbh = dbhash.open(database_file, 'c')
    dbh[code] = alias
    dbh.close()

# ricostruisce i db delle mappe per postfix
def rebuild_map(mapfile):
    os.system("/usr/sbin/postmap %s" % mapfile)

# logga un messaggio
def log(msg):
    if log_file:
        try:
            fp = open(log_file,'a')
        except:
            return
        string = "%s %s\n" % (time.ctime(), msg)
        fp.write(string)
        fp.close()
            

# esce con un errore di implementazione
def bailout(msg, code=70):
    log ("FATAL ERROR: " + msg)
    sys.exit(code)


# genera un nuovo codice (che non esista nel database)
def gen_code():
    try:
        dbh = dbhash.open(database_file, 'c')
    except:
        bailout('Error opening database')

    chars = '1234567890abcdefghijklmnopqrstuvwxyz'
    while(1):
        new_code = ''
        for i in range(7):
            idx = random.randint(0,len(chars)-1)
            new_code += chars[idx]
        if not dbh.has_key(new_code):
            break

    dbh.close()
    return new_code




if (len(os.sys.argv) > 1):
    if os.sys.argv[1] == 'add':
        if len(os.sys.argv) == 3:
            code = gen_code()
            add_alias(code, os.sys.argv[2])
            rebuild_map(postfix_alias_file)
            rebuild_map(postfix_virtual_file)
            print 'fwd-%s@%s' % (code, domain)
            sys.exit(0)
    elif os.sys.argv[1] == 'import':
        if len(os.sys.argv) == 4:
            code = os.sys.argv[3]
            alias = os.sys.argv[2]
            add_alias(code, alias)
            rebuild_map(postfix_alias_file)
            rebuild_map(postfix_virtual_file)
            print 'imported fwd-%s@%s -> %s' % (code, domain, alias)
            sys.exit(0)

    print "Usage: monouso add <address>"
    print "Generates a one-use email forward to <address>"
    sys.exit(1)


#log('starting')

# leggi il messaggio da stdin
try:
    message = email.message_from_file(sys.stdin)
except:
    bailout("Invalid message!")


# controlla lo header delivered-to per vedere il destinatario
# dal punto di vista di postfix.
if not message.has_key('Delivered-To'):
    bailout("No Delivered-To: header!")
dest = message['Delivered-To']
matchobj = re.search(r'^fwd-([0-9a-z]{7})', dest)
if  not matchobj:
    # rimbalza il messaggio al volo!
    bailout('%s: not understood' % dest, 67)

if message.has_key('X-MonoLoopDetect'):
    bailout('%s: loop detected!', 67)

# a questo punto qui abbiamo il codice che ci serve
code = matchobj.group(1)

log('code: %s' % code)

# controlliamo il database
try:
    dbh = dbhash.open(database_file, 'c')
except:
    bailout('Error opening database')


exit_status = 67

# ora la vera logica del programma:
# se il valore nel db e' "REJECT" vuol dire che l'indirizzo e' gia' stato
# usato, altrimenti contiene l'indirizzo su cui fare il forward.
if dbh.has_key(code):
    if dbh[code] == 'REJECT':
        log('%s: recipient rejected.' % dest)
    else:
        
        fwd_to = dbh[code]


        # inoltra il messaggio.
        message['X-Notice'] = "L'indirizzo originale a cui e' stato spedito questo messaggio (%s) non e' piu' abilitato." % dest
        message['To'] = fwd_to
        message['Errors-To'] = noreply_address
	del message['Delivered-To']
	message['X-MonoLoopDetect'] = 'ok'

        # parla con sendmail
        try:
            sp = os.popen('%s -oi -r %s %s' % (sendmail_path, noreply_address, fwd_to), 'w')
            sp.write(message.as_string())
            sp.close()
        except:
            bailout('Delivery problem. Can not start sendmail?')

        #print message.as_string()

        # disabilita l'indirizzo.
        dbh[code] = 'REJECT'
        dbh.sync()
        del_alias(code)
        add_block(code)
        rebuild_map(postfix_block_file)
        rebuild_map(postfix_alias_file)
        rebuild_map(postfix_virtual_file)

        # esce con successo.
        exit_status = 0
        log('%s: delivery to %s' % (dest, fwd_to))
        
else:
    log('%s: address unknown.' % dest)


dbh.close()        
sys.exit(exit_status)
