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