Lot of untested changes.
[confparser-old] / mail.filter
index 003b164..e6ed083 100755 (executable)
@@ -2,7 +2,7 @@
 # -*- coding: iso-8859-1 -*-
 
 #
-# Noname - Mail filter to replace procmail.
+# MailFilter - Mail filter to replace procmail.
 # Copyright (C) 2004  Frédéric Jolliton <frederic@jolliton.com>
 #
 # This program is free software; you can redistribute it and/or modify
 #
 
 #
+# Policy when error are encountered:
+#
+# We backup the mail in a special directory. It will be
+# at the admin discretion to feed it again to this program
+# (or may be script that.)
+#
+
+#
 # TODO:
 #
-#   No todo.
+#   [ ] Define precisely what return code use for each possible case.
 #
 
 import sys
-import getopt
 import os
-from os import EX_OK, EX_NOUSER, EX_TEMPFAIL, EX_DATAERR
 import time
 import email
+import types
 import re
 
+import mfvalidator
+
+from os import EX_USAGE, EX_OK, EX_NOUSER, EX_TEMPFAIL, EX_DATAERR
+# EX_OK           0       ok
+# EX_USAGE        64      command line usage error
+# EX_DATAERR      65      data format error
+# EX_NOINPUT      66      cannot open input
+# EX_NOUSER       67      addressee unknown
+# EX_NOHOST       68      host name unknown
+# EX_UNAVAILABLE  69      service unavailable
+# EX_SOFTWARE     70      internal software error
+# EX_OSERR        71      system error (e.g., can't fork)
+# EX_OSFILE       72      critical OS file missing
+# EX_CANTCREAT    73      can't create (user) output file
+# EX_IOERR        74      input/output error
+# EX_TEMPFAIL     75      temp failure; user is invited to retry
+# EX_PROTOCOL     76      remote error in protocol
+# EX_NOPERM       77      permission denied
+# EX_CONFIG       78      configuration error
+
 #
 # Path to subprocess module. Ideally not needed if subprocess
 # (formerly popen5) is installed into /site-packages/ directory.
@@ -62,6 +89,11 @@ except ImportError :
 #--[ Configuration variables ]------------------------------------------------
 
 #
+# Filename where to put log.
+#
+g_pathLog          = '/var/log/mail.filter.log'
+
+#
 # For which users receiving a mail should we send a UDP packet.
 #
 g_userNotificationFilter = [ 'fred' ]
@@ -98,31 +130,30 @@ g_directoryBackup  = '/var/mail.filter/recovery/'
 #
 g_directoryRules   = '/var/mail.filter/rules/'
 
+#--[ External commands ]------------------------------------------------------
+
+#
+# Path to Cyrus's deliver binary.
+#
+g_pathCyrusDeliver = '/usr/cyrus/bin/deliver'
+
 #
 # Path to spamprobe binary.
 #
 g_pathSpamProbe    = '/usr/bin/spamprobe'
 
+g_pathSpamProbeDb  = '/var/spamprobe/db'
+
 #
 # Path to ClamAV binary.
 #
-# Could point either to clamdscan or clamscan.
+# Could point either to 'clamdscan' or 'clamscan'.
 #
 # The first one is *HIGHLY* recommended since
 # it will use the ClamAV daemon.
 #
 g_pathClamdscan    = '/usr/bin/clamdscan'
 
-#
-# Path to Cyrus's deliver binary.
-#
-g_pathCyrusDeliver = '/usr/cyrus/bin/deliver'
-
-#
-# Filename where to put log.
-#
-g_pathLog          = '/var/log/mail.filter.log'
-
 #--[ Global variables ]-------------------------------------------------------
 
 #
@@ -153,6 +184,36 @@ g_mail = None
 #-----------------------------------------------------------------------------
 
 #
+# check if predicate is True for all items in list 'lst'.
+#
+def all( lst , predicate ) :
+
+       for item in lst :
+               if not predicate( item ) :
+                       return False
+       return True
+
+#
+# check if predicate is True for at least one item in list 'lst'.
+#
+def some( lst , predicate ) :
+
+       for item in lst :
+               if predicate( item ) :
+                       return True
+       return False
+
+#
+# Remove leading and trailing blank, and replace any
+# blank character sequence by one space character.
+#
+def normalizeBlank( s ) :
+
+       return ' '.join( s.split() )
+
+#-----------------------------------------------------------------------------
+
+#
 # Utility function to return traceback as string from most recent
 # exception.
 #
@@ -162,13 +223,16 @@ def getTraceBack() :
        return ''.join( traceback.format_exception( *sys.exc_info() ) )
 
 #
-# Return (rc, output)
+# Return (returnCode, stdout, stderr)
 #
 def pipe( cmd , input ) :
 
-       p = subprocess.Popen( cmd , stdin = subprocess.PIPE , stdout = subprocess.PIPE , stderr = subprocess.PIPE )
-       # much faster than passing 'input' to communicate directly..
+       p = subprocess.Popen( cmd ,
+               stdin = subprocess.PIPE ,
+               stdout = subprocess.PIPE ,
+               stderr = subprocess.PIPE )
        try :
+               # much faster than passing 'input' to communicate directly..
                p.stdin.write( input )
        except IOError :
                pass
@@ -176,13 +240,14 @@ def pipe( cmd , input ) :
        return p.returncode , r[ 0 ] , r[ 1 ]
 
 #
-# Return an ISO-8661 date representation in the UTC
+# Return an ISO-8661 date representation for the UTC
 # timezone.
 #
-def timestamp( t = None ) :
+# timestamp( 0 ) => 1970-01-01T00:00:00Z
+#
+def timestamp() :
 
-       if t == None :
-               t = time.gmtime()
+       t = time.gmtime()
        return '%04d-%02d-%02dT%02d:%02d:%02dZ' % t[ : 6 ]
 
 #
@@ -190,8 +255,6 @@ def timestamp( t = None ) :
 #
 def logMessage( msg ) :
 
-       logMessage.__dict__.setdefault( 'logFile' , None )
-
        if not logMessage.logFile and not g_testMode :
                #
                # If log file is not yet open, try to open it.
@@ -227,6 +290,8 @@ def logMessage( msg ) :
                        sys.stdout.write( line + '\n' )
                        sys.stdout.flush()
 
+logMessage.logFile = None
+
 #
 # Make a backup of the mail (in case it's impossible
 # to store the mail to Cyrus.)
@@ -234,7 +299,7 @@ def logMessage( msg ) :
 def backup( filenamePrefix = None ) :
 
        if g_testMode :
-               logMessage( 'TEST MODE: Backup of the mail.' )
+               logMessage( 'TEST MODE: Backup of the mail requested.' )
                return
 
        try :
@@ -243,10 +308,15 @@ def backup( filenamePrefix = None ) :
                os.makedirs( g_directoryBackup )
        except :
                pass
+
        basename = ''
        if filenamePrefix :
                basename += filenamePrefix + '-'
+       #
+       # Append current unix time as suffix
+       #
        basename += '%.3f' % time.time()
+
        fn = g_directoryBackup + '/' + basename
        try :
                f = open( fn , 'a+' )
@@ -257,6 +327,38 @@ def backup( filenamePrefix = None ) :
        else :
                logMessage( 'Message appended to backup directory as `%s\'.' % basename )
 
+#-----------------------------------------------------------------------------
+
+class Action : pass
+
+class NullAction( Action ) :
+
+       def __repr__( self ) :
+
+               return '<NullAction>'
+
+class FileToFolderAction( Action ) :
+
+       def __init__( self , folder ) :
+
+               self.folder = folder
+
+       def __repr__( self ) :
+
+               return '<FileToFolderAction %r>' % ( self.folder , )
+
+class CustomErrorCodeAction( Action ) :
+
+       def __init__( self , code ) :
+
+               self.code = code
+
+       def __repr__( self ) :
+
+               return '<NullAction %r>' % ( self.code , )
+
+#-----------------------------------------------------------------------------
+
 #
 # Experimental !
 #
@@ -291,10 +393,6 @@ def deliverTo( username , folderName = None ) :
                pseudoFolderName = 'INBOX.' + folderName
                folderName = 'user.' + username + '.' + folderName
 
-       if g_testMode :
-               logMessage( 'TEST MODE: Delivering mail in `%s\'.' % ( folderName , ) )
-               return EX_OK
-
        #
        # Build the command line for running deliver.
        #
@@ -302,6 +400,11 @@ def deliverTo( username , folderName = None ) :
        cmd += [ '-a' , username ]
        cmd += [ '-m' , folderName ]
 
+       if g_testMode :
+               logMessage( 'TEST MODE: Delivering mail in `%s\' requested.' % ( folderName , ) )
+               logMessage( 'TEST MODE: Command: %r.' % cmd )
+               return EX_OK
+
        try :
                rc , stdout , stderr = pipe( cmd , g_mailText )
        except OSError , e :
@@ -332,10 +435,13 @@ def deliverTo( username , folderName = None ) :
                elif m == 'Mailbox does not exist' :
                        rc = EX_NOUSER
                        rcMsg += '->%d' % rc
+               else :
+                       # FIXME: DATAERR ok here ?
+                       rc = EX_DATAERR
                logMessage( 'Refused by Cyrus: [%s] `%s\'.' % ( rcMsg , errorMessage ) )
        return rc
 
-#-----------------------------------------------------------------------------
+#--[ Antivirus ]--------------------------------------------------------------
 
 #
 # Return virus list from the output of ClamAV.
@@ -359,6 +465,12 @@ extractVirusList.reClamdVirus = re.compile( r'^[^:]+: (\S+) FOUND$' )
 def antivirusScan() :
 
        cmd = [ g_pathClamdscan , '-' ]
+
+       if g_testMode :
+               logMessage( 'TEST MODE: Virus scan requested.' )
+               logMessage( 'TEST MODE: Command: %r.' % cmd )
+               return True
+
        rc , stdout , stderr = pipe( cmd , g_mailText )
        output = stderr or ''
        #logMessage( 'clamdscan returned %s' % rc )
@@ -373,6 +485,8 @@ def antivirusScan() :
                logMessage( msg )
        return ok
 
+#--[ Antispam ]---------------------------------------------------------------
+
 #
 # Check for spam.
 #
@@ -383,23 +497,79 @@ def spamScan() :
        if not g_user : return True
 
        cmd = [ g_pathSpamProbe ]
-       cmd += [ '-d' , '/var/spamprobe/db/%s/' % g_user ]
+       cmd += [ '-d' , g_pathSpamProbeDb + '/' + g_user + '/' ]
        cmd += [ 'receive' ]
+
+       if g_testMode :
+               logMessage( 'TEST MODE: Spam scan requested.' )
+               logMessage( 'TEST MODE: Command: %r.' % cmd )
+               return True
+
        rc , stdout , stderr = pipe( cmd , g_mailText )
        r = ( stdout or '' ).split()
        return r[ 0 ] != 'SPAM'
 
 #-----------------------------------------------------------------------------
 
+def errorNameToErrorCode( code ) :
+
+       code = code.lower()
+       if code == 'nouser' :
+               return EX_NOUSER
+       elif code == 'tempfail' :
+               return EX_TEMPFAIL
+       elif code == 'dataerr' :
+               return EX_DATAERR
+       else :
+               try :
+                       return int( code )
+               except :
+                       return 0
+
+#-----------------------------------------------------------------------------
+
+#
+# FIXME: I think it could be better to cache the parsed
+# configuration, and also to cache the result of the validator
+# so that we don't run the test each time this script is run !
+#
 def readUserRules( user ) :
 
        import confparser
+       #
+       # Read the configuration file.
+       #
        try :
                f = open( g_directoryRules + '/' + user + '.mf' )
        except IOError :
                return
-       return confparser.parse( f.read() )
+       conf = f.read()
+
+       #
+       # Parse the configuration.
+       #
+       conf = confparser.parse( conf )
+
+       #
+       # Validate the configuration.
+       #
+       r = mfvalidator.checkConf( conf )
+       if r == None :
+               return conf
+
+       #
+       # Output a error message in case an error is encountered.
+       #
+       exception , node = r
+       meta = node[ 3 ]
+       msg = 'at line %s, column %s' % ( meta[ 0 ] , meta[ 1 ] )
+       logMessage( 'Error in configuration file %s: %s' % ( msg , exception ) )
 
+#-----------------------------------------------------------------------------
+
+#
+# Test a match rule against a particular header.
+#
 def ruleMatch( header , matchType , text ) :
 
        if matchType == 'match' :
@@ -416,192 +586,308 @@ def ruleMatch( header , matchType , text ) :
                logMessage( 'Unknown match type `%s\' from %s\'s user configuration.' % ( matchType , g_user ) )
        return False
                        
+#
+# Test rule 'rule' against the mail.
+#
 def testRule( rule ) :
 
        cmd = rule[ 0 ]
+
        if cmd == 'and' :
-               for subrule in rule[ 2 ] :
-                       if testRule( subrule ) == False :
-                               break
-               else :
-                       return True
-       elif cmd == 'or' :
-               for subrule in rule[ 2 ] :
-                       if testRule( subrule ) == True :
-                               return True
-       elif cmd == 'not' :
-               for subrule in rule[ 2 ] :
-                       if testRule( subrule ) == True :
-                               break
-               else :
-                       return True
-       elif cmd == 'header' :
-               if g_mail :
-                       args = rule[ 1 ]
-                       headerName , matchType , text = args
-                       header = g_mail[ headerName ] or ''
-                       header = header.replace( '\n' , ' ' )
-                       if ruleMatch( header , matchType , text ) :
-                               return True
-       elif cmd == 'broken' :
-               if not g_mail :
-                       return True
-       elif cmd == 'infected' :
-               if g_mail :
-                       return not antivirusScan()
-       elif cmd == 'spam' :
-               if g_mail :
-                       return not spamScan()
-       else :
-               logMessage( 'Unknown rule name `%s\'.' % ( cmd , ) )
-       return False
+               return all( rule[ 2 ] , testRule )
 
-#-----------------------------------------------------------------------------
+       if cmd == 'or' :
+               return some( rule[ 2 ] , testRule )
 
-def errorNameToErrorCode( code ) :
+       if cmd == 'not' :
+               return not some( rule[ 2 ] , testRule )
 
-       code = code.lower()
-       if code == 'nouser' :
-               return EX_NOUSER
-       elif code == 'tempfail' :
-               return EX_TEMPFAIL
-       elif code == 'dataerr' :
-               return EX_DATAERR
-       else :
-               try :
-                       return int( code )
-               except :
-                       return 0
+       #
+       # Matching a header
+       #
+       if cmd == 'header' :
+               if g_mail == None :
+                       return False
+               args = rule[ 1 ]
+               headerName , matchType , text = args
+               headers = map( normalizeBlank , g_mail.get_all( headerName ) or [] )
+               return some( headers , lambda header : ruleMatch( header , matchType , text ) )
+
+       #
+       # Broken mail
+       #
+       if cmd == 'broken' :
+               return g_mail == None
+
+       #
+       # Infected mail
+       #
+       if cmd == 'infected' :
+               return g_mail != None and not antivirusScan()
+
+       #
+       # Spam mail
+       #
+       if cmd == 'spam' :
+               return g_mail != None and not spamScan()
+
+       #
+       # Unknown rule
+       #
+       logMessage( 'Unknown rule name `%s\'.' % ( cmd , ) )
+       return False
+
+#-----------------------------------------------------------------------------
 
 #
 # Find the destination folder for user 'user' according to rules defined for
 # him/her against the current mail.
 #
-# Return either:
+# Return an Action.
 #
-#  ( False , mailBoxName | None )
-#  ( True  , customErrorCode    )
-#
-def getDestFolder( user ) :
+def checkUserRules( user ) :
+
+       action = FileToFolderAction( None )
 
        conf = readUserRules( user )
 
        if not conf :
 
                logMessage( 'No rules defined for user `%s\'.' % user )
-               return ( False , None )
 
-       if not conf[ 2 ] :
+       elif not conf[ 2 ] :
 
                logMessage( 'Empty rules set or syntax error encountered for user `%s\'.' % user )
-               return ( False , None )
 
-       for item in conf[ 2 ] :
-
-               action , args , subs = item[ : 3 ]
+       else :
 
-               if action not in [ 'folder' , 'reject' ] : continue
+               for item in conf[ 2 ] :
+                       actionName , args , subs = item[ : 3 ]
 
-               for rule in subs :
-                       #
-                       # First rule that match is used.
-                       #
-                       if testRule( rule ) :
+                       if some( subs , testRule ) :
+                               if actionName == 'folder' :
+                                       action = FileToFolderAction( args[ 0 ] )
+                               elif actionName == 'reject' :
+                                       action = CustomErrorCodeAction( errorNameToErrorCode( args[ 0 ] ) )
+                               else :
+                                       logMessage( 'Unknown action `%s\'.' % actionName )
                                break
-               else :
-                       continue
 
-               if action == 'folder' :
-                       return ( False , args[ 0 ] )
-
-               if action == 'reject' :
-                       try :
-                               return ( True , errorNameToErrorCode( args[ 0 ] ) )
-                       except :
-                               logMessage( 'Invalid reject code %r.' % args[ 0 ] )
-
-       return ( False , None )
+       return action
 
 #-----------------------------------------------------------------------------
 
 #
-# Dispatch the mail to the correct folder (deduced from rules.)
-#
-# Return either:
-#
-#  ( False , errorCode       )
-#  ( True  , customErrorCode )
+# Read mail from standard input.
 #
-def dispatch() :
+def readMail() :
 
-       isCustomErrorCode , value = getDestFolder( g_user )
+       global g_mailText
 
        #
+       # FIXME: Should we be reading the mail by block, so that
+       # we can at least read and backup a part of the standard input
+       # in case an error occur ? (broken pipe for example)
        #
+       # If error occur, and since we can't backup the mail,
+       # we ask sendmail to retry later.
        #
-       if isCustomErrorCode :
-               return ( True , value )
+       try :
+               g_mailText = sys.stdin.read()
+       except :
+               logMessage( getTraceBack() )
+               sys.exit( EX_TEMPFAIL )
+
+#
+# Check if the mail is bigger than a predefined amount.
+#
+def checkForLargeMail() :
+
+       if len( g_mailText ) > g_maxMailSize :
+               logMessage( 'Message too big (%s bytes). Not filtering it.' % len( g_mailText ) )
+               rc = None
+               if g_user :
+                       rc = deliverTo( g_user )
+                       if rc != EX_OK :
+                               logMessage( 'Unable to deliver it to user `%s\'.' % g_user )
+               if rc != EX_OK :
+                       backup( g_user )
+               sys.exit( EX_OK )
+
+#
+# Check if user was specified of command line.
+#
+def checkForUser() :
 
        #
-       # We got a folder name (or None for default folder.)
+       # No user specified.
        #
-       mbox = value
-       pseudoName = 'INBOX'
-       if mbox : pseudoName += '.' + mbox
+       if not g_user :
+               logMessage( 'No user specified.' )
+               backup()
+               sys.exit( EX_OK )
+
+#
+# Parse the mail using email python standard module.
+#
+def parseMail() :
+
+       global g_mail
 
        #
-       # Try to deliver in the named folder
-       #
-       rc = deliverTo( g_user , mbox )
+       # Parsing the mail.
        #
-       # If we get an error code, then we deliver the mail to default folder,
-       # except if the error was "data error" or if we already tried to deliver
-       # it to default folder.
+       try :
+               g_mail = email.message_from_string( g_mailText , strict = False )
+       except :
+               logMessage( getTraceBack() )
+
+#
+# Dispatch the mail to the correct folder (deduced from rules.)
+#
+# Return an error code.
+#
+def dispatchMail() :
+
+       action = checkUserRules( g_user )
+
        #
-       if rc not in [ EX_OK , EX_DATAERR ] and mbox :
-               logMessage( 'Error delivering to folder %s of user `%s\'.' % ( pseudoName , g_user ) )
-               logMessage( 'Mail will go into default folder.' )
-               rc = deliverTo( g_user )
+       # If custom error code is returned, stop processing
+       # here (mail is not saved.)
        #
-       # Check again.
+       if isinstance( action , CustomErrorCodeAction ) :
+               logMessage( 'Custom exit code is %d.' % r.code )
+               return action.code
+
        #
-       # Here we also handle the case of EX_DATAERR not handled above.
+       # File the mail into the specified folder.
        #
-       if rc != EX_OK :
-               logMessage( 'Error delivering to default folder of user `%s\'.' % ( g_user , ) )
+       if isinstance( action , FileToFolderAction ) :
                #
-               # All errors code different from "data error" are translated to
-               # "no user" error code.
+               # We got a folder name (or None for default folder.)
                #
-               if rc != EX_DATAERR :
-                       rc = EX_NOUSER
+               folder = action.folder
+               pseudoName = 'INBOX'
+               if folder : pseudoName += '.' + folder
+
+               #
+               # Try to deliver in the named folder
+               #
+               rc = deliverTo( g_user , folder )
+               #
+               # If we get an error code, then we deliver the mail to default folder,
+               # except if the error was "data error" or if we already tried to deliver
+               # it to default folder.
+               #
+               if rc not in [ EX_OK , EX_DATAERR ] and folder != None :
+                       logMessage( 'Error delivering to folder %s of user `%s\'.' % ( pseudoName , g_user ) )
+                       logMessage( 'Mail will go into default folder.' )
+                       rc = deliverTo( g_user )
+               #
+               # Check again.
+               #
+               # Here we also handle the case of EX_DATAERR not handled above.
+               #
+               if rc != EX_OK :
+                       logMessage( 'Error delivering to default folder of user `%s\'.' % ( g_user , ) )
+                       #
+                       # Since it's still not ok, backup the mail.
+                       #
+                       backup( g_user )
+                       #
+                       # All errors code different from "data error" are translated to
+                       # "no user" error code.
+                       #
+                       # FIXME: Why?!
+                       #
+                       if rc != EX_DATAERR :
+                               rc = EX_NOUSER
+
+               #
+               # FIXME: !!!!!
+               #
+               rc = EX_OK
+
+               return rc
+       
+       raise Exception( 'Unknown action type' )
+
+#
+#
+#
+def process() :
 
-       return ( False , rc )
+       readMail()
+       try :
+               checkForUser()
+               checkForLargeMail()
+               parseMail()
+               return dispatchMail()
+       except :
+               logMessage( getTraceBack() )
+               return EX_DATAERR
 
 #-----------------------------------------------------------------------------
 
-def usage() :
+def checkConfiguration( filename ) :
 
-       print '''Usage: mail.filter [OPTIONS] [username]
+       mfvalidator.checkFile( filename )
+
+#-----------------------------------------------------------------------------
 
- -h, --help             Print this help.
- -v, --verbose          Verbose mode, output log to stdout.
- -t, --test             Test mode. Don't feed the mail to Cyrus, and don't
-                        write anything into log file.
- -l, --log=FILENAME     Set log filename (default: %s).
- -r, --rules=DIRECTORY  Directory where are located users rules (default %s).
+def usage() :
 
-Report bugs to <fj@tuxee.net>.''' \
-       % ( g_pathLog , g_directoryRules )
+       print '''Usage: mail.filter [OPTIONS] username < EMAIL
+
+ -h, --help                Print this help.
+ -v, --verbose             Verbose mode, output log to stdout.
+ -t, --test                Test mode. Don't feed the mail to Cyrus, don't
+                           do backup, and don't write anything into log file.
+ -l, --log=FILENAME        Set log filename.
+ -r, --rules=DIRECTORY     Directory where are located users rules.
+ -c, --check-config=FILENAME
+                           Check syntax and structure of configuration file
+                           FILENAME.
+'''
+
+       print 'Current paths are:\n'
+       print '    spamprobe  : %s' % g_pathSpamProbe
+       print '    clamd      : %s' % g_pathClamdscan
+       print '    deliver    : %s' % g_pathCyrusDeliver
+       print '    log        : %s' % g_pathLog
+       print
+       print 'Current directories are:\n'
+       print '    spamprobedb: %s' % g_pathSpamProbeDb
+       print '    rules      : %s' % g_directoryRules
+       print '''
+Latest version is available from:
+
+    arch://arch.intra.tuxee.net/2004/mail-filter
+
+Report bugs to <fj@tuxee.net>.'''
 
 def main() :
 
        global g_user, g_mail, g_mailText, g_copyLogToStdout, g_pathLog, g_directoryRules , g_testMode
 
-       options , parameters = \
-               getopt.getopt( sys.argv[ 1 : ] ,
-                       'hvtl:r:' ,
-                       ( 'help' , 'verbose' , 'test' , 'log=' , 'rules=' ) )
+       #--[ Command line ]-------------------------------------------------------
+
+       import getopt
+       try :
+               _getopt = getopt.gnu_getopt
+       except :
+               _getopt = getopt.getopt
+
+       try :
+               options , parameters = \
+                       _getopt( sys.argv[ 1 : ] ,
+                               'hvtl:r:c:' ,
+                               ( 'help' , 'verbose' , 'test' , 'log=' , 'rules=' , 'check-config=' ) )
+       except getopt.GetoptError , e :
+               myName = sys.argv[ 0 ].split( '/' )[ -1 ]
+               print '%s: %s' % ( myName , e[ 0 ] )
+               print 'Try `%s --help\' for more information.' % myName
+               sys.exit( 1 )
 
        for option , argument in options :
                if option in [ '-h' , '--help' ] :
@@ -615,6 +901,9 @@ def main() :
                        g_pathLog = argument
                elif option in [ '-r' , '--rules' ] :
                        g_directoryRules = argument
+               elif option in [ '-c' , '--check-config' ] :
+                       checkConfiguration( argument )
+                       sys.exit( 0 )
 
        #
        # At most one parameter expected.
@@ -624,87 +913,16 @@ def main() :
                # We just log a error message. We continue to proceed
                # to not lost the mail !
                #
-               logMessage( 'Warning: More than one parameter on command line.' )
+               logMessage( 'Warning: Expected only one user name.' )
 
        if parameters :
                g_user = parameters[ 0 ]
 
-       logMessage( 'Running mail.filter for user `%s\'.' % g_user )
-
-       #
-       # FIXME: Should we be reading the mail by block, so that
-       # we can at least read and backup a part of the standard input
-       # in case an error occur ? (broken pipe for example)
-       #
-       try :
-               g_mailText = sys.stdin.read()
-       except :
-               logMessage( getTraceBack() )
-               sys.exit( 1 )
-
-       #
-       # Handling big mail.
-       #
-       if len( g_mailText ) > g_maxMailSize :
-               logMessage( 'Message too big (%s bytes). Not filtering it.' % len( g_mailText ) )
-               ok = False
-               if g_user :
-                       ok = deliverTo( g_user )
-                       logMessage( 'Unable to deliver it to user `%s\'.' % g_user )
-               if not ok :
-                       backup( g_user )
-               sys.exit( 0 )
-
-       #
-       # Parsing the mail.
-       #
-       try :
-               g_mail = email.message_from_string( g_mailText , strict = False )
-       except :
-               logMessage( getTraceBack() )
-
-       #
-       # No user specified.
-       #
-       if not g_user :
-               logMessage( 'No user specified.' )
-               backup( g_user )
-               sys.exit( 0 )
-       
-       #
-       # Return code default to "temporary failure".
-       #
-       isCustomErrorCode , rc = ( False , EX_TEMPFAIL )
-
-       #
-       # Dispatch the mail
-       #
-       try :
-               isCustomErrorCode , rc = dispatch()
-       except :
-               logMessage( getTraceBack() )
-
-       if not isCustomErrorCode :
-
-               if rc not in [ EX_OK , EX_DATAERR ] :
-                       logMessage( 'Rescue mode - Trying to deliver to default folder of user %s.' % g_user )
-                       rc = deliverTo( g_user )
-
-               if rc != EX_OK :
-                       backup( g_user )
-                       if rc != EX_DATAERR :
-                               rc = EX_NOUSER
-                       logMessage( 'Exit code is %d.' % rc )
-               #
-               # FIXME: !!!!!
-               #
-               rc = EX_OK
+       #--[ Core ]---------------------------------------------------------------
 
-       else :
-
-               logMessage( 'Custom exit code is %d.' % rc )
+       logMessage( 'Running mail.filter for user `%s\'.' % g_user )
 
-       sys.exit( rc )
+       return process()
 
 if __name__ == '__main__' :
-       main()
+       sys.exit( main() )