'''
Policy scanners for sagator

(c) 2005-2020 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

from avlib import *
from .match import match_any

__all__ = [
  'set_action',
  'greylist', 'not_listed', 'listed', 'elisted',
  'list_cleanup', 'auto_whitelist',
  'spf_check', 'dns_check', 'rbl_check',
  'add_listed',
  'policy_quota_auth_limit', 'policy_quota_cleanup',
  'geoip_country', 'geoip2_country'
]

class set_action(match_any):
  '''
  An interscanner to return a status to set an action to return.

  You can user this scanner to set default policy returned by policy scanner,
  or you can set policy according to scanner reply.

  Usage: set_action(action, *scanners)

  Where: action can be an action defined in postfix's SMTPD_POLICY_README,
           for example: dunno, defer_if_permit, ok, reject, ...

  New in version 0.8.0.
  '''
  is_policy_scanner = True
  name = 'set_action()'
  def __init__(self, action, *scanners):
      self.ACTION = tobytes(action)
      match_any.__init__(self, scanners)
  def scanbuffer(self, buffer, args={}):
      if not self.scanners:
        globals.ACTION = self.ACTION
        return 0.0, b'', []
      level, detected, ret = match_any.scanbuffer(self, buffer, args)
      if is_infected(level, detected):
        globals.ACTION = self.ACTION
      return level, detected, ret

class greylist(ascanner):
  '''
  A greylist policy scanner.

  This is an greylist implementation for sagator.
  You can learn more about greylisting at:
    http://www.salstar.sk/sagator/greylisting/

  Usage: greylist(min=300, expire=48*3600,
           info_url="http://www.salstar.sk/sagator/greylisting/")

  Where: min is minimum required seconds after an email is cleanly sent
           (default: 5 minutes)
         expire is record expiration time in seconds (default 48 hours).
           If this record will be passed after min time, expiration
           time will be disabled.
         info_url is an string, which is displayed to greylisted clients

  Example: greylist()

  New in version 0.8.0.
  '''
  is_policy_scanner = True
  name = 'greylist()'
  def __init__(self, min=300, expire=5*3600,
               info_url="http://www.salstar.sk/sagator/greylisting/"):
      self.MIN_DELAY = min
      self.EXPIRE_TIME = expire
      self.INFO_URL = info_url
  def get_key(self, dbc):
      key = [b'', b'', b'']
      try:
        key = [mail.policy_request.get(b'client_address'),
               mail.policy_request.get(b'sender'),
               mail.policy_request.get(b'recipient')]
      except:
        debug.echo(4, '%s: wrong request: %s' % (self.name, mail.policy_request))
        raise
      return key
  def update_counter(self, dbc, cntr, flags, t0, key):
      dbc.execute_cycle(
        "UPDATE greylist SET %s_count=%s_count+1, last_update=%d"
        " WHERE flags=%%s AND ip=%%s AND sender=%%s AND recipient=%%s"
        % (cntr, cntr, t0),
        [flags]+key)
  def scanbuffer(self, buffer, args={}):
      key = self.get_key(args['dbc'])
      t0 = time.time()
      args['dbc'].refresh()
      q = args['dbc'].query(
          "SELECT timestamp,pass_count,expire FROM greylist"
          " WHERE flags='GL' AND ip=%s AND sender=%s AND recipient=%s",
          key)
      debug.echo(8, "%s: %s %s" % (self.name, key, q))
      if q and (q[0][2]>t0 or q[0][2]<1): # one not-expired row returned
        # found a time in database
        t = q[0][0]
        if t0-t > self.MIN_DELAY:
          # delay is acceptable
          args['dbc'].execute_cycle(
            "UPDATE greylist SET expire=-1, last_update=%d,"
                               " pass_count=pass_count+1"
            " WHERE flags='GL' AND ip=%%s AND sender=%%s"
                  " AND recipient=%%s" % (t0),
            key)
          # add header if this is first pass
          if q[0][1]==0:
            globals.PREPEND='X-Sagator-Greylist: delayed %d seconds at %s; %s' \
                            % (t0-t, socket.gethostname(), time.strftime("%c"))
          return 0.0, b'', []
        else:
          self.update_counter(args['dbc'], 'block', 'GL', t0, key)
      elif q: # one row, but expired
        # remove all possible expired records
        t = t0
        q = args['dbc'].execute_cycle(
              "UPDATE greylist SET"
              "  timestamp=%d, expire=%d, created=%d, last_update=%d,"
              "  block_count=1, pass_count=1"
              " WHERE flags='GL' AND ip=%%s AND sender=%%s AND recipient=%%s"
              % (t0, t0+self.EXPIRE_TIME, t0, t0),
              key)
      else: # no rows (expired or not)
        # add new record for nonexistent key
        t = t0
        try:
          args['dbc'].execute_cycle(
            "INSERT INTO greylist "
                "(flags,timestamp,expire,created,last_update,"
                 "ip,sender,recipient,block_count,pass_count)"
            " VALUES ('GL',%d,%d,%d,%d,%%s,%%s,%%s,1,0)"
            % (t0, t0+self.EXPIRE_TIME, t0, t0), key)
        except args['dbc'].IntegrityError:
          globals.ACTION = 'defer_if_permit Greylisted (parallel connect)'
          return 1.0, b'greylisted', ['%s greylisted' % tostr(key[2])]
      globals.ACTION = 'defer_if_permit Greylisted for %d seconds (see %s)' \
        % (self.MIN_DELAY+t-t0, self.INFO_URL)
      return 1.0, b'greylisted', ['%s greylisted' % tostr(key[2])]

class listed(greylist):
  '''
  SQL blacklist for a policy.

  You can use this scanner to chcek, if an record is present in SQL table.

  Usage: listed(flags='B', table='greylist')

  Where: flags is an character, which defines which flag will be searched
           in SQL table. By default 'B' for this scanner.
         table can define table alternative

  Returns: level=1.0, when client is in blacklist
           level=0.0, when client is not in blacklist

  Example: listed()

  New in version 0.8.0.
  '''
  name = 'listed()'
  def __init__(self, flags='B', repeats=0, table='greylist'):
      self.FLAGS = flags.upper()
      self.TABLE = table
      self.REPEATS = repeats
  def scanbuffer(self, buffer, args={}):
      args['dbc'].refresh()
      key = self.get_key(args['dbc'])
      t0 = time.time()
      args['dbc'].execute_cycle(
        "UPDATE %s SET block_count=block_count+1, last_update=%d"
          " WHERE (expire<%d) AND (repeats>=%d) AND ("
          " (flags='%sA' AND %%s LIKE ip) OR"
          " (flags='%sS' AND %%s LIKE sender) OR"
          " (flags='%sR' AND %%s LIKE recipient))"
        % (self.TABLE, t0, t0, self.REPEATS, self.FLAGS, self.FLAGS, self.FLAGS),
        key)
      if args['dbc'].rowcount>0:
        globals.ACTION = 'reject You are blacklisted!'
        return 1.0, b'blacklisted', [b'blacklisted']
      return 0.0, b'', []

class elisted(greylist):
  '''
  SQL blacklist for a policy. Enhanced version.

  You can use this scanner to chcek, if an record is present in SQL table.

  Usage: elisted(flags='B', table='greylist')

  Where: flags is an character, which defines which flag will be searched
           in SQL table. By default 'B' for this scanner.
         table can define table alternative

  Returns: level=1.0, when client is in blacklist
           level=0.0, when client is not in blacklist

  This is experimental and may change in future releases.

  Example: elisted()

  New in version 1.1.1.
  '''
  name = 'elisted()'
  def __init__(self, flags='B', repeats=0, table='greylist'):
      self.FLAGS = flags.upper()
      self.TABLE = table
      self.REPEATS = repeats
  def scanbuffer(self, buffer, args={}):
      args['dbc'].refresh()
      key = self.get_key(args['dbc'])
      t0 = time.time()
      args['dbc'].execute_cycle(
        "UPDATE %s SET block_count=block_count+1, last_update=%d"
          " WHERE (expire<%d) AND (repeats>=%d) AND (flags='%s')"
            " AND (%%s LIKE ip)"
            " AND (%%s LIKE sender)"
            " AND (%%s LIKE recipient)"
        % (self.TABLE, t0, t0, self.REPEATS, self.FLAGS),
        key)
      if args['dbc'].rowcount>0:
        globals.ACTION = 'reject You are blacklisted!'
        return 1.0, b'blacklisted', [b'blacklisted']
      return 0.0, b'', []

class not_listed(greylist):
  '''
  SQL whitelist for a policy.

  Usage: not_listed(flags='W', table='greylist')

  Where: flags is an character, which defines which flag will be searched
           in SQL table. By default 'B' for this scanner.
         table can define table alternative

  Returns: level=1.0, when client is not in whitelist
           level=0.0, when client is in whitelist

  Example: not_listed()

  New in version 0.8.0.
  '''
  name = 'not_listed()'
  def __init__(self, flags='W', repeats=0, table='greylist'):
      self.FLAGS = flags.upper()
      self.TABLE = table
      self.REPEATS = repeats
  def scanbuffer(self, buffer, args={}):
      args['dbc'].refresh()
      key = self.get_key(args['dbc'])
      t0 = time.time()
      args['dbc'].execute_cycle(
        "UPDATE %s SET pass_count=pass_count+1, last_update=%d"
         " WHERE (expire<%d) AND (repeats>=%d) AND ("
          " (flags='%sA' AND %%s LIKE ip) OR"
          " (flags='%sS' AND %%s LIKE sender) OR"
          " (flags='%sR' AND %%s LIKE recipient))"
        % (self.TABLE, t0, t0, self.REPEATS, self.FLAGS, self.FLAGS, self.FLAGS),
        key)
      if args['dbc'].rowcount>0:
        debug.echo(4, "%s: %s whitelisted" % (self.name, tostr_list(key)))
        return 0.0, b'', []
      return 1.0, b'not_listed', [b'not listed']

class list_cleanup(greylist):
  '''
  Cleanup obsolete records from an SQL list.
  This cleanup is done not more than every 10 minutes to avoid server
  overload.

  Usage: list_cleanup(lifetime=36*24*3600, table='greylist')

  Where: lifetime is number of seconds after which records are removed (36 days)
         table can define table alternative

  Example: list_cleanup()

  New in version 0.8.0.
  '''
  name = 'list_cleanup()'
  INTERVAL = 600 # every 10-20 minutes (randomized)
  NEXT_ACTION = 0 # immediatelly
  def __init__(self, lifetime=36*24*3600, table='greylist'):
      self.LIFETIME = lifetime
      self.TABLE = table
      random.seed()
  def scanbuffer(self, buffer, args={}):
      t0 = time.time()
      if t0>self.NEXT_ACTION:
        self.NEXT_ACTION = t0 + self.INTERVAL + self.INTERVAL*random.random()
        args['dbc'].refresh()
        args['dbc'].execute_cycle(
          "DELETE FROM %s WHERE "
            "((last_update>1) AND (last_update<%d))"
            " OR ((expire>1) AND (expire<%d))"
            % (self.TABLE, t0-self.LIFETIME, t0))
        if args['dbc'].rowcount:
          debug.echo(3, "%s: %d expired rows deleted in %5.3f s"
                        % (self.name, args['dbc'].rowcount,
                           time.time()-t0))
      return 0.0, b'', []

class auto_whitelist(greylist):
  '''
  Whitelist IP addresses after sending some emails properly.
  This whitelisting is done not more than every 10 minutes to avoid server
  overload. This function is slow for large number of records, use it
  carefully.

  Usage: auto_whitelist(match_count=10, table='greylist')

  Where: match_count is number of record which will must match
         table can define table alternative

  Example: auto_whitelist(10)

  New in version 0.8.0.
  '''
  name = 'auto_whitelist()'
  INTERVAL = 600 # every 10 minutes
  def __init__(self, match_count=10, table='greylist'):
      self.MATCH_COUNT = match_count
      self.TABLE = table
      self.NEXT_ACTION = time.time()
  def scanbuffer(self, buffer, args={}):
      args['dbc'].refresh()
      t0 = time.time()
      if t0<self.NEXT_ACTION:
        return 0.0, b'', []
      self.NEXT_ACTION = t0 + self.INTERVAL
      for (ip, repeats) in args['dbc'].query(
          "SELECT ip, sum(pass_count+repeats) FROM %s WHERE flags='GL'"
                        " AND ip NOT IN (SELECT ip FROM %s WHERE flags='WA')"
          " GROUP BY ip HAVING sum(pass_count+repeats)>%d" \
          % (self.TABLE, self.TABLE, self.MATCH_COUNT)):
        try:
          debug.echo(3, "%s: Whitelisting %s, repeats=%d"
                        % (self.name, ip, repeats))
          args['dbc'].execute_cycle(
            "INSERT INTO %s (flags,timestamp,expire,ip,created,last_update)"
            " VALUES ('WA',%d,-1,%%s,%d,%d)"
            % (self.TABLE, t0, t0, t0), [ip])
        except Exception as e:
          # May be this record has been whitelisted from another process.
          # Ignore this error.
          debug.echo(8, "%s: Error auto-whitelisting %s: %s" \
                        % (self.name, ip, e))
      return 0.0, b'', []

# DNS and SPF tests

class dns_check(ascanner):
  '''
  IP to domain (and back) resolving checker.

  This scanner can be used to check senders IP address to resolve via
  DNS service. It can be uses asn real scanner or policy scanner.

  Usage: dns_check(reverse=False)

  Where: reverse can fe used to check also reverse records

  Example: dns_check()

  New in version 0.8.0.
  '''
  name = 'dns()'
  is_policy_scanner = True
  def __init__(self, reverse=False):
      self.REVERSE = reverse
  def getsender(self):
      # first try to use policy_request for policy scanners
      try:
        ip = mail.policy_request[b'client_address']
        sender = mail.policy_request[b'sender']
        helo = mail.policy_request[b'helo_name']
      except KeyError:
        # use standard method for content scanner
        ip = mail.getsender()['ADDR']
        sender = mail.sender
        try:
          helo = smtp.reg_helo_ehlo.search(mail.comm).group(1)
        except:
          helo = 'localhost'
      return ip, sender, helo
  def scanbuffer(self, buffer, args={}):
      ip, sender, helo = self.getsender()
      try:
        host = socket.gethostbyaddr(ip)
        debug.echo(4, "%s: %s" % (self.name, host))
        if self.REVERSE:
          rhost = socket.gethostbyname(host[0])
          debug.echo(4, "%s: %s" % (self.name, rhost))
      except socket.herror as err:
        (ec, es) = err.args
        if ec==1: # Unknown host
          return 1.0, b"%s for %s" % (tobytes(es), tobytes(ip)), \
                 [es, ip, sender, helo]
      return 0.0, b'', []

class spf_check(dns_check):
  '''
  SPF (Sender Permitted From) checker.

  This scanner can be used to scan for sender's SPF records in content
  scanner or policy scanner.

  Usage: spf_check(hard_error=False)

  Where: hard_error can be set to True, if you want errors raise as exceptions,
           by default it is turned off.

  Example: spf_check()

  New in version 0.8.0.
  '''
  name = 'spf()'
  is_policy_scanner = True
  def __init__(self, hard_error=False):
      try:
        import spf
      except ImportError:
        print("You need pyspf (or python-spf) package to run spf_check() scanner.")
        sys.exit(1)
      self.SPF = spf
      self.HARD_ERROR = hard_error
      # preload IDNA encoding
      import encodings.idna
  def scanbuffer(self, buffer, args={}):
      ip, sender, helo = self.getsender()
      ip = tostr(ip)
      sender = tostr(sender)
      helo = tostr(helo)
      debug.echo(6, 'spf_check(): %s[%s] %s' % (ip, helo, sender))
      result, status_code, explanation = self.SPF.check(ip, sender, helo)
      debug.echo(4, 'spf_check(): %s[%s] %s: %s %d %s' \
                 % (ip, helo, sender, result, status_code, explanation))
      if result=='deny':
        return 1.0, b'spf_hard_fail', ['%s: %s' % (result, explanation)]
      elif result=='unknown':
        return 0.8, b'spf_soft_fail', ['%s: %s' % (result, explanation)]
      elif self.HARD_ERROR and (result=='error'):
        raise ScannerError('%s: %s' % (result, explanation))
      return 0.0, b'', []

class rbl_check(dns_check):
  '''
  RBL (Real-time Blackhole list) checker.

  This scanner can be used to scan for sender's IP address contained
  in a blacklist.

  Usage: rbl_check(domains)

  Where: domains are blacklist domain names (multiple parameters allowed).

  Example: rbl_check('zen.spamhaus.org')

  New in version 1.1.0.
  '''
  name = 'rbl()'
  is_policy_scanner = True
  def __init__(self, *domains):
      self.DOMAINS = [tobytes(x) for x in domains]
  def scanbuffer(self, buffer, args={}):
      ip, sender, helo = self.getsender()
      debug.echo(6, '%s: %s' % (self.name, tostr(ip)))
      reversed_ip = b'.'.join(reversed(ip.split(b'.')))
      addrs = []
      for domain in self.DOMAINS:
        host = reversed_ip + b'.' + domain
        debug.echo(6, '%s: Testing %s ...' % (self.name, tostr(host)))
        try:
          addrs.append(
            "%s: %s" % (
              tostr(domain),
              #socket.gethostbyname(host)
              socket.getaddrinfo(host, 25, socket.AF_INET)[0][4][0]
            )
          )
        except socket.gaierror as e:
          debug.echo(4, "%s: socket.gaierror: %s" % (self.name, e))
        except socket.timeout as e:
          debug.echo(4, "%s: socket.timeout: %s" % (self.name, e))
        except socket.error as e:
          debug.echo(4, "%s: socket.error: %s" % (self.name, e))
      debug.echo(6, '%s: Result: %s' % (self.name, addrs))
      if addrs:
        return len(addrs), b'Found_in_RBL', addrs
      return 0.0, b'', []

# This scanner is not a policy scanner, but it is a content scanner.
# It can set a whitelist or blacklist for policy scanners.

class add_listed(match_any):
  '''
  Add one record into SQL list for a policy.

  Usage: add_listed(dbc, flags, expire, table, scanners)

  Where: dbc is an database connection
         flags is an string, which defines what and where to add. It can be:
             BA - to blacklist sender's IP address
             BS - to blacklist sender's email address
             BR - to blacklist recipients
             WA - to whitelist sender's IP address
             WS - to whitelist sender's email address
             WR - to whitelist recipients
         expire is an integer, which defines validity of a new record
           in seconds. Set it to -1 for infinite time.
         table can define table alternative

  Example: add_listed(db.sqlite(), 'BA', -1, 'greylist', b2f(libclam()))

  New in version 0.8.0.
  '''
  name = 'add_listed()'
  def __init__(self, dbc, flags, expire=-1, table='greylist', *scanners):
      self.dbc = dbc
      self.FLAGS = flags.upper()
      self.EXPIRE_TIME = expire
      self.TABLE = table
      match_any.__init__(self, scanners)
  def scanbuffer(self, buffer, args={}):
      level, detected, ret = match_any.scanbuffer(self, buffer, args)
      if not is_infected(level, detected):
        return level, detected, ret
      if mail.getsender()['ADDR'].lower()=='unknown':
        debug.echo(4, "add_listed(): no sender address specified: %s" \
                   % (mail.getsender()['ADDR']))
        return level, detected, ret
      try:
        keys = {
          'A': [[mail.getsender()['ADDR'], '', '']],
          'S': [['', mail.sender, '']],
          'R': [['', '', x] for x in mail.recip]
        }[self.FLAGS[1]]
      except KeyError:
        raise
      for key in keys:
        self.dbc.refresh()
        q = self.dbc.query(
            "SELECT count(*) FROM "+self.TABLE+" WHERE \
               flags=%s AND ip=%s AND sender=%s and recipient=%s",
            [self.FLAGS]+key)
        t0 = time.time()
        if self.EXPIRE_TIME<0:
          t_expire = 0
        else:
          t_expire = t0+self.EXPIRE_TIME
        if q[0][0]==0:
          self.dbc.execute_cycle(
            "INSERT INTO %s "
            "(flags,timestamp,expire,created,last_update,ip,sender,recipient)"
            " VALUES ('%s',%d,%d,%d,%d,%%s,%%s,%%s)"
            % (self.TABLE, self.FLAGS, t0, t_expire, t0, t0),
            key)
        else:
          self.dbc.execute_cycle(
            "UPDATE %s SET last_update=%d, expire=%d, repeats=repeats+1"
            " WHERE flags='%s' AND ip=%%s AND sender=%%s and recipient=%%s"
            % (self.TABLE, t0, t_expire, self.FLAGS),
            key)
      return level, detected, ret

## Policy quota
#################

class policy_quota_auth_limit(ascanner):
  '''
  A policy quota for authorized users.
  
  Limit number of emails from an authorized user.
  Do not use multiple of policy_quota_auth_limit tests in one policy service,
  use arrays for interval, max_conn, max_rcpt instead.

  Usage: policy_quota_auth_limit(interval, max_conn, max_rcpt)

  Where: interval - counting interval in seconds
         max_conn - maximum number of connections to server
         max_rcpt - maximum number of recipients
  
  Example: policy_quota_auth_limit(300, 10, 150)

  Postfix configuration example:
    smtpd_data_restrictions =  check_policy_service inet:127.0.0.1:30

  SQL command, which can help you to set proper parameters.
  Change 3600 with your current interval settings.
    SELECT username, sum(1) AS count, sum(recipient_count) AS rcpt,
           from_unixtime(timestamp/1000) AS time, group_concat(DISTINCT ip)
    FROM policy_quota
    GROUP BY username, floor(timestamp/1000/3600)
    ORDER BY count DESC, rcpt DESC;

  New in version 1.3.0.
  '''
  is_policy_scanner = True
  name = 'policy_quota_auth_limit()'
  precision = 1000 # milisecond
  def __init__(self, interval, max_conn, max_rcpt):
      if type(interval)==list or type(interval)==tuple:
        self.INTERVALS = list(zip(interval, max_conn, max_rcpt))
      else:
        self.INTERVALS = [[interval, max_conn, max_rcpt]]
  def scanbuffer(self, buffer, args={}):
      sasl_username = mail.policy_request.get(b"sasl_username")
      if not sasl_username:
        return 0.0, b'', []
      recipient_count = mail.policy_request.get(b"recipient_count", 1)
      ip = mail.policy_request.get(b'client_address')
      t0 = self.precision*time.time()
      args['dbc'].refresh()
      for interval, max_conn, max_rcpt in self.INTERVALS:
        q = args['dbc'].query(
              "SELECT sum(1),sum(recipient_count)"
              " FROM policy_quota"
              " WHERE username=%s AND timestamp>=%s",
              [sasl_username, t0-self.precision*interval]
            )
        debug.echo(8, "%s: %s %s" % (self.name, sasl_username, q))
        if not q:
          return 0.0, b'', []
        else:
          if (q[0][0] or 0)>=max_conn:
            return 1.0, b"POLICY_QUOTA_EXCEEDED_%d_%d_FOR_%s" % (
              max_conn, interval, sasl_username
            ), [
              "Over connection limit for %s [%d>%d/%d]"
              % (sasl_username, q[0][0], max_conn, interval),
              mail.policy_request
            ]
          if (q[0][1] or 0)>=max_rcpt:
            return 1.0, b"RCPT_QUOTA_EXCEEDED_%d_%d_FOR_%s" % (
              max_rcpt, interval, sasl_username
            ), [
              "Over recipient limit for %s [%d>%d/%d]"
              % (sasl_username, q[0][1], max_rcpt, interval),
              tostr(mail.policy_request)
            ]
      # insert
      try:
        args['dbc'].execute_cycle(
          "INSERT INTO policy_quota (timestamp, username, recipient_count, ip)"
          " VALUES (%s, %s, %s, %s)",
          (t0, sasl_username, recipient_count, ip)
        )
      except args['dbc'].IntegrityError as e:
        # This can happen only if request comes faster than precision.
        globals.ACTION = 'defer_if_permit Too fast, try again later!'
        return 1.0, b"TOO_FAST", [e]
      return 0.0, b'', []

class policy_quota_cleanup(policy_quota_auth_limit):
  '''
  Clean old records from SQL policy database table.

  Usage: policy_quota_cleanup(age=7*24, table="policy_quota")

  Where: age - record age in hours (by default 7 days)
         table - name of policy quota SQL table

  New in version 1.3.0.
  '''
  name='policy_quota_cleanup()'
  def __init__(self, age=7*24, table="policy_quota"):
      self.age = 3600000*age
      self.table_name = table
  def scanbuffer(self, buffer, args={}):
      t0 = time.time()
      args['dbc'].execute_cycle(
        "DELETE FROM %s WHERE timestamp<%%s" % self.table_name,
        [self.precision*t0 - self.age]
      )
      if args['dbc'].rowcount:
        debug.echo(3, "%s: %d old records deleted in %5.3f s"
                      % (self.name, args['dbc'].rowcount,
                         time.time()-t0))
      return 0.0, b'', []

class geoip_country(dns_check):
  '''
  GeoIP country match.

  Usage: geoip_country(country_codes, allow=True,
                       db="/usr/share/GeoIP/GeoIP.dat")

  Where: country_codes is a list of allowed/denied countries.
           Use upper case country names only!
         allow is an boolean, which defines if countries should
           be allowed or denied
         db is a full pathname to GeoIP.dat file

  Example: geoip_country(["SK"])

  New in version 1.3.1.
  '''
  def __init__(self, country_codes, allow=True,
               db="/usr/share/GeoIP/GeoIP.dat"):
      import GeoIP
      self.gi = GeoIP.open(db, GeoIP.GEOIP_STANDARD)
      self.country_codes = [x.upper() for x in country_codes]
      self.allow = allow
  def scanbuffer(self, buffer, args={}):
      ip, sender, helo = self.getsender()
      if ":" in ip:
        client_country = self.gi.country_code_by_addr_v6(ip)
      else:
        client_country = self.gi.country_code_by_addr(ip)
      debug.echo(4, "Client coutry: %s" % client_country)
      if client_country:
        if (client_country in self.country_codes)!=self.allow:
          return 0.0, b'', []
        else:
          return 1.0, b'Country_%s' % client_country, \
                 ['Country %s match' % client_country.decode()]
      return 0.0, b'', []

class geoip2_country(dns_check):
  '''
  GeoIP2 country match (using mmdb files).

  Usage: geoip2_country(country_codes, allow=True,
                        db="/usr/share/GeoIP/GeoLite2-Country.mmdb")

  Where: country_codes is a list of allowed/denied countries.
           Use upper case country codes only!
         allow is an boolean, which defines if countries should
           be allowed or denied
         db is a full pathname to an GeoIP.mmdb file

  Example: geoip2_country(["SK"])

  New in version 2.0.0.
  '''
  def __init__(self, country_codes, allow=True,
               db="/usr/share/GeoIP/GeoLite2-Country.mmdb"):
      from geoip2.database import Reader
      self.gip = Reader(db)
      self.country_codes = [x.upper() for x in country_codes]
      self.allow = allow
  def scanbuffer(self, buffer, args={}):
      ip, sender, helo = self.getsender()
      try:
        client_country = self.gip.country(ip).country.iso_code
      except Exception as err:
        debug.echo(4, "GeoIP2 error: %s" % err)
        client_country = "" # no match
      debug.echo(4, "Client coutry: %s" % client_country)
      if client_country:
        if (client_country in self.country_codes)!=self.allow:
          return 0.0, b'', []
        else:
          return 1.0, b'Country_%s' % client_country, \
                 ['Country %s match' % client_country.decode()]
      return 0.0, b'', []
