'''
Antivir library for Sagator

(c) 2003-2009 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 += 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_cmd = re.compile("^([A-Z]+) ", re.IGNORECASE)
  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 += 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.conn.shutdown(socket.SHUT_RDWR)
      except:
        pass
      try:
        self.conn.close()
        self.f.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()
