'''
Antivir library for Sagator

(c) 2003-2006 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.
    
'''

import os,sys,re,time,random,gettext
import traceback,signal,resource,struct,socket
import pwd,grp
from cStringIO import StringIO
from email import message_from_string
import version

KB=1024
MB=KB*KB
MINUTE=60
HOUR=MINUTE*60
DAY=HOUR*24
WEEK=DAY*7
MONTH=DAY*31
YEAR=DAY*365
SG_VER_REL=version.VERSION+'-'+version.RELEASE
BUFSIZE=10240
AZaz09='ABCDEFGHIJKLMNOPQRSTUVWXYZ'\
       'abcdefghijklmnopqrstuvwxyz'\
       '0123456789'
allowed_chars=AZaz09+'._-'

# python-2.3 socket has no SHUT_* constants
if not hasattr(socket, 'SHUT_WR'):
  socket.SHUT_RD = 0
  socket.SHUT_WR = 1
  socket.SHUT_RDWR = 2

#########################################################################
### Translations

class trans_class:
  def __init__(self, domain='sagator', locale_dir='/usr/share/locale'):
      self.TR = {}
      self.LANG = 'en'
      self.DOMAIN = domain
      self.LOCALE_DIR = locale_dir
  def __call__(self,msg):
      return self.gettext(msg)
  def gettext(self,msg):
      return msg
  def lgettext(self,msg):
      return self.TR[self.LANG].gettext(msg).decode('UTF-8')
  def set_lang(self,lang):
      self.LANG=lang
      if not self.TR.has_key(self.LANG):
        self.TR[self.LANG] = gettext.translation(
                               self.DOMAIN, self.LOCALE_DIR, [self.LANG],
                               fallback=True, codeset='UTF-8')
      self.gettext=self.lgettext
_=trans_class()

#########################################################################
### Debuging support

class debug_class:
  '''
  Debug class. Used for debugging informations.
  '''
  debug_level=2
  trace_level=9
  fd=1
  def __init__(self):
      self.set_logfile('-')
  def dup(self):
      if self.fd>2:
        os.dup2(self.fd,1)
        os.dup2(self.fd,2)
  def set_logfile(self,logfile,silent=False):
      self.logfile=logfile
      if self.fd>2:
        # try to close an old file descriptor opened by sagator
        self.logfd.close()
      if logfile=='-':
        try:
          self.fd=os.dup(1)
        except OSError:
          self.fd=1
      else:
        try:
          # open a new logfile
          self.fd=os.open(logfile,os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0640)
        except OSError, (ec,es):
          self.fd=1
          if not silent:
            self.echo(2, "WARNING: Can't open logfile: ", logfile, ": ", es) 
      self.logfd=os.fdopen(self.fd, 'a', 0)
      sys.stdout=self.logfd
      sys.stderr=self.logfd
  def reopen(self):
      if (self.logfile!='-') and \
          (self.logfile[0:len(safe.ROOT_PATH)]==safe.ROOT_PATH):
        lfn=self.logfile[len(safe.ROOT_PATH):]
        if lfn[0]!="/":
          lfn="/"+lfn
        self.echo(7,"Reopening log %s ..." % lfn)
        try:
          oldumask=os.umask(0002)
          self.fd=os.open(lfn, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0660)
          os.umask(oldumask)
          self.logfd=os.fdopen(self.fd, 'a', 0)
          self.dup()
        except Exception, es:
          self.echo(2, "WARNING: Log is not reopened! [%s]" % es)
          debug.traceback(4, 'debug.reopen()')
        # try to fix ownership
        try:
          os.chown(lfn,globals.UID,globals.GID)
        except OSError:
          pass
      else:
        self.echo(1,"ERROR: Logrotation not possible! (log isn't in chroot?)")
  def set_level(self,level):
      self.debug_level=level
  def echo(self,level,*s):
      if self.debug_level>=level:
        if self.debug_level>=9:
          o=time.strftime("%c")+': '
        else:
          o=''
        for i in s:
          if type(i)==type([]):
            o+=''.join([str(x) for x in i])
          else:
            o+=str(i)
        p=os.getpid()
        while 1:
          try:
            self.logfd.write("%5d: %s\n" % (p,o.rstrip('\r\n')))
            break
          except IOError,(ec,es):
            if ec!=4:
              break
          except UnicodeEncodeError:
            o=re.sub('[^\x00-\x7F]','_',o) # leave only ascii chars
            break
  def stack(self,level,arg):
      self.echo(level,arg,traceback.format_stack())
  def traceback(self,level,arg=''):
      e_type,e_value,e_tb=sys.exc_info()
      self.echo(level,arg,traceback.format_exception(e_type,e_value,e_tb))
  def traceback_value_str(self):
      return ', '.join(traceback.format_exception_only(
                         sys.exc_type, sys.exc_value))

debug=debug_class()

#########################################################################
### Useful functions

def is_infected(level,detected=''):
    if level>=1.0:
      return True
    else:
      return False

def iret(level,vname,ret):
    if vname:
      return level,vname,ret
    else:
      return 0.0,vname,ret

class safe_class:
  ROOT_PATH='/'
  def fn(self,f):
      if (f[0]=='/') and (self.ROOT_PATH):
        return os.path.join(self.ROOT_PATH,f.lstrip('/'))
      else:
        return f
  def open(self,name,mode='',buffering=-1):
      if buffering>=0:
        return open(self.fn(name),mode,buffering)
      if mode:
        return open(self.fn(name),mode)
      return open(self.fn(name))
  def osopen(self,filename,flag,mode=0777):
      return os.open(self.fn(filename),flag,mode)

safe=safe_class()

def normalize_filename(sfn):
    '''replace !allowed_chars from filename with _'''
    ofn=''
    for chp in range(len(sfn)):
      if not sfn[chp] in allowed_chars:
        ofn=ofn+'_'
      else:
        ofn=ofn+sfn[chp]
    return str(ofn)

def quote(s):
    return re.sub("([\\\\'])","\\\\\\1",s)

class popen:
  '''
  POpen implementation (with some changes)
  '''
  def __init__(self, cmd, wd='', resource_limits={}):
      self.exitstatus=-1
      self.killsignal=-1
      self.execerror=''
      self.pid=0
      self.pcr,self.pcw = os.pipe()
      self.cpr,self.cpw = os.pipe()
      self.oldsigchld=signal.getsignal(signal.SIGCHLD)
      signal.signal(signal.SIGUSR1,self.sighandler)
      signal.signal(signal.SIGCHLD,self.sighandler)
      self.pid = os.fork()
      if self.pid == 0: # child
        signal.signal(signal.SIGUSR1,signal.SIG_DFL)
        signal.signal(signal.SIGCHLD,signal.SIG_DFL)
        os.close(self.pcw)
        os.close(self.cpr)
        os.dup2(self.pcr,0); os.close(self.pcr)
        os.dup2(self.cpw,1)
        os.dup2(self.cpw,2); os.close(self.cpw)
        try:
          if wd:
            os.chdir(wd)
          for key,value in resource_limits.items():
            resource.setrlimit(key,(value,value))
          debug.echo(4,"POpen: Running: ",str(cmd))
          os.execvp(cmd[0],cmd)
        except os.error,(ec,es):
          debug.echo(3,"POpen: os.error: ",es+str([cmd[0]]))
          os.write(1,es+str([cmd[0]]))
        os._exit(10)
      os.close(self.pcr)
      os.close(self.cpw)
      self.tocmd=os.fdopen(self.pcw, 'w', 0)
      self.fromcmd=os.fdopen(self.cpr, 'r', 0)
  def sighandler(self,sn,stack):
      if sn==signal.SIGUSR1:
        self.execerror=os.read(self.cpr,1024)
      else:
        self.wait()
        debug.echo(4,"POpen SIG=",sn,", pid=",self.pid,
              ", exitstatus=",self.exitstatus,
              ", signal=",self.killsignal)
  def wait(self):
      try:
        pid,es=os.waitpid(self.pid,0)
        self.exitstatus=es/256
        self.killsignal=es%256
      except OSError:
        if self.exitstatus<0:
          self.exitstatus=0 # pid already exited, but no exit status
      signal.signal(signal.SIGCHLD,self.oldsigchld)
      return self.exitstatus
  def readlines(self):
      try:
        return self.fromcmd.readlines()
      except IOError,(ec,es):
        if ec==4: #Interrupted system call
          return []
        else:
          raise
  def readline(self):
      try:
        return self.fromcmd.readline()
      except IOError,(ec,es):
        if ec==4: #Interrupted system call
          return []
        else:
          raise
  def read(self):
      return self.fromcmd.read()
  def write(self,s):
      if self.exitstatus<0:
        return self.tocmd.write(s)
      else:
        return 0
  def close(self,rw=3):
      if rw&1:
        self.tocmd.flush()
        os.close(self.pcw)
      if rw&2:
        os.close(self.cpr)

def rlistdir(rootpath,path=''):
    '''
    list files and directories recursively
    
    return values: [filelist],size
      [filelist] is an array of file and directory names
      size is added filesizes
    '''
    # decode to plain string from unicode
    rootpath=str(rootpath)
    path=str(path)
    files,fsize=[],0L
    try:
      listdir=os.listdir(safe.fn(os.path.join(rootpath,path)))
    except:
      return [],0L
    for d in listdir:
      stat=os.lstat(safe.fn(os.path.join(rootpath,path,d)))
      if stat.st_mode & 0x4000: # is_dir
        rld=rlistdir(rootpath,os.path.join(path,d))
        files=files+rld[0]
        fsize=fsize+rld[1]
      else: # is_file or is_symlink or ...
        fsize=fsize+stat.st_size
        files.append(os.path.join(path,d))
    return files,fsize

def randomchars(count=10,chars=AZaz09):
    '''
    Return count random characters. Characters are randomly selected
    from chars array.
    '''
    s=''
    r=random.Random()
    for n in range(count):
      s=s+chars[int(r.random()*len(chars))]
    return s

class mktemp:
      '''
      make a temporary file or directory
      Makeing of file:
        mktemp('filename-prefix','.suffix','w',0600).autorm()
      Makeinf od directory:
        mktemp('filename-prefix','.suffix','d',0600).autorm()
      '''
      def __init__(self,prefix,suffix='',flags='w',umode=-1):
          self.flag_autorm=0
          self.flags=flags
          while 1:
            s=randomchars()
            self.name=prefix+s+suffix
            self.name_no_prefix=s+suffix
            self.root_name=safe.fn(self.name)
            debug.echo(7,"mktemp: ",self.root_name)
            try:
              if flags=='d':
                if umode<0:
                  umode=0700
                os.mkdir(self.root_name)
                os.chmod(self.root_name,umode)
                if (os.stat(self.root_name).st_mode & 0170000)==040000:
                  return
              else:
                if umode<0:
                  umode=0600
                self.fd=os.open(self.root_name,
                                os.O_WRONLY|os.O_CREAT|os.O_EXCL,umode)
                self.f=os.fdopen(self.fd,'w',BUFSIZE)
                if (os.stat(self.root_name).st_mode & 0170000)==0100000:
                  return
            except OSError,(ec,es):
              if not (ec in [17,21]):
                if ec in [2,13]:
                  debug.echo(1,"ERROR: Can't make temp file: ",
                             self.root_name,": ",es)
                raise
      def __del__(self):
          if self.flag_autorm:
            self.rm()
      def rm(self):
          if self.flags=='d':
            if debug.debug_level<10:
              os.rmdir(self.name)
          else:
            try:
              self.f.close()
            except:
              pass
            if debug.debug_level<10:
              os.unlink(self.root_name)
      def autorm(self):
          self.flag_autorm=1
          return self

def tempcleanup(filename,s=''):
    '''
    Safe cleanup for a file.
    '''
    try:
      os.unlink(safe.fn(filename))
    except OSError,(er,es):
      debug.echo(0,s+": ERROR: OSError: ",es,filename)

def fromhdr(sender='local@local.local'):
    '''
    Return a "From local@... DATE" string. String is by RFC.
    '''
    sender=sender or 'nobody@localhost'
    return "From "+sender+\
      time.strftime("  %c"+globals.EOL,time.localtime(time.time()))

def replace_tmpl(msg,vars):
    if '%(' in msg:
      return msg % vars
    idx={}
    for v in vars.keys():
      try:
        n=0
        while True:
          n=msg.index('$'+v,n)
          idx[n]=v
          n+=len(v)
      except ValueError:
        pass
      for c in '0123456789':
        try:
          idx[msg.index('$'+v+c)]=v+c
        except ValueError:
          pass
    keys=idx.keys() # replace it from last to first
    keys.sort()
    keys.reverse()
    for k in keys:
      idxk=idx[k]
      if idxk[-1] in '0123456789':
        idxk=idxk[:-1]
      msg=msg[:k]+vars[idxk]+msg[k+len(idx[k])+1:]
    return msg

class average:
  '''
  Count averages. It can count average values for "delta" time.
  Number of counted arguments can be variable.
  '''
  def __init__(self,count,delta=60):
      ''' initialize variables '''
      self.last=time.time()
      self.delta=delta
      self.count=count
      self.data=[0]*count
      self.avg=[0]*count
  def update(self,*args):
      ''' update data by args '''
      self.data=[x+y for x,y in zip(self.data,args)]
      ctime=time.time()
      if (ctime-self.last)>=self.delta:
        # count averages
        t=ctime-self.last
        self.avg=[count*self.delta/t for count in self.data]
        # zero current data and time
        self.data=[0]*self.count
        self.last=ctime
  def get(self):
      ''' return countet averages '''
      return self.avg

def socket_settimeout(sock,seconds):
    '''
    Set a timeout for a socket. Timeout is in seconds.
    '''
    try:
      # try to use settimeout
      sock.settimeout(seconds)
    except AttributeError:
      # use this for python < 2.3
      sock.setsockopt(socket.SOL_SOCKET,socket.SO_RCVTIMEO,
        struct.pack('ll',seconds,0))

#########################################################################
### SMTP and mail classes

S_OK=_('SENT')
S_TEMPFAIL=_('TEMP-FAILED')
S_REJECT=_('REJECTED')
S_DROP=_('DROPPED')
S_CUSTOM=_('CUSTOM')
S_FORCE_SEND=_('SEND-FORCED')

RECV_SMTP=0
RECV_HEADER=1 # obsolete
RECV_BODY=2
RECV_QUIT=3 # waiting for quit

class SmtpcError(Exception):
  """SMTP Client Error"""
  ConnectError='ConnectError'
  WrongReturnCode='WrongReturnCode'
  SendmailError='SendmailError'

class smtp:
  '''
  Basic SMTP class. All SMTP clases are descended from this class.
  '''
  f=False
  bufsize=BUFSIZE
  SMTP_SERVER=('127.0.0.1', 25)
  reg_smtp_reply=re.compile("^([0-9]{3}) ", re.M)
  reg_smtp_reply_ok=re.compile("^[23][0-9][0-9] ", re.M)
  reg_mailfrom=re.compile("^MAIL +FROM: *(.*?)\r?\n?$", re.IGNORECASE)
  reg_rcptto=re.compile("^RCPT +TO: *(.*?)\r?\n?$", re.IGNORECASE)
  reg_data=re.compile("^DATA", re.IGNORECASE)
  reg_quit=re.compile("^QUIT", re.IGNORECASE)
  reg_helo_ehlo=re.compile('^(?:HELO|EHLO) +(.*?)[ \r\n]', re.I|re.M)
  reg_xforward_addr=re.compile(r"^XFORWARD.* ADDR=\(?'?([0-9.]+)'?[, \r\n]",
                               re.I|re.M)
  reg_xforward_name=re.compile(r"^XFORWARD.* NAME=([^ ]+)[ \r\n]", re.I|re.M)
  reg_xforward=re.compile(r"^XFORWARD ", re.IGNORECASE)
  reg_rset=re.compile(r"^RSET", re.IGNORECASE)

class smtpc(smtp):
  '''
  SMTP client class.
  '''
  def __init__(self,mail_from=''):
      try:
        self.conn=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        socket_settimeout(self.conn,120)
        self.conn.connect(self.SMTP_SERVER)
      except socket.error,(ec,es):
        debug.echo(0,"SMTPC: ERROR: connect: ",es)
        raise SmtpcError,(SmtpcError.ConnectError,
                          "451 SMTP connection refused\r\n")
      self.f=self.conn.makefile('rw',self.bufsize)
      if mail_from:
        ret=self.send('','2..')
        ret=self.send('EHLO %s' % socket.gethostname(),'2..')
        ret=self.send('MAIL FROM:%s' % mail_from,'2..')
  def __del__(self):
      self.close()
  def send(self,data,rc='...'):
      if data!='':
        debug.echo(5,"SMTPC>: ",data)
        self.conn.sendall(data+"\r\n")
      ret=''
      while 1:
        r=self.readline()
        ret=ret+r
        smtp_reply=self.reg_smtp_reply.search(r)
        if smtp_reply:
          break
      debug.echo(5,"SMTPC<: ",ret)
      if not re.compile("^%s$" % rc).search(smtp_reply.group(1)):
        raise SmtpcError,(SmtpcError.WrongReturnCode,
                          "%s!=%s" % (smtp_reply.group(1),rc))
      return ret
  def readline(self):
      while True:
        try:
          return self.f.readline()
        except IOError,(ec,es):
          if ec!=11: # Try again
            raise
      return '' # required by pychecker
  def close(self):
      try:
        self.f.close()
        self.conn.shutdown(2)
        self.conn.close()
      except:
        pass
  def sendmail(self,sender,recipients,ldata=''):
      ret="451 SMTP connection error\r\n"
      try:
        self.send('','220')
        self.send("HELO "+socket.gethostname(),'250')
        self.send("MAIL FROM:"+(sender or '<>'),'250')
        for rec in recipients:
          self.send("RCPT TO:"+rec,'250')
        self.send("DATA",'354')
        self.conn.sendall(ldata)
        debug.echo(5,"SMTPC>: DATA SENT")
        ret=self.send(".","250")
      except SmtpcError.WrongReturnCode,ret:
        raise SmtpcError,(SmtpcError.SendmailError,ret)
      try:
        self.send("QUIT","221")
        self.close()
      except SmtpcError.WrongReturnCode,ret:
        debug.echo(5,"SMTPC: quit error",ret)
      return ret

class mail_class:
  '''
  EMail structure with some functions.
  '''
  reg_header=re.compile('^[!-9;-~]+:').search
  reg_header_cont=re.compile('^[ \t]+').search
  reg_recv=re.compile(
             '^Received: +from +([^ ]+) +\(([^ ]+) +\[([0-9.]+)\]\)',
             re.M|re.I
           )
  reg_xforward=smtp.reg_xforward_addr
  def __init__(self):
      self.addr=('0.0.0.0', 0, '') # Connection address,port,name
      self.comm='' # SMTP communication
      self.data='' # Email Data (with header too)
      self.bodypos=0 # Body position
      self.xheader='' # Extended header
      self.xhdra={} # Extended header array
      self.headers={} # parsed message headers
      self.recip=[] # recipient emails
      self.sender='' # sender
      self.policy_request={} # smtpd_policy request
      self.saved=() # not saved yet
      self.df=StringIO()
  def close(self):
      self.df.flush()
      self.data=self.df.getvalue()
      self.df.close()
      del self.df # not required now
      self.findbody()
  def store(self):
      self.saved=(self.comm, self.data, self.bodypos,
                  self.xheader, self.xhdra, self.headers,
                  self.sender, self.recip)
  def restore(self):
      if self.saved:
        (self.comm, self.data, self.bodypos, 
           self.xheader, self.xhdra, self.headers,
           self.sender, self.recip)=self.saved
  def findbody(self):
      # find end of header (beginnig of email body)
      df=StringIO(self.data)
      lineno=0
      while True:
        lineno+=1
        l=df.readline()
        if (not l) | (l=='\r\n') | (l=='\n'):
          break
        if self.reg_header(l):
          continue
        if lineno==1:
          break
        if not self.reg_header_cont(l):
          break
      self.bodypos=df.tell()
      df.close()
      # also create an headers attribute
      try:
        self.headers = message_from_string(self.data[:self.bodypos])
      except:
        # unable to parse headers
        pass
  def normalize_header(self,lines,value=None):
      ''' normalize to <80 chars per line '''
      if value!=None:
        lines+=': '+value+globals.EOL
      SPACE8 = ' '*8
      MAXLINELEN2=78
      MAXLINELEN=MAXLINELEN2-len(SPACE8)
      sublines_semi=[]
      for line in lines.splitlines():
        if len(line.replace("\t", SPACE8)) <= MAXLINELEN2:
          sublines_semi.append(line.rstrip())
        else:
          # split line by semicolons
          while len(line) > MAXLINELEN:
            i=line.rfind(';', 0, MAXLINELEN)
            if i<0:
              break
            sublines_semi.append(line[:i+1].lstrip())
            line=line[i+1:]
          sublines_semi.append(line.lstrip())
      # now split sublines by spaces
      sublines_space=[]
      for line in sublines_semi:
        if len(line.replace("\t", SPACE8)) <= MAXLINELEN2:
          sublines_space.append(line)
        else:
          while len(line) > MAXLINELEN:
            i=line.rfind(' ', 0, MAXLINELEN)
            if i<0:
              break
            sublines_space.append(line[:i])
            line=line[i+1:]
          sublines_space.append(line)
      # and now join it
      sublines=''
      current=sublines_space.pop(0)
      while sublines_space:
        line=sublines_space.pop(0)
        nline=current+' '+line
        if len(nline.replace("\t", SPACE8)) <= MAXLINELEN2:
          current=nline
        else:
          sublines=sublines+current+globals.EOL
          current="\t"+line
      sublines=sublines+current+globals.EOL
      return sublines
  def addheader(self,desc,value=None):
      if value==None:
        hdr=desc
      else:
        hdr=desc+': '+value+globals.EOL
        self.xhdra[desc]=value
      self.xheader+=self.normalize_header(hdr)
  def modheader(self,frm,to):
      '''
      Modifies header by regular expression.
      Returns number of modifications (max. 1).
      '''
      h=re.compile(frm,re.MULTILINE).subn(to,self.data[:self.bodypos],1)
      if h[1]>0:
        self.data=h[0]+self.data[self.bodypos:]
      return h[1]
  def delheader(self,hdr):
      reg_hdr=re.compile('^%s: .*?(^[!-9;-~])' % hdr,re.I|re.M|re.DOTALL)
      while True:
        reg1=reg_hdr.search(self.data)
        if reg1:
          if reg1.end()<self.bodypos:
            self.data=self.data[:reg1.start()]+self.data[reg1.end()-1:]
          else:
            break
        else:
          break
  def addvirusinfo(self,detected,scannames=''):
      if not "X-Sagator-Scanner" in self.xhdra.keys():
        self.addheader("X-Sagator-Scanner",
          SG_VER_REL+' at '+socket.gethostname()+'; '+scannames)
      if not "X-Sagator-ID" in self.xhdra.keys():
        if globals.id:
          self.addheader("X-Sagator-ID", globals.id)
      if detected:
        if not "X-Sagator-Status" in self.xhdra.keys():
          self.addheader("X-Sagator-Status",
                         "%s [%s]" % (detected,globals.found_by.name))
  def getnamebyaddr(self,ip):
      try:
        return socket.gethostbyaddr(ip)[0]
      except:
        return ''
  def getsender(self):
      '''
      Extract sender's [IP,hostname,HELO_string] from XFORWARD SMTP
      command or email's Received header. Return this array.
      '''
      aa=dict(ADDR=self.addr[0], NAME=self.addr[2], HELO='')
      try:
        r1=self.reg_xforward.search(self.comm)
        if r1:
          regs=dict(
            ADDR=smtp.reg_xforward_addr,
            NAME=smtp.reg_xforward_name,
            HELO=smtp.reg_helo_ehlo
          )
          for key,value in regs.items():
            r2=value.search(self.comm)
            if r2:
              aa[key]=r2.group(1)
            else:
              break
        else:
          r3 = self.reg_recv.search(self.data)
          if r3:
            aa = dict(
                   ADDR=r3.group(3),
                   NAME=r3.group(2),
                   HELO=r3.group(1)
                 )
      except:
        pass
      return aa
  def noop_connect_from(self):
      return 'NOOP CONNECT FROM: %s:%d, %s\r\n' % self.addr

# mail object
mail=mail_class()

#########################################################################
### Scanner templates

class ScannerError(Exception):
      """Any Error Of Any Scanner"""

class scanoper:
  '''
  A scanner used to operate over various scanners.
  '''
  name='op()'
  is_scanner=True
  is_multibuffer=False
  is_interscanner=True
  ignore_name=True
  filename='buffer'
  def __init__(self,op,a,b=None):
      self._oper=(op,a,b)
      if b:
        self.name=a.name+op+b.name
      else:
        self.name=a.name
      self.scanner=a
      self.scannerb=b
  def _join(self,d1,d2,op=','):
      if (d1!='') and (d1!=' '):
        if (d2!='') and (d2!=' '):
          return d1+op+d2
        else:
          return d1
      else:
        return d2
  def _eval(self,l1,d1,v1,l2,d2,v2):
      op=self._oper[0]
      debug.echo(8,"oper: %f %s %f ['%s','%s']" % (l1,op,l2,d1,d2))
      if op=='+':
        return l1+l2,self._join(d1,d2),v1+v2
      elif op=='-':
        return l1-l2,self._join(d1,d2),v1+v2
      elif op=='*':
        return l1*l2,self._join(d1,d2),v1+v2
      elif op=='/':
        return l1/l2,self._join(d1,d2),v1+v2
      elif op=='<':
        if l1<l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      elif op=='>':
        if l1>l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      elif op=='=':
        if l1==l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      elif op=='<=':
        if l1<=l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      elif op=='>=':
        if l1>=l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      elif op=='!=':
        if l1!=l2:
          return 1.0,self._join(d1,d2),v1+v2
        else:
          return 0.0,'',[]
      else:
        raise ScannerError,"Unknown operator: "+self._oper[0]
  def param(self,key,value=None):
      if self.scanner.param(key,value):
        return self.scannerb.param(key,value)
      return ''
  def help(self):
      h=self.scanner.help()
      h.update(self.scannerb.help())
      return h
  def rcpt_signature(self,rcpt):
      siga=self.scanner.rcpt_signature(rcpt)
      if self.scannerb:
        sigb=self.scannerb.rcpt_signature(rcpt)
        if siga and sigb:
          return siga+self._oper[0]+sigb
      else:
        if siga:
          return self._oper[0]+siga
      return ''
  def scanbuffer(self,buffer,args={}):
      l,d,v={},{},{}
      if self._oper[0] in ('+','-','*','/','<','>','=','<=','>=','!='):
        for i in [1,2]:
          self._oper[i].prescan()
          l[i],d[i],v[i]=self._oper[i].scanbuffer(buffer,args)
          self._oper[i].postscan(l[i],d[i],v[i])
        return self._eval(l[1],d[1],v[1],l[2],d[2],v[2])
      elif self._oper[0]=='|':
        try:
          self._oper[1].prescan()
          l,d,v=self._oper[1].scanbuffer(buffer,args)
          self._oper[1].postscan(l,d,v)
          return l,d,v
        except:
          self._oper[2].prescan()
          l,d,v=self._oper[2].scanbuffer(buffer,args)
          self._oper[2].postscan(l,d,v)
          return l,d,v
      elif self._oper[0]=='&':
        self._oper[1].prescan()
        l1,d1,v1=self._oper[1].scanbuffer(buffer,args)
        self._oper[1].postscan(l1,d1,v1)
        if is_infected(l1):
          self._oper[2].prescan()
          l2,d2,v2=self._oper[2].scanbuffer(buffer,args)
          self._oper[2].postscan(l2,d2,v2)
          return l1*l2,self._join(d1,d2,'&'),v1+['&']+v2
        else:
          return 0.0,'',[]
      elif self._oper[0]=='~':
        try:
          self._oper[1].prescan()
          l,d,v=self._oper[1].scanbuffer(buffer,args)
        except:
          debug.echo(4,"Scanner %s failed, recovering..." % self._oper[1].name)
          if self._oper[1].is_interscanner:
            l,d,v=self._oper[1].get('child_status')
          else:
            return 0.0,'',[]
        self._oper[1].postscan(l,d,v)
        return l,d,v
      else:
        raise ScannerError,"Unknown operator: "+self._oper[0]
  def scanfile(self,files,dirname='',args={}):
      l,d,v={},{},{}
      if self._oper[0] in ('+','-','*','/','<','>','=','<=','>=','!='):
        for i in [1,2]:
          self._oper[i].prescan()
          l[i],d[i],v[i]=self._oper[i].scanfile(files,dirname)
          self._oper[i].postscan(l[i],d[i],v[i])
        return self._eval(l[1],d[1],v[1],l[2],d[2],v[2])
      elif self._oper[0]=='|':
        try:
          self._oper[1].prescan()
          l,d,v=self._oper[1].scanfile(files,dirname)
          self._oper[1].postscan(l,d,v)
          return l,d,v
        except:
          self._oper[2].prescan()
          l,d,v=self._oper[2].scanfile(files,dirname)
          self._oper[2].postscan(l,d,v)
          return l,d,v
      elif self._oper[0]=='&':
        self._oper[1].prescan()
        l1,d1,v1=self._oper[1].scanfile(files,dirname)
        self._oper[1].postscan(l1,d1,v1)
        if is_infected(l1):
          self._oper[2].prescan()
          l2,d2,v2=self._oper[2].scanfile(files,dirname)
          self._oper[2].postscan(l2,d2,v2)
          return l1*l2,self._join(d1,d2,'&'),v1+['&']+v2
        else:
          return 0.0,'',[]
      elif self._oper[0]=='~':
        try:
          self._oper[1].prescan()
          l,d,v=self._oper[1].scanfile(files,dirname)
        except:
          debug.echo(4,"Scanner %s failed, recovering..." % self._oper[1].name)
          if self._oper[1].is_interscanner:
            l,d,v=self._oper[1].get('child_status')
          else:
            return 0.0,'',[]
        self._oper[1].postscan(l,d,v)
        return l,d,v
      else:
        raise ScannerError,"Unknown operator: "+self._oper[0]
  def prescan(self):
      pass
  def postscan(self,level,vir,ret):
      pass
  def destroy(self):
      self._oper[1].destroy()
      if self._oper[2]:
        self._oper[2].destroy()
  def reinit(self):
      self._oper[1].reinit()
      if self._oper[2]:
        self._oper[2].reinit()
  def get(self,var):
      try:
        return getattr(self._oper[1], var)
      except AttributeError:
        try:
          return eval(self._oper[2], var)
        except AttributeError:
          return "UNKNOWN"
  def __add__(self,second):
      return scanoper('+',self,second)
  def __sub__(self,second):
      return scanoper('-',self,second)
  def __mul__(self,second):
      return scanoper('*',self,second)
  def __div__(self,second):
      return scanoper('/',self,second)
  def __or__(self,second):
      return scanoper('|',self,second)
  def __and__(self,second):
      return scanoper('&',self,second)
  def __invert__(self):
      return scanoper('~',self)
  def __lt__(self,second):
      return scanoper('<',self,second)
  def __gt__(self,second):
      return scanoper('>',self,second)
  def __eq__(self,second):
      return scanoper('=',self,second)
  def __le__(self,second):
      return scanoper('<=',self,second)
  def __ge__(self,second):
      return scanoper('>=',self,second)
  def __ne__(self,second):
      return scanoper('!=',self,second)

class ascanner(scanoper):
  '''
  Default scanner user for building all other realscanners.
  '''
  name='AScanner()'
  is_spamscan=False
  is_policy_scanner=False
  is_interscanner=False
  ignore_name=False
  scanner=0
  def __init__(self):
      pass
  def destroy(self):
      pass
  def help(self):
      return {}
  def param(self,key,value=None):
      return 'Unknown parameter: %s' % key
  def prescan(self):
      '''This function is called before running a scanner'''
      debug.echo(5,"Running: ",self.name)
  def postscan(self,level,vir,ret):
      '''This function is called after running a scanner'''
      self.child_status=(level,vir,ret)
      debug.echo(4,"Values: %f, '%s', %s" % (level,vir,str(ret)))
      if is_infected(level):
        if not self.ignore_name:
          globals.found_by=self
          debug.echo(5,"Found %s by %s, level %f" % (vir,self.name,level))
  def rcpt_signature(self,rcpt):
      return self.name
  def scanbuffer(self,buffer,args={}):
      raise ScannerError,'Not implemented'
  def scanfile(self,files,dirname='',args={}):
      raise ScannerError,'Not implemented'
  def reinit(self):
      pass
  def get(self,var):
      try:
        return getattr(self, var)
      except AttributeError:
        return "UNKNOWN"

class interscanner(ascanner):
  '''
  Default scanner used for building all other interscanners.
  '''
  name='AInterScanner()'
  is_interscanner=True
  scanners=[]
  def rename(self,scanners):
      n=[s.name for s in scanners]
      self.name=self.name.replace("()","("+', '.join(n)+")")
      if self.name=="":
        self.name=','.join(n)
  def destroy(self):
      self.scanner.destroy()
  def prescan(self):
      '''This function is called before running a scanner'''
      debug.echo(5,"Running: ",self.name)
  def postscan(self,level,vir,ret):
      '''This function is called after running a scanner'''
      self.child_status=(level,vir,ret)
  def rcpt_signature(self,rcpt):
      return "%s(%s)" % (
        self.name.split('(',1)[0], # only it's name
        self.scanner.rcpt_signature(rcpt)
      )
  def reinit(self):
      self.scanner.reinit()
  def param(self,key,value=None):
      return self.scanner.param(key,value)
  def help(self):
      return self.scanner.help()
  def get(self,var):
      try:
        r=getattr(self, var)
        if not r:
          r=self.scanner.get(var)
        return r
      except AttributeError:
        return self.scanner.get(var)

class globals_class:
  QFNAME='' # quarantine filename (generated)
  USER=''
  GROUP=''
  UID=0
  GID=0
  SRV=[]
  EOL='\r\n'
  DBC=None # a policy database connection
  id=None # sagator's message ID
  scan_only=False # only scan or also send reports?
  daemon=True # daemonize or not?
  pid_file=None # PID filename
  pidf=None # PID file (as python file object)
  # policy specifications
  sender_policy=[]
  recipient_policy=[]
  def __init__(self):
      self.reset()
  def reset(self,action=S_REJECT):
      self.found_by=ascanner
      self.QFNAME=''
      self.ACTION=action
      self.PREPEND=''
      self.RCPT_MATCH={}
  def action(self,level,detected=''):
      if is_infected(level):
        return self.ACTION
      else:
        return S_OK
  def gen_id(self,time1,time2):
      self.id="%s-%04d-%05d-%s@%s" \
              % (time1,time2,os.getpid(),randomchars(6),socket.gethostname())
  def setuidgid(self,user,group):
      if user:
        try:
          globals.UID=pwd.getpwnam(user)[2]
          globals.USER=user
        except KeyError,err_str:
          debug.echo(0,"ERROR, getpwnam: %s %s, using current user"
                       % ([user],err_str))
          globals.UID=os.getuid()
          globals.USER=pwd.getpwuid(globals.UID)[0]
      if group:
        try:
          globals.GID=grp.getgrnam(group)[2]
          globals.GROUP=group
        except KeyError,err_str:
          debug.echo(0,"ERROR, getgrnam: %s %s, using current group"
                       % ([group],err_str))
          globals.GID=os.getgid()
          globals.GROUP=grp.getgrgid(globals.GID)[0]
globals=globals_class()
