''' Condition scanners for sagator (c) 2006-2007 Jan ONDREJ (SAL) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ''' from avlib import * from match import match_any from cache import cache __all__=['sql_find', 'regexp_find', 'check_level', 'rcpt_in_sql','rcpt_in_txt','rcpt_no_match'] class sql_find(match_any): ''' Recipient email address to index scanner, operating on SQL database. This scanner is an reimplementation of e2i_sql lmtpd() service from older sagator 0.7.2. It is useful only as lmtpd() scanner, because it operates on email recipients. Usage: sql_find(key, dbc, query, scanners) Where: key is an string, which defines which variable to compare. Currently only "recipient" or "sender" can be used here. dbc is an database connection query is an SQL condition to use scanners is an dictionary of scanners, each returned key from database must have coresponding key in this dictionary Example: sql_find( 'recipient', db.sqlite(), "SELECT key FROM userpref WHERE email=%s", { 'AV': b2f(libclam()), 'AS': spamassassind(), '': s2f(libclam())+spamassassind() # default } ) Limitations: It is not possible to include one sql_find() into another. New in version 0.9.0. ''' name='sql_find()' def __init__(self,key,dbc,query,scanners): self.FIND_KEY=key # 'recipient' or 'sender' self.DBC=dbc self.QUERY=query self.SCANDICT=scanners self.reinit() def reinit(self): self.MATCHED={} for scanner in self.SCANDICT.values(): scanner.reinit() def rcpt_signature(self,rcpt): try: if self.FIND_KEY=='sender': find_email=mail.sender else: find_email=rcpt key=self.DBC.query(self.QUERY, [find_email])[0][0] debug.echo(4,'%s: "%s" matched for %s %s' \ % (self.name, key, self.FIND_KEY, find_email)) if self.SCANDICT.has_key(key): debug.echo(4,'%s: Selected scanner: %s' \ % (self.name, self.SCANDICT[key].name)) else: key='' except IndexError, e: key='' self.MATCHED[rcpt]=key return 'sql_find(%s,%s)' % (key, self.SCANDICT[key].rcpt_signature(rcpt)) def scanbuffer(self,buffer,args={}): try: self.scanners=[self.SCANDICT[self.MATCHED[mail.recip[0]]]] return match_any.scanbuffer(self,buffer,args) except IndexError: debug.echo(4, "sql_find(): Default key '' not found!") return 0.0,'',[] class regexp_find(sql_find): ''' Recipient email address to index scanner, operating with regexp. It is useful only as lmtpd() scanner, because it operates on email recipients. Usage: regexp_find(key, scanners) Where: key is an string, which defines which variable to compare. Currently only "recipient" can be used here. scanners is an dictionary of scanners, each returned key from database must have coresponding key in this dictionary Example: regexp_find( 'recipient', { '@somedomain.com$': b2f(libclam()), '@anotherdomain.sk$': spamassassind(), '': s2f(libclam())+spamassassind() } ) Limitations: It is not possible to include one regexp_find() into another. New in version 0.9.0. ''' name='regexp_find()' def __init__(self,dbc,query,scanners): self.DBC=dbc self.QUERY=query self.SCANDICT=scanners # compile regexps, ignore default ('') here self.REDICT=dict([[re.compile(key, re.I), key] for key in scanners.keys() if key!='']) # reinit self.reinit() def rcpt_signature(self,rcpt): for reg,key in self.REDICT.values(): if reg.search(rcpt): self.MATCHED[rcpt]='key' debug.echo(4,'%s: "%s" matched for %s' % (self.name, key, rcpt)) return key self.MATCHED[rcpt]='' return '' class check_level(match_any): ''' Select scanner based on tested scanner return status. Usage: check_level(tested_scanner, { (min,max): scanner, (min,max): scanner, ... }) or: check_level() Where: tested_scanner is a scanner, which return level will be tested min is an integer, minimal level for this scanner max is an integer, maximal level for this scanner This scanner with no arguments will return previously saved status. Evaluation function is: min <= LEVEL < max . When no range is found, cached reply will be returned without changes. Example: check_level(spamassassind(), { (1.0, 5.0): deliver( modify_subject('[SPAM:%L]', check_level() ) ), (5.0, 99999.0): drop('.', check_level()) }) New in version 0.9.0. ''' name='check_level()' def __init__(self, tested_scanner=None, scanners={}): if type(tested_scanner) == type(None): match_any.__init__(self, [cache(self.name)] + scanners.values() ) else: match_any.__init__(self, [cache(self.name, tested_scanner)] + scanners.values() ) self.SCANDICT=scanners def scanbuffer(self, buffer, args={}): if not self.SCANDICT: return self.scanners[0].scanbuffer(buffer, args) level, detected, virlist = self.scanners[0].scanbuffer(buffer, args) for (lmin,lmax),scanner in self.SCANDICT.items(): if lmin <= level < lmax: debug.echo(5, "check_level(): %f<=%f<%f: %s" \ % (lmin, level, lmax, scanner.name)) return scanner.scanbuffer(buffer, args) return level, detected, virlist class rcpt_in_sql(ascanner): ''' Retun virus if recipient matches against a database row. This scanner is useful as an condition for other scanners. You can use SQL database to assingn required return value. Table description: CREATE TABLE signatures ( prio int, email text, ckey varchar(16) ); Priority is used when more than one rows will match. By default this SELECT is used to find proper line: SELECT email FROM signatures WHERE ckey='__name__' AND '__rcpt__' LIKE email ORDER BY prio The "__name__" is replaced by key from scanner definition and the "__rcpt__" is replaced by recipient's email address. Usage: rcpt_in_sql(dbc, key) Where: dbc is an database connection key is a scanner name defined in SQL table Example: rcpt_in_sql(db.sqlite(), 'AntiSpam') & spamassassind() Obsolete since 0.9.0, use sql_find() instead. ''' name='rcpt_in_sql()' def __init__(self,dbc,key): self.DBC=dbc self.QUERY="SELECT email FROM signatures WHERE " \ "ckey=%(key)s AND %(rcpt)s LIKE email ORDER BY prio" self.KEY=key self.reinit() def reinit(self): self.MATCHED={} def rcpt_signature(self,rcpt): try: match=self.DBC.query( self.QUERY, {'key':self.KEY, 'rcpt':rcpt} )[0][0] self.MATCHED[rcpt]=match globals.RCPT_MATCH[self.KEY]=match debug.echo(4,'%s: %s matched for %s [%s]' \ % (self.name,self.KEY,rcpt,match)) return self.KEY except IndexError: self.MATCHED[rcpt]='' return '' def scanbuffer(self,buffer,args={}): try: rcpt=mail.recip[0] if self.MATCHED[rcpt]: # hide virus name return 1.0,'',[self.MATCHED[rcpt]] except IndexError: pass return 0.0,'',[] class rcpt_in_txt(rcpt_in_sql): ''' Retun virus if recipient matches against an text file row. This scanner is useful as an condition for other scanners. You can use an plain text file to assingn required return value. Each row in this file defines an regular expression searched against recipient's email address. Priority is not required, because it is defined by row. Usage: rcpt_in_txt(filename, flags=re.IGNORECASE) Where: filename is configuration full filename flags are flags for regular expression. By default ignore case. Example: rcpt_in_txt('/var/lib/sagator/antispam.conf') & spamassassind() Obsolete since 0.9.0, use regexp_find() instead. ''' name='rcpt_in_txt()' def __init__(self,filename,flags=re.I): self.FILENAME=filename self.STAT=0 self.FLAGS=flags def reinit(self): self.MATCHED={} fn=safe.fn(self.FILENAME) # reload file if it has been changed if os.stat(fn).st_mtime!=self.STAT: self.STAT=os.stat(fn).st_mtime self.DATA=[ (line.rstrip(),re.compile(line.rstrip(),self.FLAGS)) for line in open(fn,'rt').readlines() ] debug.echo(3,'%s: reloaded %d lines from %s' \ % (self.name,len(self.DATA),self.FILENAME)) def rcpt_signature(self,rcpt): for line,reg in self.DATA: if reg.search(rcpt): self.MATCHED[rcpt]=line globals.RCPT_MATCH[self.FILENAME]=line debug.echo(4,'%s: %s matched for %s [%s]' \ % (self.name,self.FILENAME,rcpt,line)) return self.FILENAME self.MATCHED[rcpt]='' return '' class rcpt_no_match(ascanner): ''' Return virus is no rcpt_in_... match before. When no one rcpt_in_sql() or rcpt_in_txt() scanner matches, this scanner return's a match. It is useful when you need to use default scanner, when no one is defined in configuration. Usage: rcpt_no_match(key) Where: key is a scanner name defined in SQL table Example: rcpt_no_match('AntiSpam') & spamassassind() Obsolete since 0.9.0, use sql_find() or regexp_find() instead. ''' name='rcpt_no_match()' def __init__(self,key): self.KEY=key self.MATCHED=False def rcpt_signature(self,rcpt): if globals.RCPT_MATCH.has_key(self.KEY): self.MATCHED=True return self.KEY else: debug.echo(4,'%s: %s not mathed before' % (self.name,rcpt)) self.MATCHED=False return '' def scanbuffer(self,buffer,args={}): if self.MATCHED: return 0.0,'',[] else: # hide virus name return 1.0,'',['%s not matched' % self.KEY]