'''
Condition scanners for sagator
 
(c) 2006-2007 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>

 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_leve())
           })

  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]
