Please find attached a version of greylistd & Co ported to Python 3.

On Wed, 23 Oct 2019 02:33:26 +0000 [email protected] wrote:
> Source: greylistd
> Version: 0.8.8.7
> Severity: normal
> Tags: sid bullseye
> User: [email protected]
> Usertags: py2removal
>
> Python2 becomes end-of-live upstream, and Debian aims to remove
> Python2 from the distribution, as discussed in
> https://lists.debian.org/debian-python/2019/07/msg00080.html
>
> Your package either build-depends, depends on Python2, or uses Python2
> in the autopkg tests (the specific reason can be found searching this
> source package in
> https://people.debian.org/~morph/mass-bug-py2removal_take2.txt ).
> Please stop using Python2, and fix this issue by one of the following
> actions.
>
> - Convert your Package to Python3. This is the preferred option.  In
>   case you are providing a Python module foo, please consider dropping
>   the python-foo package, and only build a python3-foo package.  Please
>   don't drop Python2 modules, which still have reverse dependencies,
>   just document them.
>
>   This is the preferred option.
>
> - If the package is dead upstream, cannot be converted or maintained
>   in Debian, it should be removed from the distribution.  If the
>   package still has reverse dependencies, raise the severity to
>   "serious" and document the reverse dependencies with the BTS affects
>   command.  If the package has no reverse dependencies, confirm that
>   the package can be removed, reassign this issue to ftp.debian.org,
>   make sure that the bug priority is set to normal and retitle the
>   issue to "RM: PKG -- removal triggered by the Python2 removal".
>
> - If the package has still many users (popcon >= 300), or is needed to
>   build another package which cannot be removed, document that by
>   adding the "py2keep" user tag (not replacing the py2remove tag),
>   using the [email protected] user.  Also any
>   dependencies on an unversioned python package (python, python-dev)
>   must not be used, same with the python shebang.  These have to be
>   replaced by python2/python2.7 dependencies and shebang.
>
>   This is the least preferred option.
>
> If there are questions, please refer to the wiki page for the removal:
> https://wiki.debian.org/Python/2Removal, or ask for help on IRC
> #debian-python, or the [email protected] mailing list.
>
>
>

--

#!/usr/bin/python3
########################################################################
### FILE:       greylist
### PURPOSE:    Command line interface to "greylistd(8)"
###
### Copyright (C) 2004, Tor Slettnes <[email protected]>
###
### 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.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
###
### On Debian GNU/Linux systems, the complete text of the GNU General
### Public License can be found in `/usr/share/common-licenses/GPL'.
### It is also available at: http://www.gnu.org/licenses/gpl.html
########################################################################

from socket       import socket, AF_UNIX, SOCK_STREAM
from sys          import argv, stdout, stderr, exit
from configparser import ConfigParser


### Define values not yet availble in Python 2.1
False, True = (0 is 1), (1 is 1)


### Exit codes based on responses from greylistd
exitcodes = { "error:" : -1,
              "white"  : 0,
              "grey"   : 1,
              "black"  : 2,
              "true"   : 0,
              "false"  : 1 }


### File paths
conffile = "/etc/greylistd/config"
sockfile = "/var/run/greylistd/socket"

### Commands that can be given over the socket
commands = ("add", "delete", "check", "update",
            "stats", "list",  "clear", "save",
            "reload", "mrtg")
            


def usage (progname, message=None):
    if message:
        out = stderr
        out.write("%s: %s\n\n"%(progname, message))
    else:
        out = stdout
        out.write("Command line interface to \"greylistd(8)\"\n\n")

    out.write("\n".join([
        "Usage: %s --help"%progname,
        "       %s <action>"%progname,
        "",
        "Actions:",
        "  add [--white|--grey|--black] <data> ...",
        "    Add <data> to the specified list (\"--white\" if unspecified).",
        "",
        "  delete <data> ...",
        "    Remove <data> from any list.",
        "",
        "  check [--white|--grey|--black] <data> ...",
        "    Check the current status of <data>.",
        "",
        "  update [--white|--grey|--black] <data> ...",
        "    Check the current status of <data>; update lists accordingly.",
        "    This is how MTAs would normally use the command.",
        "",
        "  stats",
        "    Show some general list statistics.",
        "",
        "  mrtg",
        "    Show grey and white list statistics in a format that MRTG can use 
directly.",
        "",
        "  list [--white] [--grey] [--black]",
        "    Show all data items in the specified list(s).",
        "",
        "  save",
        "    Force an immediate dump of data to filesystem.",
        "",
        "  reload",
        "    Save data, then reload configuration and data.",
        "",
        "  clear [--white|--grey|--black]",
        "    Remove ALL data and statistics, i.e. reset greylistd(8).",
        "    This means that ALL new requests will initially receive",
        "    a \"grey\" response.  Use with caution!",
        "",
        ""]))

    exit((-1, 0)[not message])


progname = argv[0].split("/")[-1]

if len(argv) < 2:
    usage(progname, "No action specified.")


action = argv[1].lower()
if action in ("-h", "-help", "--help", "help"):
    usage(progname)

elif not action in commands:
    usage(progname, "Invalid action: '%s'"%action)



confParser = ConfigParser()
confParser.read(conffile)
try:
    sockfile = confParser.get("socket", "path")
except:
    pass
del confParser


sock = socket(AF_UNIX, SOCK_STREAM)

try:
    sock.connect(sockfile)
except Exception as e:
    stderr.write("%s: %s\n"%(sockfile, e[1]))
    exit(-1)

try:
    sock.send(" ".join(argv[1:]))
except Exception as e:
    stderr.write("%s: %s\n"%(sockfile, e[1]))
    exit(-1)


stat      = True
firstword = None

while stat:
    stat = sock.recv(1024)
    try:
        stdout.write("%s"%stat)

    except IOError:
        break
    
    else:
        if not firstword and stat.strip():
            firstword = stat.split(None, 1)[0]

else:
    try:
        stdout.write("\n")
    except IOError:
        pass


if firstword:
    exit(exitcodes.get(firstword, 0))
else:
    stderr.write("(No response from greylistd)\n")
    exit(-1)
#!/usr/bin/python3
########################################################################
### FILE:       greylistd.py
### PURPOSE:    Simple greylisting daemon.  See "greylistd(8)".
###             For an introduction to greylisting, see:
###             http://projects.puremagic.com/greylisting/
###
###             This program listens for connections on a UNIX domain
###             socket, presumably from an MTA such as Exim.  Nominally, 
###             it reads an identifier (referred to as a "triplet"),
###             and returns a single word ("white" or "grey") depending
###             on prior knowledge of said identifier.
###
### Copyright (C) 2004, Tor Slettnes <[email protected]>
###               2006, Sven Anderson <sven(at)anderson.de>
###
### 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.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
###
### On Debian GNU/Linux systems, the complete text of the GNU General
### Public License can be found in `/usr/share/common-licenses/GPL'.
### It is also available at: http://www.gnu.org/licenses/gpl.html
########################################################################

from time         import time, ctime, localtime, strftime
from socket       import socket, AF_UNIX, SOCK_STREAM, error as SocketError, 
gethostname
from os           import remove, rename, chmod, chown, getuid, getpid, isatty
from pwd          import getpwnam
from grp          import getgrnam
from os.path      import join, exists
from sys          import version as PyVersion, stdout, stderr, exit, exc_info
from signal       import signal, SIGTERM, SIGHUP, SIGUSR1, SIG_IGN, SIG_DFL
from syslog       import openlog, syslog, LOG_NOTICE, LOG_WARNING, LOG_ERR
from select       import select
from inspect      import getargspec


### Ensure that we can run this program
if PyVersion < "2.3":
    stderr.write("This program requires Python 2.3 or newer\n")
    exit(1)


### Configuration file sections, items
(DATA, STATEFILE, TRIPLETFILE, SAVETRIPLETS, UPDATEINTERVAL,
    SINGLECHECK, SINGLEUPDATE ) = (
    "data", "statefile", "tripletfile", "savetriplets", "update",
    "singlecheck", "singleupdate" )

(SOCKET, SOCKPATH, SOCKOWNER, SOCKMODE) = (
    "socket", "path", "owner", "mode")

(TIMEOUTS, RETRYMIN, RETRYMAX, EXPIRE) = (
    "timeouts", "retryMin", "retryMax", "expire")


### Defaults for various configuration items
conffile   = "/etc/greylistd/config"

config     = { DATA       : { STATEFILE      : "/var/lib/greylistd/states",
                              TRIPLETFILE    : "/var/lib/greylistd/triplets",
                              SAVETRIPLETS   : True,
                              SINGLECHECK    : False,
                              SINGLEUPDATE   : False,
                              UPDATEINTERVAL : 300 },

               SOCKET     : { SOCKPATH       : "/var/run/greylistd/socket",
                              SOCKOWNER      : "greylist:greylist",
                              SOCKMODE       : "0660" },

               TIMEOUTS   : { RETRYMIN       : 60 * 60,
                              RETRYMAX       : 60 * 60 * 8,
                              EXPIRE         : 60 * 60 * 24 * 60 } }


### Lists/states
(WHITE, GREY, BLACK)   = ("white", "grey", "black")

### Additional data file sections/items
(STATS, START, LASTSAVE) = ("statistics", "start", "lastsave")

### Greylist data
data       = { WHITE      : {},
               GREY       : {},
               BLACK      : {},
               STATS      : { WHITE   : 0,
                              GREY    : 0,
                              BLACK   : 0,
                              START   : 0,
                              LASTSAVE: 0 }}

### Type conversions for items in data file
datatypes  = { WHITE : (int, (int, int, int)),
               GREY  : (int, (int, int, int)),
               BLACK : (int, (int, int, int)) }

### Index of elements in data
(IDX_LAST, IDX_FIRST, IDX_COUNT) = range(3)

### Unhashed/original data not yet saved to disk
newTriplets = {}


class RunException (Exception):
    pass

class CommandError (RunException):
    pass


def expireKeys (now):
    for (listKey, timeoutKey) in ((GREY,  RETRYMAX),
                                  (WHITE, EXPIRE),
                                  (BLACK, EXPIRE)):
        for (dataKey, dataValue) in data[listKey].items():
            if dataValue[IDX_LAST] + config[TIMEOUTS][timeoutKey] < now:
                del data[listKey][dataKey]


def listStatus (searchkey):
    for listkey in datatypes:
        if searchkey in data[listkey]:
            return listkey

    return None



def log (message, priority=LOG_NOTICE):
    if isatty(stderr.fileno()):
        stderr.write("%s\n"%(message,))
    else:
        syslog(priority, message)


def duration (secs):
    plural = ("", "s")

    if secs < 60:
        return "%d second%s"%(secs, plural[secs > 1])

    elif secs < 60 * 60:
        (mins, secs) = (secs / 60, secs % 60)
        return "%s%s%s%s%s%s%s" % (mins, " minute", plural[mins != 1],
                                   secs and " and " or "",
                                   secs or "",
                                   secs and " second" or "",
                                   plural[secs > 1] or "")

    elif (secs + 30) < 60 * 60 * 24:
        (hrs, mins) = ((secs + 30) / 3600, ((secs + 30) / 60) % 60)
        return "%s%s%s%s%s%s%s" % (hrs, " hour", plural[hrs != 1],
                                   mins and " and " or "",
                                   mins or "",
                                   mins and " minute" or "",
                                   plural[mins > 1] or "")

    else:
        (days, hrs) = ((secs + 1800) / 86400, ((secs + 1800) / 3600) % 24)
        return "%s%s%s%s%s%s%s" % (days, " day", plural[days != 1],
                                   hrs and " and " or "",
                                   hrs or "",
                                   hrs and " hour" or "",
                                   plural[hrs > 1] or "")



def typeConvert(typeobject, string):
    if type(typeobject) in (tuple, list):
        typelist   = typeobject
        stringlist = string.split(None, len(typeobject) - 1)
        valuelist  = []

        for idx, typeobject in enumerate(typelist):
            word = stringlist[idx]
            valuelist.append(typeConvert(typeobject, word))

        return valuelist

    elif type(typeobject) is not type:
        return typeConvert(type(typeobject), string)

    elif typeobject is bool:
        try:
            if int(string):
                return True
            else:
                return False

        except ValueError:
            if string.lower() in ("yes", "true", "on"):
                return True
            elif string.lower() in ("no", "false", "off"):
                return False
            else:
                raise ValueError("Not a valid boolean: '%s'"%string)

    else:
        return(typeobject(string))




def loadFromFile (datafile, dictionary, typelist=None):
    try:
        fp      = file(datafile)
        section = None

        logPfx  = 'In %s:'%datafile

        for line in fp:
            line = line.strip()

            if line.startswith("#"):
                continue

            elif (line[0:1] == '[') and (']' in line):
                section = line[1:line.find(']')].strip().lower()

                if not section in dictionary:
                    log("%s Invalid or obsolete section: [%s]"%
                        (logPfx, section))
                    section = None

            elif section and ('=' in line):
                key, data = [s.strip() for s in line.split('=', 1)]

                if typelist and (section in typelist):
                    keytype, valuetype = typelist[section]

                elif key in dictionary[section]:
                    keytype      = str
                    valuetype    = dictionary[section][key]
                else:
                    log("%s Invalid or obsolete key: [%s] %s"%
                        (logPfx, section, key))
                    continue

                try:
                    key                      = typeConvert(keytype, key)
                    dictionary[section][key] = typeConvert(valuetype, data)

                except ValueError:
                    log("%s Invalid value for [%s] %s: '%s'"%
                        (logPfx, section, key, data))

                except IndexError:
                    log("%s Too few values for [%s] %s (%d, should be %d)"%
                        (logPfx, section, key, len(stringlist), len(typelist)))

            elif line:
                log("%s Invalid line: '%s'"%(logPfx, line))

        fp.close()

    except IOError as e:
        raise RunException("Cannot read from '%s': %s"%(datafile, e[1]))



def saveToFile (datafile, dictionary, perm=0600):
    try:
        fp = file(datafile, 'w')

        chmod(datafile, perm)
        
        for (section, subdict) in dictionary.items():
            fp.write("[%s]\n"%section)

            for (key, value) in subdict.items():
                if type(value) in (list, tuple):
                    value = " ".join(map(str, value))
                fp.write("%s = %s\n"%(key, value))

            fp.write("\n")

        fp.close()

    except IOError as e:
        raise RunException("Cannot write to %s: %s"%(datafile, e[1]))

    except OSError as e:
        raise RunException("Cannot set mode 0%o on %s: %s"%(perm, datafile, 
e[1]))



def loadConfigAndData ():
    now = int(time())

    try:
        loadFromFile(conffile, config)
    except RunException as e:
        log(str(e))

    try:
        loadFromFile(config[DATA][STATEFILE], data, datatypes)
    except RunException:
        data[STATS][START] = now

    expireKeys(now)
    data[STATS][LASTSAVE] = now
    


def saveData (datafile):
    ### Save data hashes and timestamps
    tempfile = "%s.%s"%(datafile, getpid())

    saveToFile(tempfile, data)
    
    try:
        rename(tempfile, datafile)
    except OSError as e:
        raise RunException("Cannot rename %s to %s: %s"%(tempfile,
                                                           datafile, e[1]))



def syncTriplets (datafile, perm=0600):
    source = datafile
    target = "%s.%s"%(source, getpid())

    try:
        infile  = file(source, "r")
    except IOError:
        infile  = None

    try:
        outfile = file(target, "w")

        chmod(target, perm)

        if infile:
            for line in infile:
                try:
                    (key, value) = line.split(" = ", 1)
                    key          = int(key)

                    if listStatus(key) and not key in newTriplets:
                        outfile.write(line)

                except ValueError:
                    continue

        for (key, data) in newTriplets.items():
            if listStatus(key):
                outfile.write("%d = %s\n"%(key, data))

        newTriplets.clear()
        outfile.close()

    except IOError as e:
        raise RunException("Could not write to %s: %s"%(target, e[1]))

    except OSError as e:
        raise RunException("Cannot set mode 0%o on %s: %s"%(perm, target, e[1]))


    if infile:
        infile.close()

    try:
        rename(target, source)
    except OSError as e:
        raise RunException("Could not rename %s to %s: %s"%(target, source, 
e[1]))



def listTriplets (fp, socket, options):
    parseErrors = False
    firstPass   = True
    listformat  = "  %-20s %5s  %s\n"

    for listkey in (options or datatypes):
        if not listkey in data:
            raise CommandError("Invalid list: %s"%listkey)

        elif not data[listkey]:
            continue

        listdata = data[listkey]
        line     = "%slist data:"%listkey.capitalize()
        dash     = "="*len(line)
        client.send("\n%s\n%s\n"%(line, dash))
        client.send(listformat%("Last Seen", "Count", "Data"))

        fp.seek(0)
        for line in fp:
            try:
                (key, value) = line.split(" = ", 1)
                key          = int(key)

                if key in listdata:
                    last, first, num = listdata[key]
                    ldate = strftime("%Y-%m-%d %H:%M:%S", localtime(last))
                    client.send(listformat%(ldate, num, value.strip()))
            
            except ValueError:
                if not parseErrors:
                    log("While reading triplets from %s:"%
                        (config[DATA][TRIPLETFILE],))
                    parseErrors = True

                if firstPass:
                    log("Invalid line: '%s'"%line)

        firstPass = False



def do_add (options, key):
    if not options:
        state = WHITE
    elif options[0] in datatypes:
        state = options[0]
    else:
        raise CommandError("No such list: '%s'"%options[0])

    now       = int(time())
    oldstate  = listStatus(key)

    if state == oldstate:
        (lastseen, firstseen, count) = data[state][key]

    else:
        (lastseen, firstseen, count) = (now, now, 0)
        data[STATS][state] += 1
        if oldstate:
            del data[oldstate][key]

    data[state][key] = (now, firstseen, count+1)
    return "Added to %slist"%state



def do_delete (options, key):
    listkey = listStatus(key)
    if listkey:
        del data[listkey][key]
        return "Removed from %slist"%listkey
    else:
        raise CommandError("Not found")


def do_check (options, args, update=False):
    if options:
        truthtest = options[0]
        if not truthtest in datatypes:
            raise CommandError("'%s' is not a known state"%truthtest)
    else:
        truthtest = None

    now       = int(time())
    ### according to #375504 this call to expireKeys() can be
    ### removed here, expiration takes place when periodically
    ### saving to disk
    ### #375504: expireKeys(now)
    state = None

    if config[DATA][SINGLECHECK]:
        key = hash(args[0])
        state     = listStatus(key)

    if state is None:
        key = hash(" ".join(args))
        state     = listStatus(key)

    if config[DATA][SINGLEUPDATE]:
        key = hash(args[0])
        triplet = args[0]
    else:
        key = hash(" ".join(args))
        triplet = " ".join(args)

    if state is None:
        state = GREY
        if update and config[DATA][TRIPLETFILE] and config[DATA][SAVETRIPLETS]:
            newTriplets[key] = triplet.lower()

    elif ((state == GREY) and 
          (data[GREY][key][IDX_FIRST] + config[TIMEOUTS][RETRYMIN] < now)):
        state = WHITE

    if update:
        do_add([ state ], key)

    if truthtest:
        return (state == truthtest) and "true" or "false"
    else:
        return state



def do_update (options, args):
    return do_check(options, args, update=True)



def do_stats ():
    text      = []
    now       = int(time())
    stats     = data[STATS]
    starttime = stats.get(START, None)
    expireKeys(now)


    if starttime:
        title = "Statistics since %s (%s ago)"%(
            ctime(starttime), duration(now - starttime))
    else:
        title = "Statistics"

    text.append(title)
    text.append("-" * len(title))
    hits    = {}
    items   = {}

    for listkey in datatypes:
        items[listkey] = len(data[listkey])
        hits[listkey]  = 0
        for (key, value) in data[listkey].items():
            (lastseen, firstseen, count) = value
            hits[listkey] += count


    for listkey in datatypes:
        hitdigits  = len(str(max(hits.values())))
        itemdigits = len(str(max(items.values())))

        text.append("%s items, matching %s requests, are currently %slisted"%
                    (str(items[listkey]).rjust(itemdigits),
                     str(hits[listkey]).rjust(hitdigits),
                     listkey))


    previousGrey  = stats[GREY] - len(data[GREY])
    expiredGrey   = previousGrey - stats[WHITE]

    if previousGrey:
        digits = len(str(previousGrey))

        text.append("")
        text.append("Of %s items that were initially greylisted:"%
                    str(previousGrey).rjust(digits))

        text.append(" - %s (%5.1f%%) became whitelisted"%
                    (str(stats[WHITE]).rjust(digits),
                     100.0 * stats[WHITE] / previousGrey))

        text.append(" - %s (%5.1f%%) expired from the greylist"%
                    (str(expiredGrey).rjust(digits),
                     100.0 * expiredGrey / previousGrey))

    text.append('')
    return "\n".join(text)



def do_mrtg ():
    text      = []
    now       = int(time())
    stats     = data[STATS]
    starttime = stats.get(START, None)
    expireKeys(now)


    text.append(str(stats[GREY]))
    text.append(str(stats[WHITE]))

    text.append(duration(now - starttime))
    text.append(gethostname())
    
    text.append('')
    return "\n".join(text)



def do_list (options, socket):
    if not config[DATA][TRIPLETFILE] or not config[DATA][SAVETRIPLETS]:
        raise CommandError("Original triplet data is not retained.")

    do_save()

    try:
        infile = file(config[DATA][TRIPLETFILE], "r")
        error = listTriplets(infile, socket, options)
        infile.close()

    except IOError as e:
        raise CommandError("Cannot read from '%s': 
%s\n"%(config[DATA][TRIPLETFILE], e[1]))
    

def do_clear (options):
    for listkey in (options or datatypes):
        if listkey in data:
            data[listkey].clear()
            data[STATS][listkey] = 0
        else:
            raise CommandError("Invalid list: '%s'"%listkey)

    if not options:
        data[STATS][START] = int(time())

    return "data and statistics cleared"


def do_reload ():
    do_save()
    loadConfigAndData()
    return "configuration and data reloaded"


def do_save ():
    now = int(time())
    expireKeys(now)

    ### Save data hashes and timestamps
    saveData(config[DATA][STATEFILE])

    ### Save unhashed triplets
    if newTriplets:
        syncTriplets(config[DATA][TRIPLETFILE])

    data[STATS][LASTSAVE] = now

    return "greylistd data has been saved"



def nodata ():
    raise CommandError("No data received")


def runCommand (line, client):
    now     = int(time())
    words   = line.lower().split()
    options = []

    if not words:
        function = nodata
    elif "do_"+words[0] in globals():
        function = globals()["do_"+words.pop(0)]
    else:
        function = do_update

    args, varargs, varkw, defaults =  getargspec(function)

    while words and words[0].startswith("-"):
        options.append(words.pop(0).lstrip("-"))

    arglist = []
    key     = None
    useargs = False

    for arg in args:
        if arg == "options":
            arglist.append(options)

        elif arg == "key":
            key = hash(" ".join(words))
            useargs = True
            arglist.append(key)

        elif arg == "args":
            useargs = True
            arglist.append(words)

        elif arg == "socket":
            arglist.append(client)

        else:
            break

    try:
        if useargs and not words:
            raise CommandError("Missing argument")

        if words and not useargs:
            raise CommandError("Too many arguments")

        if key and config[DATA][TRIPLETFILE] and config[DATA][SAVETRIPLETS]:
            newTriplets[key] = " ".join(words).lower()

        return function(*tuple(arglist)) or ""


    except RunException as e:
        if not isinstance(e, CommandError):
            log(str(e))

        return "error: %s"%str(e).replace("\n", "\n       ")



def createSocket (path, owner, mode):
    sock = socket(AF_UNIX, SOCK_STREAM)
    sock.bind(path)

    if getuid() == 0:
        try:
            if ":" in owner:
                user, group = owner.split(":", 1)
            else:
                user, group = owner, None

            (name, passwd, uid, gid, gecos, dir, shell) = getpwnam(user)

            if group:
                (name, passwd, gid, members) = getgrnam(group)

        except KeyError:
            raise RunException("Invalid owner specified in configuration file: 
%s: %s"%owner)

        try:
            chown(path, uid, gid)
        except OSError as e:
            raise RunException("Could not change ownership of socket %s: 
%s"%(path, e[1]))

    try:
        chmod(path, int(mode, 8))

    except ValueError:
        raise RunException("Specified socket mode '%s' is not a valid octal 
number"%mode)
    except OSError as e:
        raise RunException("Could not set mode 0%o on socket %s: %s"%(mode, 
path, e[1]))

    sock.listen(5)
    return sock



def startup ():
    global listener, sockets
    listener = None
    sockets  = []

    signal(SIGTERM, term)
    signal(SIGHUP,  hangup)
    openlog("greylistd")

    loadConfigAndData()

    try:
        listener = createSocket(**config[SOCKET])

    except SocketError as e:
        log("Could not bind/listen to socket %s: %s"%(config[SOCKET][SOCKPATH], 
str(e)))
        exit(-1)

    except RunException as e:
        log(str(e))
        cleanup(False)
        exit(-1)

    sockets  = [ listener ]




def cleanup (save=True):
    if exists(config[SOCKET][SOCKPATH]):
        remove(config[SOCKET][SOCKPATH])

    if save:
        do_save()


def term (signum=None, frame=None):
    cleanup()
    exit(0)


def hangup (signum=None, frame=None):
    do_reload()


startup()

try:
    while sockets:
        interval = config[DATA][UPDATEINTERVAL]
        lastsave = data[STATS][LASTSAVE]
        
        if interval and (lastsave + interval < time()):
            (inlist, outlist, errlist) = select(sockets, [], [], 0)
        else:
            (inlist, outlist, errlist) = select(sockets, [], [])


        if not inlist:
            try:
                do_save()
            except RunException as e:
                log(str(e))


        elif inlist[0] is listener:
            (client, addr) = listener.accept()
            sockets.append(client)

        else:
            client = inlist[0]

            try:
                line = client.recv(16384)
                reply = runCommand(line, client)
                client.send(reply)

            except SocketError as e:
                log("Socket error: %s"%e[1])

            client.close()
            sockets.remove(client)


except SystemExit:
    pass

except KeyboardInterrupt:
    cleanup()

except Exception as e:
    (type, value, tb) = exc_info()
    while tb.tb_next:
        tb       = tb.tb_next

    frame    = tb.tb_frame
    code     = frame.f_code
    line     = frame.f_lineno
    filename = code.co_filename
            
    log("### Fatal event in %s, line %d:"%(filename, line), LOG_ERR)
    log(">>> %s"%e, LOG_ERR)

    cleanup(save=False)
#!/usr/bin/python3
########################################################################
### FILE:       greylist-setup-exim4
### PURPOSE:    Add a greylisting statement to Exim 4 configuration file
########################################################################

from sys       import version, stdin, stderr, argv, exit
from os.path   import isdir, join, exists
from os        import listdir, spawnl, P_WAIT
from re        import compile

### Ensure that we can run this program
if version < "2.3":
    stderr.write("This program requires Python 2.3 or newer\n")
    exit(1)


### What files/ACLs do we want to edit by default?
exim4conf_default_places = (
    ("/etc/exim4/exim4.conf.template",                   "acl_check_rcpt"),
    ("/etc/exim4/exim4.conf.template",                   "acl_check_data"),
    ("/etc/exim4/conf.d/acl/30_exim4-config_check_rcpt", "acl_check_rcpt"),
    ("/etc/exim4/conf.d/acl/40_exim4-config_check_data", "acl_check_data"))


### Words that separate blocks in the Exim 4 configuation file
exim4conf_blocks = [
    "begin", "accept", "defer", "deny", "discard", "drop", "require", "warn" ]

### What blocks do we remove when unconfiguring greylistd?
### Every line in the block must match at least one item in this list,
### and every item in the list must match at least one line in the block
exim4conf_remove_block = [ '^\s*defer',  '/var/run/greylistd/socket' ]


### What text do we add to which ACLs?
exim4conf_texts = {
    "rcpt" : '''  # greylistd(8) configuration follows.
  # This statement has been added by "greylistd-setup-exim4",
  # and can be removed by running "greylistd-setup-exim4 remove".
  # Any changes you make here will then be lost.
  # 
  # Perform greylisting on incoming messages from remote hosts.
  # We do NOT greylist messages with no envelope sender, because that
  # would conflict with remote hosts doing callback verifications, and we
  # might not be able to send mail to such hosts for a while (until the
  # callback attempt is no longer greylisted, and then some).
  #
  # We also check the local whitelist to avoid greylisting mail from
  # hosts that are expected to forward mail here (such as backup MX hosts,
  # list servers, etc).
  #
  # Because the recipient address has not yet been verified, we do so
  # now and skip this statement for non-existing recipients.  This is
  # in order to allow for a 550 (reject) response below.  If the delivery
  # happens over a remote transport (such as "smtp"), recipient callout
  # verification is performed, with the original sender intact.
  #
  defer
    message        = $sender_host_address is not yet authorized to deliver \\
                     mail from <$sender_address> to <$local_part@$domain>. \\
                     Please try later.
    log_message    = greylisted.
    !senders       = :
    !hosts         = : +relay_from_hosts : \\
                     ${if exists {/etc/greylistd/whitelist-hosts}\\
                                 {/etc/greylistd/whitelist-hosts}{}} : \\
                     ${if exists {/var/lib/greylistd/whitelist-hosts}\\
                                 {/var/lib/greylistd/whitelist-hosts}{}}
    !authenticated = *
    !acl           = acl_local_deny_exceptions
    !dnslists      = ${if exists {/etc/greylistd/dnswl-known-good-sender}\
                                 
{${readfile{/etc/greylistd/dnswl-known-good-sender}}}{}}
    domains        = +local_domains : +relay_to_domains
    verify         = recipient
    condition      = ${readsocket{/var/run/greylistd/socket}\\
                                 {--grey \\
                                  %s \\
                                  $sender_address \\
                                  $local_part@$domain}\\
                                 {5s}{}{false}}

 # Deny if blacklisted by greylist
 deny
   message = $sender_host_address is blacklisted from delivering \\
                     mail from <$sender_address> to <$local_part@$domain>.
   log_message = blacklisted.
   !senders        = :
   !authenticated = *
   domains        = +local_domains : +relay_to_domains
   verify         = recipient
   condition      = ${readsocket{/var/run/greylistd/socket}\\
                                 {--black \\
                                  $sender_host_address \\
                                  $sender_address \\
                                  $local_part@$domain}\\
                                 {5s}{}{false}}



''',

    "data" : '''  # greylistd(8) configuration follows.
  # This statement has been added by "greylistd-setup-exim4",
  # and can be removed by running "greylistd-setup-exim4 remove".
  # Any changes you make here will then be lost.
  # 
  # Perform greylisting on incoming messages with no envelope sender here.
  # We did not subject these to greylisting after RCPT TO:, because that
  # would interfere with remote hosts doing sender callout verifications.
  #
  # Because there is no sender address, we supply only two data items:
  #  - The remote host address
  #  - The recipient address (normally, bounces have only one recipient)
  #
  # We also check the local whitelist to avoid greylisting mail from
  # hosts that are expected to forward mail here (such as backup MX hosts,
  # list servers, etc).
  #
  defer
    message        = $sender_host_address is not yet authorized to deliver \\
                     mail from <$sender_address> to <$recipients>. \\
                     Please try later.
    log_message    = greylisted.
    senders        = :
    !hosts         = : +relay_from_hosts : \\
                     ${if exists {/etc/greylistd/whitelist-hosts}\\
                                 {/etc/greylistd/whitelist-hosts}{}} : \\
                     ${if exists {/var/lib/greylistd/whitelist-hosts}\\
                                 {/var/lib/greylistd/whitelist-hosts}{}}
    !authenticated = *
    !acl           = acl_local_deny_exceptions
    condition      = ${readsocket{/var/run/greylistd/socket}\\
                                 {--grey \\
                                  %s \\
                                  $recipients}\\
                                  {5s}{}{false}}

 # Deny if blacklisted by greylist
 deny
   message = $sender_host_address is blacklisted from delivering \\
                     mail from <$sender_address> to <$recipients>.
   log_message = blacklisted.
   !senders        = :
   !authenticated = *
   condition      = ${readsocket{/var/run/greylistd/socket}\\
                                 {--black \\
                                  $sender_host_address \\
                                  $recipients}\\
                                  {5s}{}{false}}


'''
    }



class RunException (Exception):
    pass


def find_block (lines, decl, rxList, commentPfx="#", blocks=exim4conf_blocks):
    startComment = None
    startBlock   = None
    region       = None
    currentDecl  = None
    rxDecl       = compile(r"^\s*(\w+):")

    rxFound      = {}
    for rx in rxList:
        rxFound[compile(rx)] = False


    for index in range(len(lines)):
        line  = lines[index].strip()

        if not line:
            pass

        elif line.startswith(commentPfx):
            if startComment is None:
                startComment = index

        elif rxDecl.search(line):
            if region:
                return region

            startComment = None
            startBlock   = None
            currentDecl  = rxDecl.search(line).group(1)

        elif currentDecl == decl:
            if line.split()[0] in blocks:
                if region:
                    return region

                elif startComment is not None:
                    startBlock = startComment

                else:
                    startBlock = index

                rxFound = {}.fromkeys(rxFound, False)

            for rx in rxFound:
                if rx.search(line):
                    rxFound[rx] = True

            startComment = None


        if (startBlock is None) or not min(rxFound.values()):
            region = None

        elif startComment is None:
            region = (startBlock, index+1)

    return region


def exim4_deconfigure (lines, aclname, options):
    region = find_block(lines, aclname, exim4conf_remove_block)

    if region:
        del lines[region[0]:region[1]]
        return "OK"
    else:
        raise RunException("Not Configured")


def exim4_configure (lines, aclname, options):
    if find_block(lines, aclname, exim4conf_remove_block):
        raise RunException("Already Configured")

    acltype = options.get("acltype", None)
    
    if not acltype:
        for knowntype in exim4conf_texts.keys():
            if knowntype in aclname:
                acltype = knowntype
                break
        else:
            raise RunException(("I cannot guess what type of ACL '%s' is.\n"
                   "Please supply '-acltype={rcpt|data}'.")%aclname)

    elif not acltype in exim4conf_texts:
        raise RunException("Invalid ACL type: '%s'"%acltype)
        

    if "netmask" in options:
        try:
            netmask = int(options["netmask"])
        except:
            nmstring=options["netmask"]
            raise RuntimeError("Invalid netmask size: '%(nmstring)s'"%vars())
### org           raise "Invalid netmask size: '%s'"%options["netmask"]

        hostaddress="${mask:$sender_host_address/%s}"%options["netmask"]

    else:
        hostaddress="$sender_host_address"

    text = exim4conf_texts[acltype]%hostaddress

    for lineno in range(len(lines)):
        if lines[lineno].strip().startswith(aclname + ":"):
            lines[lineno+1:lineno+1] = text.splitlines(True)
            return "OK"

    else:
        raise RunException("Could not find ACL '%s'"%aclname)


def run (command, *args):
    return (spawnl(P_WAIT, command, command.split("/")[-1], *args) == 0)


def exim4_setup (filename, aclname, function, options, doupdate):
    try:
        fp = open(filename, "r")
        lines = fp.readlines()
        fp.close()

        message = function(lines, aclname, options)

        if doupdate:
            fp = file(filename, "w")
            fp.writelines(lines)
            fp.close()

        ok = True

    except IOError as e:
        ok, message = False, e[1]


    except RunException as e:
        ok, message = False, e[0]
    

    if not "quiet" in options:
        if len(filename) > 40:
            name = "..." + filename[-37:]
        else:
            name = filename.ljust(40)
        stderr.write("%s: %s\n"%(name, message))


    return ok or ("no-fail" in options)



def exim4_default_setup (description, function, options, doupdate):
    if not "quiet" in options:
        stderr.write("%s\n"%description)

    results = {}

    for (filename, aclname) in exim4conf_default_places:
        ok = exim4_setup(filename, aclname, function, options, doupdate)
        results[aclname] = results.get(aclname, False) or ok

    return min(results.values())





def usage (progname, message=None):
    if message:
        stderr.write("%s: %s\n"%(progname, message))

    stderr.write("\n".join([
        "Usage: %s {add|remove|test} [options] [<file> <acl_name>]"%progname,
        "",
        "  Add, remove or test for greylistd support in the given Exim 4",
        "  configuration file and Access Control List (ACL).",
        "",
        "  If no file or ACL name is supplied, changes are made to the",
        "  default Exim 4 configuration files for your distribution.",
        "",
        "  -quiet",
        "      Do not print anything to standard output.",
        "  -no-fail",
        "      Exit status is zero even on failure",
        "  -no-reload",
        "      Do not tell Exim4 to reload configuration after add / remove.",
        "  -netmask=<bits>",
        "      Filter the remote host address though a netmask of the given",
        "      size (useful values are between 16 and 31) before it is passed",
        "      to greylistd.  Hosts within the same network are then pooled",
        "      together as if they represented a single host.",
        "  -acltype={rcpt|data}",
        "      Insert a statement suitable for an ACL used to validate the",
        "      SMTP 'RCPT TO:' command or the message DATA, respectively.",
        "      This is implicit when the supplied ACL name contains either of",
        "      the substrings 'rcpt' or 'data' (such as Debian's default",
        "      'acl_check_rcpt' and 'acl_check_data'); otherwise this option",
        "      has to be present for the 'add' command",
        ""]))

    exit((-1, 0)[message == None])


progname = None
action   = None
filename = None
aclname  = None
options  = {}


for arg in argv:
    if progname is None:
        progname = arg.split("/")[-1]

    elif arg.startswith("-"):
        try:
            (option, value) = arg.lstrip("-").split("=", 1)
        except:
            (option, value) = arg.lstrip("-"), None
            
        options[option] = value

    elif not action:
        action = arg.lower()

    elif not filename:
        filename = arg

    elif not aclname:
        aclname = arg

    else:
        usage(progname, "Too many arguments")


if action == "add":
    function, doupdate = exim4_configure, True
    description = "Adding greylistd support to Exim 4 configuration files"

elif action == "remove":
    function, doupdate = exim4_deconfigure, True
    description = "Removing greylistd support from Exim 4 configuration files"

elif action == "test":
    function, doupdate = exim4_deconfigure, False
    description = "Testing for greylistd support in Exim 4 configuration files"

elif action in (None, "help"):
    usage(progname)

else:
    usage(progname, "Invalid action: %s"%action)


if aclname:
    ok = exim4_setup(filename, aclname, function, options, doupdate)

elif filename:
    usage(progname, "If you supply a file name, you must also supply an ACL")

else:
    ok = exim4_default_setup(description, function, options, doupdate)


if ok and doupdate and not ("no-reload" in options):
    run("/usr/sbin/invoke-rc.d", "exim4", "reload")


exit((0, -1)[not ok])

Reply via email to