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