1 # -*- coding: iso-8859-1 -*-
5 # Copyright (C) 2005 Frédéric Jolliton <frederic@jolliton.com>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
30 import cPickle as pickle
37 class Error( Exception ) : pass
40 # Configuration parser check *syntax*.
41 # Validator check *structure*, and eventually some values.
44 #--[ Parser ]-----------------------------------------------------------------
46 def parseString( s ) :
48 if s.startswith( "'" ) :
50 elif s.startswith( '"' ) :
55 def parseConf( p , meta = None ) :
63 t = p.next( 'keyword' )
70 t = p.next( 'string' , '{' , ';' )
71 if t[ 0 ] in [ '{' , ';' ] : break
72 values.append( parseString( t[ 1 ] ) )
79 while not p.snext( '}' ) :
82 return ( kw , values , subNodes , ( y , x , meta ) )
86 # Parse the entire file
88 while not p.snext( 'eot' ) :
92 return ('__root__',None,nodes,None)
94 def parse( doc , filename = None ) :
99 'keyword' : r'[_a-zA-Z][_a-zA-Z0-9]*' ,
100 'string' : r'[_a-zA-Z][_a-zA-Z0-9]*|\'(?:[^\\\']|\\.)*\'' ,
101 'comment' : r'#[^\n]*(?:\n|$)' ,
106 p = basicparser.Parser( tokenMatches , doc )
107 p.ignore( 'blank' , 'comment' )
109 return parseConf( p , filename )
110 except basicparser.ParserError , e :
115 #--[ Validator ]--------------------------------------------------------------
118 # Check mail.filter configuration file structure.
121 from basicvalidator import *
123 #-----------------------------------------------------------------------------
125 class Keyword( Validator ) : pass
127 class Header( Validator ) :
129 allowedMatches = [ 'is' , 'contains' , 'match' ]
131 def check( self , values ) :
133 if len( values ) == 2 :
134 if values[ 1 ] != 'present' :
135 error( 'Invalid keyword %r. Expected \'present\'.' % values[ 1 ] )
136 elif len( values ) == 3 :
137 if values[ 1 ] not in self.allowedMatches :
138 error( '%r is not an allowed match type. Allowed matches type are: %r'
139 % ( values[ 1 ] , self.allowedMatches ) )
141 error( '''header expect 2 or 3 arguments:
142 HEADER-NAME MATCH-TYPE MATCH-ARGUMENT
143 HEADER-NAME MATCH-FLAG''' )
145 #-----------------------------------------------------------------------------
147 class Logical( Validator , MixinNonEmpty ) :
149 def descend( self , item ) :
152 return ruleValidator( item )
154 class Reject( Validator , MixinNonEmpty ) :
156 def descend( self , item ) :
159 return ruleValidator( item )
161 def check( self , values ) :
163 if len( values ) != 1 :
164 error( 'reject CODE { .. }' )
166 class Folder( Validator , MixinNonEmpty ) :
168 def descend( self , item ) :
171 return ruleValidator( item )
173 def check( self , values ) :
175 if len( values ) != 1 :
176 error( 'folder FOLDER-NAME { .. }' )
178 #-----------------------------------------------------------------------------
180 def ruleValidator( item ) :
182 if item in [ 'broken' , 'infected' , 'spam' , 'all' ] :
183 return Keyword( item )
184 elif item in [ 'or' , 'and' , 'not' ] :
185 return Logical( item )
186 elif item == 'header' :
187 return Header( item )
189 error( 'unexpected keyword %r.' % item )
191 #-----------------------------------------------------------------------------
193 class Root( Validator ) :
195 def descend( self , item ) :
197 if item == 'reject' :
198 return Reject( item )
199 elif item == 'folder' :
200 return Folder( item )
202 error( 'unexpected keyword %r.' % item )
204 def values( self , values ) :
206 raise Exception( 'Internal error' )
208 #--[ Read&Write configuration ]-----------------------------------------------
210 def changedDate( filename ) :
213 return os.stat( filename )[ stat.ST_CTIME ]
218 # Return None | ( tree , isValid )
220 def readCachedConfiguration( filename ) :
222 cachedFilename = filename + '.cache'
224 # Check if cached file is older than the source.
226 dateCached = changedDate( cachedFilename )
227 if not dateCached : return
228 dateSource = changedDate( filename )
229 if not dateSource : return
230 if dateCached <= dateSource : return
235 r = pickle.load( open( cachedFilename ) )
240 def writeCachedConfiguration( filename , tree , isValid ) :
243 pickle.dump( ( tree , isValid ) , open( filename + '.cache' , 'w' ) )
247 def readConfiguration( filename ) :
251 # 1. Read from cache file
253 r = readCachedConfiguration( filename )
263 conf = open( filename ).read()
264 conf = parse( conf , filename )
269 basicvalidator.checkConf( conf , Root )
271 # 4. Keep cached result
273 writeCachedConfiguration( filename , conf , isValid )
275 if e[ 0 ] == errno.ENOENT :
277 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
278 except Exception , e :
279 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
282 #--[ Dump configuration tree ]------------------------------------------------
284 def printTreeInner( t , prt = sys.stdout.write , prefix = '' ) :
297 prt( '%s:' % t[ 3 ][ 2 ] )
298 prt( '%s:%s' % ( t[ 3 ][ 0 ] , t[ 3 ][ 1 ] ) )
303 printTreeInner( sub , prt , prefix + ' ' )
309 for sub in t[ 2 ] or [] :
310 printTreeInner( sub )