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])