# _*_ coding:utf-8 _*_

'''
Antivir library for Sagator

(c) 2003-2019 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 __future__ import absolute_import
from __future__ import print_function

import os, sys, re, time, random, gettext
import traceback, signal, resource, struct, socket
import pwd, grp
from threading import Thread
if sys.version_info[0]>2:
  from urllib.parse import quote_plus, unquote_plus
  from email import message_from_bytes as message_from_string
  from io import StringIO, BytesIO
  unicode = str
  empty_string = b""
else:
  from urllib import quote_plus, unquote_plus
  from email import message_from_string
  from cStringIO import StringIO
  BytesIO = StringIO
  empty_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

def tostr(s, enc="latin1"):
    if hasattr(s, "decode") and type(s)!=str:
      try:
        # decode with error replacing
        return s.decode(enc, "replace")
      except UnicodeDecodeError:
        return repr(s)[1:-1]
    return s

def tobytes(s, enc="latin1"):
    if type(s)==bytes:
      return s
    # encode with error replacing
    return s.encode(enc, "replace")

def tostr_list(a, enc="latin1"):
    def conv(x):
        if type(x)==str:
          return x
        elif type(x)==bytes:
          return x.decode(enc)
        return x
    return [conv(x) for x in a]

def tostr_dict(d, enc="latin1"):
    def conv(x):
        if type(x)==str:
          return x
        elif type(x)==bytes:
          return x.decode(enc)
        return x
    return dict([(conv(x), conv(y)) for x,y in d.items()])

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):
      t = self.TR[self.LANG].gettext(msg)
      if sys.version_info[0]<=2:
        return t.decode("UTF-8")
      return t
  def set_lang(self, lang):
      self.LANG = lang
      if self.LANG not in self.TR:
        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 logfile=='-':
        #try:
        #  self.fd = os.dup(1)
        #except OSError:
        #  self.fd = 1
        self.logfd = sys.stdout
        self.fd = 1
      else:
        if self.fd>2:
          # try to close an old file descriptor opened by sagator
          self.logfd.close()
        try:
          # open a new logfile
          self.fd = os.open(logfile, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0o640)
        except OSError as err:
          (ec, es) = err.args
          self.fd = 1
          if not silent:
            self.echo(2, "WARNING: Can't open logfile: ", logfile, ": ", es)
        self.logfd = os.fdopen(self.fd, 'a')
        sys.stdout = self.logfd
        # redirect stderr only for non-standard logging
        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(0o002)
          self.fd = os.open(lfn, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0o660)
          os.umask(oldumask)
          self.logfd = os.fdopen(self.fd, 'a')
          self.dup()
        except Exception as 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])
          elif hasattr(i, "decode") and type(i)==bytes:
            try:
              o += i.decode("latin1")
            except UnicodeEncodeError:
              o += i
          else:
            try:
              o += str(i)
            except UnicodeEncodeError:
              o += i
        p = os.getpid()
        while 1:
          try:
            self.logfd.write("%5d: %s\n" % (p, o.rstrip('\r\n')))
            self.logfd.flush()
            break
          except IOError as err:
            (ec, es) = err.args
            if ec!=4:
              break
          except UnicodeEncodeError:
            o = re.sub('[^\x00-\x7F]', '_', o) # leave only ascii chars
            continue
          except RuntimeError:
            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_info()[0], sys.exc_info()[1]))

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=0o777):
      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):
    if type(s)==bytes:
      return re.sub(b"([\\\\'])", b"\\\\\\1", s).decode("latin1")
    return re.sub("([\\\\'])", "\\\\\\1", s)

class core_count(object):
  '''
  Return number of cores on this system. You can define minimal value
  as first argument.
  '''
  def __init__(self):
      self.detected = None
  def __call__(self, minimum=1):
      if self.detected is None:
        try:
          self.detected = len([
            x for x in open('/proc/cpuinfo').read().split('\n')
            if x.startswith('processor\t:')
          ])
        except IOError:
          self.detected = 0
      n = self.detected
      # minimum number of cores is 1
      if n<minimum:
        n = minimum
      return n
core_count = core_count()

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 as err:
          (ec, es) = err.args
          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, 'wb')
      self.fromcmd = os.fdopen(self.cpr, 'rb')
  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 as err:
        (ec, es) = err.args
        if ec==4: #Interrupted system call
          return []
        else:
          raise
  def readline(self):
      try:
        return self.fromcmd.readline()
      except IOError as err:
        (ec, es) = err.args
        if ec==4: #Interrupted system call
          return []
        else:
          raise
  def read(self):
      return self.fromcmd.read()
  def write(self, s):
      if self.exitstatus<0:
        ret = self.tocmd.write(s)
        self.tocmd.flush()
        return ret
      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 = [], 0
    try:
      listdir = os.listdir(safe.fn(os.path.join(rootpath, path)))
    except:
      return [], 0
    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='wb', 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 = 0o700
                os.mkdir(self.root_name)
                os.chmod(self.root_name, umode)
                if (os.stat(self.root_name).st_mode & 0o170000)==0o40000:
                  return
              else:
                if umode<0:
                  umode = 0o600
                self.fd = os.open(self.root_name,
                                  os.O_WRONLY|os.O_CREAT|os.O_EXCL, umode)
                self.f = os.fdopen(self.fd, 'wb', BUFSIZE)
                if (os.stat(self.root_name).st_mode & 0o170000)==0o100000:
                  return
            except OSError as err:
              (ec, es) = err.args
              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 as err:
      (er, es) = err.args
      debug.echo(0, s+": ERROR: OSError: ", es, filename)

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

def replace_tmpl(msg, vars):
    if '%(' in msg:
      return msg % vars
    idx = {}
    for v in list(vars.keys()):
      try:
        n = 0
        while True:
          n = msg.index('$'+v, n)
          idx[n] = v
          n += len(v)
      except ValueError:
        pass
      for c in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
        try:
          idx[msg.index('$'+v+c)] = v+c
        except ValueError:
          pass
    keys = list(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(br"^([A-Z]+) ", re.IGNORECASE)
  reg_smtp_reply = re.compile(br"^([0-9]{3}) ", re.M)
  reg_smtp_reply_ok = re.compile(br"^[23][0-9][0-9] ", re.M)
  reg_mailfrom = re.compile(br"^MAIL +FROM: *(.*?)\r?\n?$", re.IGNORECASE)
  reg_rcptto = re.compile(br"^RCPT +TO: *(.*?)\r?\n?$", re.IGNORECASE)
  reg_data = re.compile(br"^DATA", re.IGNORECASE)
  reg_quit = re.compile(br"^QUIT", re.IGNORECASE)
  reg_helo_ehlo = re.compile(br'^(?:HELO|EHLO) +(.*?)[ \r\n]', re.I|re.M)
  reg_xforward_addr = re.compile(br"^XFORWARD.* ADDR=\(?'?([0-9.]+)'?[, \r\n]",
                                 re.I|re.M)
  reg_xforward_name = re.compile(br"^XFORWARD.* NAME=([^ ]+)[ \r\n]", re.I|re.M)
  reg_xforward = re.compile(br"^XFORWARD ", re.IGNORECASE)
  reg_rset = re.compile(br"^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 as err:
        (ec, es) = err.args
        debug.echo(0, "SMTPC: ERROR: connect: ", es)
        raise SmtpcError(SmtpcError.ConnectError,
                           "451 SMTP connection refused\r\n")
      self.f = self.conn.makefile('rwb', self.bufsize)
      if mail_from:
        mail_from = tobytes(mail_from)
        ret = self.send(b'', b'2..')
        ret = self.send(b'EHLO %s' % socket.gethostname().encode(), b'2..')
        ret = self.send(b'MAIL FROM:%s' % mail_from, b'2..')
  def __del__(self):
      self.close()
  def send(self, data, rc=b'...'):
      if data!="" and data!=b"":
        debug.echo(5, "SMTPC>: ", data)
        self.conn.sendall(data+b"\r\n")
      ret = b''
      while 1:
        r = self.readline()
        ret += r
        smtp_reply = self.reg_smtp_reply.search(r)
        if smtp_reply:
          break
      debug.echo(5, "SMTPC<: ", tostr(ret))
      if not re.compile(br"^%s$" % rc).search(smtp_reply.group(1)):
        raise SmtpcError(SmtpcError.WrongReturnCode,
                           b"%s!=%s" % (smtp_reply.group(1), rc))
      return ret
  def readline(self):
      while True:
        try:
          return self.f.readline()
        except IOError as err:
          (ec, es) = err.args
          if ec!=11: # Try again
            raise
      return '' # required by pychecker
  def close(self):
      try:
        self.conn.shutdown(socket.SHUT_RDWR)
      except:
        pass
      try:
        self.f.close()
        self.conn.close()
      except:
        pass
  def sendmail(self, sender, recipients, ldata=b'', ret_code=""):
      sender = tobytes(sender)
      ret = b"451 SMTP connection error\r\n"
      try:
        self.send(b'', b'220')
        self.send(b"HELO "+socket.gethostname().encode(), b'250')
        self.send(b"MAIL FROM:"+(sender or b'<>'), b'250')
        for rec in recipients:
          self.send(b"RCPT TO:"+tobytes(rec), b'250')
        self.send(b"DATA", b'354')
        self.conn.sendall(tobytes(ldata))
        debug.echo(5, "SMTPC>: DATA SENT")
        ret = self.send(b".", ret_code or b"250")
      except SmtpcError as ret:
        raise SmtpcError(SmtpcError.SendmailError, ret)
      try:
        if ret[0:3]==b"250":
          self.send(b"QUIT", b"221")
      except SmtpcError as ret:
        debug.echo(5, "SMTPC: quit error", ret)
      self.close()
      return ret

class mail_class:
  '''
  EMail structure with some functions.
  '''
  reg_header = re.compile(br'^[!-9;-~]+:').search
  reg_header_cont = re.compile(br'^[ \t]+').search
  reg_recv = re.compile(
               br'^Received: +from +([^ ]+) +\(([^ ]+) +\[([0-9.]+)\]\)',
               re.M|re.I
             )
  reg_xforward = smtp.reg_xforward_addr
  def __init__(self):
      self.addr = (b'0.0.0.0', 0, b'') # Connection address, port, name
      self.comm = b'' # SMTP communication
      self.data = b'' # Email Data (with header too)
      self.bodypos = 0 # Body position
      self.xheader = b'' # 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 = BytesIO()
  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 = BytesIO(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 += b': '+value+globals.EOL
      SPACE8 = b' '*8
      MAXLINELEN2 = 78
      MAXLINELEN = MAXLINELEN2-len(SPACE8)
      sublines_semi = []
      for line in lines.splitlines():
        if len(line.replace(b"\t", SPACE8)) <= MAXLINELEN2:
          sublines_semi.append(line.rstrip())
        else:
          # split line by semicolons
          while len(line) > MAXLINELEN:
            i = line.rfind(b';', 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(b"\t", SPACE8)) <= MAXLINELEN2:
          sublines_space.append(line)
        else:
          while len(line) > MAXLINELEN:
            i = line.rfind(b' ', 0, MAXLINELEN)
            if i<0:
              break
            sublines_space.append(line[:i])
            line = line[i+1:]
          sublines_space.append(line)
      # and now join it
      sublines = b''
      current = sublines_space.pop(0)
      while sublines_space:
        line = sublines_space.pop(0)
        nline = current+b' '+line
        if len(nline.replace(b"\t", SPACE8)) <= MAXLINELEN2:
          current = nline
        else:
          sublines = sublines+current+globals.EOL
          current = b"\t"+line
      sublines = sublines+current+globals.EOL
      return sublines
  def addheader(self, desc, value=None):
      if value==None:
        hdr = tobytes(desc)
        desc, value = desc.split(b":", 1)
        value = value.strip()
      else:
        hdr = tobytes(desc)+b': '+tobytes(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(br'^%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 xheaders(self, rstrip=b''):
      hdrs = self.xheader.rstrip(b'\n').split(b'\n')
      while hdrs:
        row = hdrs.pop(0).rstrip(rstrip)
        name, row = row.split(b":", 1)
        row = row.lstrip()
        while hdrs and hdrs[0][0] in (b'\t', b' '):
          row += b'\n' + hdrs.pop(0).rstrip(rstrip)
        yield name, row
  def addvirusinfo(self, detected, scannames=''):
      if not "X-Sagator-Scanner" in list(self.xhdra.keys()):
        self.addheader("X-Sagator-Scanner",
          SG_VER_REL+' at '+socket.gethostname()+'; '+scannames)
      if not "X-Sagator-ID" in list(self.xhdra.keys()):
        if globals.id:
          self.addheader("X-Sagator-ID", globals.id)
      if detected:
        if not "X-Sagator-Status" in list(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=b'')
      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 list(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:
        debug.traceback(4)
      return aa
  def noop_connect_from(self):
      return b'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=b','):
      if (d1!=b'') and (d1!=b' '):
        if (d2!=b'') and (d2!=b' '):
          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, b'', []
      elif op=='>':
        if l1>l2:
          return 1.0, self._join(d1,d2), v1+v2
        else:
          return 0.0, b'', []
      elif op=='=':
        if l1==l2:
          return 1.0, self._join(d1,d2), v1+v2
        else:
          return 0.0, b'', []
      elif op=='<=':
        if l1<=l2:
          return 1.0, self._join(d1,d2), v1+v2
        else:
          return 0.0, b'', []
      elif op=='>=':
        if l1>=l2:
          return 1.0, self._join(d1,d2), v1+v2
        else:
          return 0.0, b'', []
      elif op=='!=':
        if l1!=l2:
          return 1.0, self._join(d1,d2), v1+v2
        else:
          return 0.0, b'', []
      else:
        raise ScannerError("Unknown operator: %s" % tostr(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, b'&'), v1+[b'&']+v2
        else:
          return 0.0, b'', []
      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, b'', []
        self._oper[1].postscan(l, d, v)
        return l, d, v
      else:
        raise ScannerError("Unknown operator: %s" % tostr(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, b'&'), v1+[b'&']+v2
        else:
          return 0.0, b'', []
      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, b'', []
        self._oper[1].postscan(l, d, v)
        return l, d, v
      else:
        raise ScannerError("Unknown operator: %s" % tostr(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, tostr(vir), tostr_list(ret)))
      if is_infected(level):
        if not self.ignore_name:
          globals.found_by = self
          debug.echo(5, "Found %s by %s, level %f"
                        % (tostr(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 = b'\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?
  fork_id = 0 # Fork internal ID
  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 as 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 as 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()
