Changes related to logs and error handling.
[confparser-old] / mail.filter
1 #!/usr/bin/python
2 # -*- coding: iso-8859-1 -*-
3
4 #
5 # MailFilter - Mail filter to replace procmail.
6 # Copyright (C) 2004  Frédéric Jolliton <frederic@jolliton.com>
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21 #
22
23 #
24 # Policy when error are encountered:
25 #
26 # We backup the mail in a special directory. It will be
27 # at the admin discretion to feed it again to this program
28 # (or may be script that.)
29 #
30
31 #
32 # TODO:
33 #
34 #   [ ] Define precisely what return code use for each possible case.
35 #
36
37 import sys
38 import os
39 import time
40 import email
41 import types
42 import re
43
44 import confparser
45
46 from os import EX_USAGE, EX_OK, EX_NOUSER, EX_TEMPFAIL, EX_DATAERR
47 # EX_OK           0       ok
48 # EX_USAGE        64      command line usage error
49 # EX_DATAERR      65      data format error
50 # EX_NOINPUT      66      cannot open input
51 # EX_NOUSER       67      addressee unknown
52 # EX_NOHOST       68      host name unknown
53 # EX_UNAVAILABLE  69      service unavailable
54 # EX_SOFTWARE     70      internal software error
55 # EX_OSERR        71      system error (e.g., can't fork)
56 # EX_OSFILE       72      critical OS file missing
57 # EX_CANTCREAT    73      can't create (user) output file
58 # EX_IOERR        74      input/output error
59 # EX_TEMPFAIL     75      temp failure; user is invited to retry
60 # EX_PROTOCOL     76      remote error in protocol
61 # EX_NOPERM       77      permission denied
62 # EX_CONFIG       78      configuration error
63
64 #
65 # Path to subprocess module. Ideally not needed if subprocess
66 # (formerly popen5) is installed into /site-packages/ directory.
67 #
68 #sys.path.insert( 0 , '/usr/local/lib/python/' )
69
70 #
71 # subprocess (formerly popen5) - See PEP 324
72 # http://www.lysator.liu.se/~astrand/popen5/
73 #
74 # >>> cat = subprocess.Popen( 'cat' , stdin = subprocess.PIPE , stdout = subprocess.PIPE )
75 # >>> cat.communicate( 'bla' )
76 # ('bla', None)
77 # >>> cat.returncode
78 # 0
79 #
80 try :
81         import subprocess
82 except ImportError :
83         try :
84                 import popen5 as subprocess
85         except ImportError :
86                 print 'Please install subprocess module.'
87                 print 'See http://www.lysator.liu.se/~astrand/popen5/.'
88                 sys.exit( 1 )
89
90 #--[ Configuration variables ]------------------------------------------------
91
92 #
93 # Filename where to put log.
94 #
95 g_pathLog          = '/var/log/mail.filter.log'
96
97 #
98 # For which users receiving a mail should we send a UDP packet.
99 #
100 g_userNotificationFilter = [ 'fred' ]
101
102 #
103 # For which IP address should we send the notification.
104 # (Can include broadcast address.)
105 #
106 g_notificationAddresses = [ '192.168.1.255' ]
107
108 #
109 # On which port should we send the notification.
110 #
111 g_notificationPort = 23978
112
113 #
114 # Max mail size to be processed by this script.
115 #
116 # Larger mail are just not filtered.
117 #
118 g_maxMailSize = 2 * 1024 * 1024
119
120 #
121 # Where to save copy of mail in case of error.
122 #
123 g_directoryBackup  = '/var/mail.filter/recovery/'
124
125 #
126 # Where to find rules about each user.
127
128 # Filename for user 'joe' will be named 'joe.mf' in that
129 # directory. If the file doesn't exist, no filtering is
130 # done (not even spam/virus filtering.)
131 #
132 g_directoryRules   = '/var/mail.filter/rules/'
133
134 #--[ External commands ]------------------------------------------------------
135
136 #
137 # Path to Cyrus's deliver binary.
138 #
139 g_pathCyrusDeliver = '/usr/cyrus/bin/deliver'
140
141 #
142 # Path to spamprobe binary.
143 #
144 g_pathSpamProbe    = '/usr/bin/spamprobe'
145
146 g_pathSpamProbeDb  = '/var/spamprobe/db'
147
148 #
149 # Path to ClamAV binary.
150 #
151 # Could point either to 'clamdscan' or 'clamscan'.
152 #
153 # The first one is *HIGHLY* recommended since
154 # it will use the ClamAV daemon.
155 #
156 g_pathClamdscan    = '/usr/bin/clamdscan'
157
158 #--[ Global variables ]-------------------------------------------------------
159
160 #
161 # Should the log be also printed on stdout ?
162 #
163 g_copyLogToStdout = False
164
165 #
166 # Don't actually feed the mail to Cyrus.
167 #
168 g_testMode = False
169
170 #
171 # The user name of the recipient.
172 #
173 g_user = None
174
175 #
176 # The current mail as string (as read from stdin.)
177 #
178 g_mailText = None
179
180 #
181 # The current mail as email.Message.Message object.
182 #
183 g_mail = None
184
185 #-----------------------------------------------------------------------------
186
187 #
188 # check if predicate is True for all items in list 'lst'.
189 #
190 def all( lst , predicate ) :
191
192         for item in lst :
193                 if not predicate( item ) :
194                         return False
195         return True
196
197 #
198 # check if predicate is True for at least one item in list 'lst'.
199 #
200 def some( lst , predicate ) :
201
202         for item in lst :
203                 if predicate( item ) :
204                         return True
205         return False
206
207 #
208 # Remove leading and trailing blank, and replace any
209 # blank character sequence by one space character.
210 #
211 def normalizeBlank( s ) :
212
213         return ' '.join( s.split() )
214
215 #-----------------------------------------------------------------------------
216
217 #
218 # Utility function to return traceback as string from most recent
219 # exception.
220 #
221 def getTraceBack() : 
222  
223         import traceback, sys 
224         return ''.join( traceback.format_exception( *sys.exc_info() ) )
225
226 #
227 # Return (returnCode, stdout, stderr)
228 #
229 def pipe( cmd , input ) :
230
231         p = subprocess.Popen( cmd ,
232                 stdin = subprocess.PIPE ,
233                 stdout = subprocess.PIPE ,
234                 stderr = subprocess.PIPE )
235         try :
236                 # much faster than passing 'input' to communicate directly..
237                 p.stdin.write( input )
238         except IOError :
239                 pass
240         r = p.communicate()
241         return p.returncode , r[ 0 ] , r[ 1 ]
242
243 #
244 # Return an ISO-8661 date representation for the UTC
245 # timezone.
246 #
247 # timestamp( 0 ) => 1970-01-01T00:00:00Z
248 #
249 def timestamp() :
250
251         t = time.gmtime()
252         return '%04d-%02d-%02dT%02d:%02d:%02dZ' % t[ : 6 ]
253
254 #
255 # Log message 'msg'.
256 #
257 def logMessage( msg ) :
258
259         if not logMessage.logFile and not g_testMode :
260                 #
261                 # If log file is not yet open, try to open it.
262                 #
263                 try :
264                         logMessage.logFile = open( g_pathLog , 'a+' )
265                 except :
266                         if not g_copyLogToStdout :
267                                 return
268
269         msg = msg.splitlines()
270         prefix = timestamp() + ' [%s] ' % os.getpid()
271
272         #
273         # Output to log file.
274         #
275         if logMessage.logFile :
276
277                 for line in msg :
278                         line = prefix + line
279                         try :
280                                 logMessage.logFile.write( line + '\n' )
281                                 logMessage.logFile.flush()
282                         except :
283                                 pass
284
285         #
286         # Output to standard output.
287         #
288         if g_copyLogToStdout :
289
290                 for line in msg :
291                         line = prefix + line
292                         sys.stdout.write( line + '\n' )
293                         sys.stdout.flush()
294
295 logMessage.logFile = None
296
297 #
298 # Make a backup of the mail (in case it's impossible
299 # to store the mail to Cyrus.)
300 #
301 def backup( filenamePrefix = None ) :
302
303         if g_testMode :
304                 logMessage( 'TEST MODE: Backup of the mail requested.' )
305                 return
306
307         try :
308                 # Ensure directory exist
309                 import os
310                 os.makedirs( g_directoryBackup )
311         except :
312                 pass
313
314         basename = ''
315         if filenamePrefix :
316                 basename += filenamePrefix + '-'
317         #
318         # Append current unix time as suffix
319         #
320         basename += '%.3f' % time.time()
321
322         fn = g_directoryBackup + '/' + basename
323         try :
324                 f = open( fn , 'a+' )
325                 f.write( g_mailText )
326                 f.close()
327         except :
328                 logMessage( 'PANIC: Unable to write backup to %s.' % fn )
329         else :
330                 logMessage( 'Message appended to backup directory as `%s\'.' % basename )
331
332 #-----------------------------------------------------------------------------
333
334 class Action : pass
335
336 class NullAction( Action ) :
337
338         def __repr__( self ) :
339
340                 return '<NullAction>'
341
342 class FileToFolderAction( Action ) :
343
344         def __init__( self , folder ) :
345
346                 self.folder = folder
347
348         def __repr__( self ) :
349
350                 return '<FileToFolderAction %r>' % ( self.folder , )
351
352 class CustomErrorCodeAction( Action ) :
353
354         def __init__( self , code ) :
355
356                 self.code = code
357
358         def __repr__( self ) :
359
360                 return '<NullAction %r>' % ( self.code , )
361
362 #-----------------------------------------------------------------------------
363
364 #
365 # Experimental !
366 #
367 # Packet payload contains:
368 #
369 # <username> + char( 0 ) + <foldername> + char( 0 )
370 #
371 def notifyDeliver( user , folder ) :
372
373         if user not in g_userNotificationFilter :
374                 return
375         try :
376                 import socket
377                 s = socket.socket( socket.AF_INET , socket.SOCK_DGRAM )
378                 msg = user + chr( 0 ) + folder + chr( 0 )
379                 for address in g_notificationAddresses :
380                         s.sendto( msg , ( address , g_notificationPort ) )
381         except :
382                 pass
383
384 #
385 # Deliver a mail to Cyrus for user 'username' in
386 # folder 'folderName' (or default folder if not
387 # specified.)
388 #
389 def deliverTo( username , folderName = None ) :
390
391         if not folderName :
392                 pseudoFolderName = 'INBOX'
393                 folderName = 'user.' + username
394         else :
395                 pseudoFolderName = 'INBOX.' + folderName
396                 folderName = 'user.' + username + '.' + folderName
397
398         #
399         # Build the command line for running deliver.
400         #
401         cmd = [ g_pathCyrusDeliver ]
402         cmd += [ '-a' , username ]
403         cmd += [ '-m' , folderName ]
404
405         if g_testMode :
406                 logMessage( 'TEST MODE: Delivering mail in `%s\' requested.' % ( folderName , ) )
407                 logMessage( 'TEST MODE: Command: %r.' % cmd )
408                 return EX_OK
409
410         try :
411                 rc , stdout , stderr = pipe( cmd , g_mailText )
412         except OSError , e :
413                 logMessage( 'Error running `%s\': %s.' % ( cmd[ 0 ] , e[ 1 ] ) )
414                 return EX_TEMPFAIL
415
416         if rc == EX_OK :
417                 logMessage( 'Message delivered in folder `%s\'.' % folderName )
418                 notifyDeliver( username , pseudoFolderName )
419         else :
420                 errorMessage = stdout.rstrip()
421                 #
422                 # Extract raw error message
423                 #
424                 # Example of output:
425                 #
426                 #   +user.fred: Message contains invalid header
427                 #
428                 m = errorMessage.split( ': ' , 1 )
429                 if len( m ) == 2 :
430                         m = m[ 1 ]
431                 else :
432                         m = None
433                 rcMsg = '%d' % rc
434                 if m == 'Message contains invalid header' :
435                         rc = EX_DATAERR
436                         rcMsg += '->%d' % rc
437                 elif m == 'Mailbox does not exist' :
438                         rc = EX_NOUSER
439                         rcMsg += '->%d' % rc
440                 else :
441                         # FIXME: DATAERR ok here ?
442                         rc = EX_DATAERR
443                 logMessage( 'Refused by Cyrus: [%s] `%s\'.' % ( rcMsg , errorMessage ) )
444         return rc
445
446 #--[ Antivirus ]--------------------------------------------------------------
447
448 #
449 # Return virus list from the output of ClamAV.
450 #
451 def extractVirusList( clamdOutput ) :
452
453         res = []
454         for line in clamdOutput.splitlines() :
455                 r = extractVirusList.reClamdVirus.search( line.rstrip() )
456                 if r == None : continue
457                 res.append( r.group( 1 ) )
458         return res
459
460 extractVirusList.reClamdVirus = re.compile( r'^[^:]+: (\S+) FOUND$' )
461
462 #
463 # Check for virus.
464 #
465 # Return True if mail is clean.
466 #
467 def antivirusScan() :
468
469         cmd = [ g_pathClamdscan , '-' ]
470
471         if g_testMode :
472                 logMessage( 'TEST MODE: Virus scan requested.' )
473                 logMessage( 'TEST MODE: Command: %r.' % cmd )
474                 return True
475
476         rc , stdout , stderr = pipe( cmd , g_mailText )
477         output = stderr or ''
478         #logMessage( 'clamdscan returned %s' % rc )
479         if rc == 2 :
480                 raise 'Unable to scan for viruses (%s)' % cmd
481         ok = not rc
482         if not ok :
483                 msg = 'Virus found.'
484                 viruses = extractVirusList( output )
485                 if viruses :
486                         msg += ' [%s]' % ' '.join( viruses )
487                 logMessage( msg )
488         return ok
489
490 #--[ Antispam ]---------------------------------------------------------------
491
492 #
493 # Check for spam.
494 #
495 # Return True if mail is correct.
496 #
497 def spamScan() :
498
499         if not g_user : return True
500
501         cmd = [ g_pathSpamProbe ]
502         cmd += [ '-d' , g_pathSpamProbeDb + '/' + g_user + '/' ]
503         cmd += [ 'receive' ]
504
505         if g_testMode :
506                 logMessage( 'TEST MODE: Spam scan requested.' )
507                 logMessage( 'TEST MODE: Command: %r.' % cmd )
508                 return True
509
510         rc , stdout , stderr = pipe( cmd , g_mailText )
511         r = ( stdout or '' ).split()
512         return r[ 0 ] != 'SPAM'
513
514 #-----------------------------------------------------------------------------
515
516 def errorNameToErrorCode( code ) :
517
518         code = code.lower()
519         if code == 'nouser' :
520                 return EX_NOUSER
521         elif code == 'tempfail' :
522                 return EX_TEMPFAIL
523         elif code == 'dataerr' :
524                 return EX_DATAERR
525         else :
526                 try :
527                         return int( code )
528                 except :
529                         return 0
530
531 #-----------------------------------------------------------------------------
532
533 #
534 # FIXME: I think it could be better to cache the parsed
535 # configuration, and also to cache the result of the validator
536 # so that we don't run the test each time this script is run !
537 #
538 def readUserRules( user ) :
539
540         filename = g_directoryRules + '/' + user + '.mf'
541
542         #
543         # Read the configuration.
544         #
545         try :
546                 return confparser.readConfiguration( filename )
547         except OSError , e :
548                 pass
549         except Exception , e :
550                 logMessage( 'Error in file %r. See option -c to check this file.' % ( filename , ) )
551
552 #-----------------------------------------------------------------------------
553
554 #
555 # Test a match rule against a particular header.
556 #
557 def ruleMatch( header , matchType , text ) :
558
559         if matchType == 'match' :
560                 try :
561                         return re.search( text , header , re.I ) != None
562                 except :
563                         logMessage( 'Error with regex `%s\' from %s\'s user configuration.' % ( text , g_user ) )
564                         return False
565         elif matchType == 'is' :
566                 return header.strip().lower() == text.strip().lower()
567         elif matchType == 'contains' :
568                 return header.lower().find( text.strip().lower() ) != -1
569         else :
570                 logMessage( 'Unknown match type `%s\' from %s\'s user configuration.' % ( matchType , g_user ) )
571         return False
572                         
573 #
574 # Test rule 'rule' against the mail.
575 #
576 def testRule( rule ) :
577
578         cmd = rule[ 0 ]
579
580         if cmd == 'and' :
581                 return all( rule[ 2 ] , testRule )
582
583         if cmd == 'or' :
584                 return some( rule[ 2 ] , testRule )
585
586         if cmd == 'not' :
587                 return not some( rule[ 2 ] , testRule )
588
589         #
590         # Matching a header
591         #
592         if cmd == 'header' :
593                 if g_mail == None :
594                         return False
595                 args = rule[ 1 ]
596                 headerName , matchType , text = args
597                 headers = map( normalizeBlank , g_mail.get_all( headerName ) or [] )
598                 return some( headers , lambda header : ruleMatch( header , matchType , text ) )
599
600         #
601         # Broken mail
602         #
603         if cmd == 'broken' :
604                 return g_mail == None
605
606         #
607         # Infected mail
608         #
609         if cmd == 'infected' :
610                 return g_mail != None and not antivirusScan()
611
612         #
613         # Spam mail
614         #
615         if cmd == 'spam' :
616                 return g_mail != None and not spamScan()
617
618         #
619         # Unknown rule
620         #
621         logMessage( 'Unknown rule name `%s\'.' % ( cmd , ) )
622         return False
623
624 #-----------------------------------------------------------------------------
625
626 #
627 # Find the destination folder for user 'user' according to rules defined for
628 # him/her against the current mail.
629 #
630 # Return an Action.
631 #
632 def checkUserRules( user ) :
633
634         action = FileToFolderAction( None )
635
636         conf = readUserRules( user )
637
638         if not conf :
639
640                 logMessage( 'No rules defined for user `%s\'.' % user )
641
642         elif not conf[ 2 ] :
643
644                 logMessage( 'Empty rules set or syntax error encountered for user `%s\'.' % user )
645
646         else :
647
648                 for item in conf[ 2 ] :
649                         actionName , args , subs = item[ : 3 ]
650
651                         if some( subs , testRule ) :
652                                 if actionName == 'folder' :
653                                         action = FileToFolderAction( args[ 0 ] )
654                                 elif actionName == 'reject' :
655                                         action = CustomErrorCodeAction( errorNameToErrorCode( args[ 0 ] ) )
656                                 else :
657                                         logMessage( 'Unknown action `%s\'.' % actionName )
658                                 break
659
660         return action
661
662 #-----------------------------------------------------------------------------
663
664 #
665 # Read mail from standard input.
666 #
667 def readMail() :
668
669         global g_mailText
670
671         #
672         # FIXME: Should we be reading the mail by block, so that
673         # we can at least read and backup a part of the standard input
674         # in case an error occur ? (broken pipe for example)
675         #
676         # If error occur, and since we can't backup the mail,
677         # we ask sendmail to retry later.
678         #
679         try :
680                 g_mailText = sys.stdin.read()
681         except :
682                 logMessage( getTraceBack() )
683                 sys.exit( EX_TEMPFAIL )
684
685 #
686 # Check if the mail is bigger than a predefined amount.
687 #
688 def checkForLargeMail() :
689
690         if len( g_mailText ) > g_maxMailSize :
691                 logMessage( 'Message too big (%s bytes). Not filtering it.' % len( g_mailText ) )
692                 rc = None
693                 if g_user :
694                         rc = deliverTo( g_user )
695                         if rc != EX_OK :
696                                 logMessage( 'Unable to deliver it to user `%s\'.' % g_user )
697                 if rc != EX_OK :
698                         backup( g_user )
699                 sys.exit( EX_OK )
700
701 #
702 # Check if user was specified of command line.
703 #
704 def checkForUser() :
705
706         #
707         # No user specified.
708         #
709         if not g_user :
710                 logMessage( 'No user specified.' )
711                 backup()
712                 sys.exit( EX_OK )
713
714 #
715 # Parse the mail using email python standard module.
716 #
717 def parseMail() :
718
719         global g_mail
720
721         #
722         # Parsing the mail.
723         #
724         try :
725                 g_mail = email.message_from_string( g_mailText , strict = False )
726         except :
727                 logMessage( getTraceBack() )
728
729 #
730 # Dispatch the mail to the correct folder (deduced from rules.)
731 #
732 # Return an error code.
733 #
734 def dispatchMail() :
735
736         action = checkUserRules( g_user )
737
738         #
739         # If custom error code is returned, stop processing
740         # here (mail is not saved.)
741         #
742         if isinstance( action , CustomErrorCodeAction ) :
743                 logMessage( 'Custom exit code is %d.' % r.code )
744                 return action.code
745
746         #
747         # File the mail into the specified folder.
748         #
749         if isinstance( action , FileToFolderAction ) :
750                 #
751                 # We got a folder name (or None for default folder.)
752                 #
753                 folder = action.folder
754                 pseudoName = 'INBOX'
755                 if folder : pseudoName += '.' + folder
756
757                 #
758                 # Try to deliver in the named folder
759                 #
760                 rc = deliverTo( g_user , folder )
761                 #
762                 # If we get an error code, then we deliver the mail to default folder,
763                 # except if the error was "data error" or if we already tried to deliver
764                 # it to default folder.
765                 #
766                 if rc not in [ EX_OK , EX_DATAERR ] and folder != None :
767                         logMessage( 'Error delivering to folder %s of user `%s\'.' % ( pseudoName , g_user ) )
768                         logMessage( 'Mail will go into default folder.' )
769                         rc = deliverTo( g_user )
770                 #
771                 # Check again.
772                 #
773                 # Here we also handle the case of EX_DATAERR not handled above.
774                 #
775                 if rc != EX_OK :
776                         logMessage( 'Error delivering to default folder of user `%s\'.' % ( g_user , ) )
777                         #
778                         # Since it's still not ok, backup the mail.
779                         #
780                         backup( g_user )
781                         #
782                         # All errors code different from "data error" are translated to
783                         # "no user" error code.
784                         #
785                         # FIXME: Why?!
786                         #
787                         if rc != EX_DATAERR :
788                                 rc = EX_NOUSER
789
790                 #
791                 # FIXME: !!!!!
792                 #
793                 rc = EX_OK
794
795                 return rc
796         
797         raise Exception( 'Unknown action type' )
798
799 #
800 #
801 #
802 def process() :
803
804         readMail()
805         try :
806                 checkForUser()
807                 checkForLargeMail()
808                 parseMail()
809                 return dispatchMail()
810         except :
811                 logMessage( getTraceBack() )
812                 return EX_DATAERR
813
814 #-----------------------------------------------------------------------------
815
816 def checkConfiguration( filename ) :
817
818         try :
819                 confparser.readConfiguration( filename )
820         except Exception , e :
821                 print e
822
823 #-----------------------------------------------------------------------------
824
825 def usage() :
826
827         print '''Usage: mail.filter [OPTIONS] username < EMAIL
828
829  -h, --help                Print this help.
830  -v, --verbose             Verbose mode, output log to stdout.
831  -t, --test                Test mode. Don't feed the mail to Cyrus, don't
832                            do backup, and don't write anything into log file.
833  -l, --log=FILENAME        Set log filename.
834  -r, --rules=DIRECTORY     Directory where are located users rules.
835  -c, --check-config=FILENAME
836                            Check syntax and structure of configuration file
837                            FILENAME.
838 '''
839
840         print 'Current paths are:\n'
841         print '    spamprobe  : %s' % g_pathSpamProbe
842         print '    clamd      : %s' % g_pathClamdscan
843         print '    deliver    : %s' % g_pathCyrusDeliver
844         print '    log        : %s' % g_pathLog
845         print
846         print 'Current directories are:\n'
847         print '    spamprobedb: %s' % g_pathSpamProbeDb
848         print '    rules      : %s' % g_directoryRules
849         print '''
850 Latest version is available from:
851
852     arch://arch.intra.tuxee.net/2004/mail-filter
853
854 Report bugs to <fj@tuxee.net>.'''
855
856 def main() :
857
858         global g_user, g_mail, g_mailText, g_copyLogToStdout, g_pathLog, g_directoryRules , g_testMode
859
860         #--[ Command line ]-------------------------------------------------------
861
862         import getopt
863         try :
864                 _getopt = getopt.gnu_getopt
865         except :
866                 _getopt = getopt.getopt
867
868         try :
869                 options , parameters = \
870                         _getopt( sys.argv[ 1 : ] ,
871                                 'hvtl:r:c:' ,
872                                 ( 'help' , 'verbose' , 'test' , 'log=' , 'rules=' , 'check-config=' ) )
873         except getopt.GetoptError , e :
874                 myName = sys.argv[ 0 ].split( '/' )[ -1 ]
875                 print '%s: %s' % ( myName , e[ 0 ] )
876                 print 'Try `%s --help\' for more information.' % myName
877                 sys.exit( 1 )
878
879         for option , argument in options :
880                 if option in [ '-h' , '--help' ] :
881                         usage()
882                         sys.exit( 0 )
883                 elif option in [ '-v' , '--verbose' ] :
884                         g_copyLogToStdout = True
885                 elif option in [ '-t' , '--test' ] :
886                         g_testMode = True
887                 elif option in [ '-l' , '--log' ] :
888                         g_pathLog = argument
889                 elif option in [ '-r' , '--rules' ] :
890                         g_directoryRules = argument
891                 elif option in [ '-c' , '--check-config' ] :
892                         checkConfiguration( argument )
893                         sys.exit( 0 )
894
895         #
896         # At most one parameter expected.
897         #
898         if len( parameters ) > 1 :
899                 #
900                 # We just log a error message. We continue to proceed
901                 # to not lost the mail !
902                 #
903                 logMessage( 'Warning: Expected only one user name.' )
904
905         if parameters :
906                 g_user = parameters[ 0 ]
907
908         #--[ Core ]---------------------------------------------------------------
909
910         logMessage( 'Running mail.filter for user `%s\'.' % g_user )
911
912         return process()
913
914 if __name__ == '__main__' :
915         sys.exit( main() )