'''
spamassassin module, version 0.6.2

(c) 2003 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>
                                                                                
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

'''

from avlib import *

__all__=['spamassassin','spamassassind']

class spamassassin(ascanner):
  '''
  SpamAssassin scanner. [obsolete]

  This scanner scans for spams. If a spam is returned, a string
  SPAM is returned as virus name. This scanner uses a spamassassin
  binary. For better performance, configure spamd and use
  spamassassind.
  
  WARNING! You need to install spamassassin into chroot manually.
  mkchroot.sh script does it not. If you want to use spamassasin
  with chroot and don't want to make a copy in chroot, use
  a spamassassind scanner, but remember, that scanning for spams
  are not done in chroot environment.
  
  WARNING! This scanner is not fully compatible with header adding.
  Some data from headers are not added!
  
  Usage: spamassassin(['/usr/bin/spamassassin','-e'],
                      spamstring='',sa_max_file=500000)
  
  Where: in [] is a path and argumets of spamassassin binary
         spamstring is a string, which define a spamassassin report,
           which is checked. If it is found, mail is assigned as 'SPAM'
           (by default all reports with exit status!=0 are reported as spam).
         sa_max_file is a number, which defines maximum file size in bytes,
           which can be tested. If a mail (with header) is larger,
           it is not checked (by default 500000).

  Obsolete since 0.8.0, use spamassassind() instead.
  '''
  is_spamscan=1
  name='SpamAssassin()'
  def __init__(self,arg,spamstring='',sa_max_file=500000):
      self.arg=arg
      self.SPAMSTRING=spamstring
      self.SA_MAX_FILE=sa_max_file
  def scanbuffer(self,buffer,args={}):
      exitstatus=''
      if len(buffer)>self.SA_MAX_FILE:
        return 0.0,'',['File too long to test it. Size: '+str(len(buffer))]
      pf=popen(self.arg)
      pf.write(buffer)
      pf.tocmd.close()
      pf.wait()
      level,ret,exitstatus,lastspamline=0.0,[],'',0
      for line in pf.readlines():
        if re.search("^$",line):
          break
        if re.search("^\t",line):
          if lastspamline:
            ret.append(ret.pop(-1)+line)
        else:
          if re.search("^X-Spam-",line):
            ret.append(line)
            lastspamline=1
            if self.SPAMSTRING:
              if re.search(self.SPAMSTRING,line):
                exitstatus='SPAM'
            reg2=re.search("X-Spam-Status: Yes, hits=([^, ]*)[, ]",line)
            if reg2:
              level=float(reg2.group(1))
          else:
            lastspamline=0
      for i in ret:
        mail.addheader(i)
      pf.fromcmd.close()
      if (pf.exitstatus>0) and (self.SPAMSTRING==''):
        exitstatus='SPAM'
      return level,exitstatus,ret

class spamassassind(ascanner):
  '''
  SpamAssassin daemon scanner.
  
  This scanner scans for spams. If a spam is returned, a string
  SPAM is returned as virus name.
  This scanner uses a spamassassind daemon. If you are not familiar with it,
  you also can use spamassassin binary. Look at spamassassin scanner.
  
  Usage: spamassassind(['localhost',783],reqspamlevel=-1,
                       sa_max_file=500000,filter=False,
                       mydestination=None,virtual_users=None)

  Where: ['localhost',783] are the host and port of spamd.
         if reqspamlevel is less than spam hits, spam status is returned,
           if it is -1 (default), spam level from spamassassin config is used
         sa_max_file is a number, which defines maximum file size in bytes,
           which can be tested. If a mail (with header) is larger,
           it is not checked (by default 500000).
         filter is an boolean whic can be used to process whole message
           by spamassassin. New message (generated by spamassassin) will
           be stored instead of original. It's default is False.
         mydestination is an regular expression, which defines local domains.
           If it is set and only one recipient is specified
           and recipients domain matches against this regular expression,
           then username part will be sent into spamd.
           This parameter is new in 0.7.0.
         virtual_users is an regular expression, which defines virtual users.
           These users can be used to configure virtual user settings
           for spamassassin. Only for emails with one recipient will be
           applied.
           This parameter is new in 0.7.0.
  '''
  is_spamscan=1
  name='SpamAssassinD()'
  reg_x_spam=re.compile('^(X-Spam-.*?)\r?\n[^ \t]',re.M|re.S).search
  reg_eoh=re.compile('^(.*?\r?\n\r?\n)',re.M|re.S).search
  reg_status=re.compile(
    '^X-Spam-Status: (Yes|No), (score|hits)=([-0-9.]*) (required)=([-0-9.]*) ').search
  reg_hdr_body=re.compile('^([^:]*):[ \t]*(.*)',re.M|re.S).search
  def __init__(self,arg=('localhost',783),reqspamlevel=-1,sa_max_file=500000,filter=False,mydestination=None,virtual_users=None):
      self.arg=arg
      self.REQSPAMLEVEL=reqspamlevel
      self.SA_MAX_FILE=sa_max_file
      self.FILTER=filter
      if not mydestination:
        self.MYDESTINATION=None
      elif type(mydestination)==type(''):
        self.MYDESTINATION=re.compile(mydestination,re.IGNORECASE).search
      self.VIRTUAL_USERS=virtual_users
  def rcpt_signature(self,rcpt):
      sa_user='vscan'
      if self.MYDESTINATION:
        try:
          sau,domain=rcpt.rsplit('@',1)
          if self.MYDESTINATION(domain):
            sa_user=sau
          # can't check real user existence in chroot
        except:
          pass
      elif self.VIRTUAL_USERS:
        sa_user=rcpt
      return '%s%s%s' % (self.name[:-1],sa_user,self.name[-1])
  def scanbuffer(self,buffer,args={}):
      if len(buffer)>self.SA_MAX_FILE:
        return 0.0,'',['File too long to test it. Size: '+str(len(buffer))]
      # check if virtual users are set
      sa_user='vscan'
      if self.MYDESTINATION and (len(mail.recip)==1):
        try:
          sau,domain=mail.recip[0].rsplit('@',1)
          if self.MYDESTINATION(domain):
            sa_user=sau
          # can't check real user existence in chroot
        except:
          pass
      elif self.VIRTUAL_USERS and (len(mail.recip)==1):
        sa_user=mail.recip[0]
      debug.echo(4,'%s: User: %s' % (self.name,sa_user))
      # make connection to spamd and send commands to process this message
      s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      socket_settimeout(s,120)
      s.connect((self.arg[0],self.arg[1]))
      f=s.makefile('rw',BUFSIZE)
      s.sendall("PROCESS SPAMC/1.2\r\n")
      s.sendall("Content-length: "+str(len(buffer))+"\r\n")
      s.sendall("User: %s\r\n\r\n" % sa_user)
      s.sendall(buffer)
      s.shutdown(1)
      s.close()
      # check reply
      output1=f.readline().rstrip()
      debug.echo(4,"Spamd output: ",[[len(buffer)]],str(output1))
      # spamd returned any error?
      if not re.search('^SPAMD/[0-9.]* 0 ',output1):
        raise ScannerError,'SpamAssassind: '+output1
      # workaround for 8bit mails
      if re.search('^SPAMD/[0-9.]* 76 ',output1):
        debug.echo(3,"ERROR: spamd(): Spamd returned: "+output1)
        return 0.0,'',[]
      # spamd returned EX_OK
      content_length=0
      while True:
        output2=f.readline()
        if not output2.strip(): # end of spamd header?
          break
        reg1=re.search('^Content-length: ([0-9]*)$',output2.rstrip())
        if reg1:
          content_length=int(reg1.group(1))
        else:
          debug.echo(0,"ERROR: spamd(): unknown header: %s" % output2)
      # read reported message and close connection
      output=f.read(content_length)
      f.close()
      # process returned email's header
      try:
        header=self.reg_eoh(output).group(1)
      except:
        header=output
      pos=0
      ret=[]
      spam_yn,spam_score,spam_req='No',0.0,1000.0
      while pos<len(header):
        reg2=self.reg_x_spam(header[pos:])
        if reg2:
          line=reg2.group(1).rstrip()
          pos+=reg2.end(1)
          ret.append(line)
          reg3=self.reg_status(line)
          if reg3:
            spam_yn=reg3.group(1)
            spam_score=float(reg3.group(3))
            spam_req=float(reg3.group(5))
          if line[:10]=='X-Spam-Rep':
            debug.echo(5,"spamd(): ",line)
          else:
            debug.echo(4,"spamd(): ",line)
          # add header line if filter is not set
          if not self.FILTER:
            mail.xheader+=line+globals.EOL # not safe, use mail.addheader instead!
        else:
          break
      if self.REQSPAMLEVEL>=0:
        req_level=self.REQSPAMLEVEL
      else:
        req_level=spam_req
      if spam_score>=req_level:
        detected="SPAM"
      else:
        detected=''
      debug.echo(3,"Spamd status: [%s], score=%f/%f" \
        % (detected,spam_score,spam_req))
      # store spamassassin output if FILTER flag is set to a string
      if self.FILTER:
        mail.data=output
        mail.findbody()
      return float(spam_score/req_level),detected,ret
