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