initial upload

This commit is contained in:
whottgen
2004-09-20 19:34:09 +00:00
parent 5de3c72eab
commit 6f06324736
41 changed files with 5416 additions and 0 deletions

37
smmapd_prototype/Cache.py Normal file
View File

@ -0,0 +1,37 @@
import threading
import time
from Logging import *
class Cache(object):
def __init__(self, expiration):
self.lock = threading.Lock()
self.expiration = expiration
self.cache = {}
def put(self, key, value):
self.lock.acquire()
self.cache[key] = (time.time(), value)
debug("cache.put(%s, %s)" % (str(key), str(value)))
self.lock.release()
def get(self, key):
try:
self.lock.acquire()
debug("cache.get(%s)" % str(key))
try:
timestamp, value = self.cache[key]
debug("cache.get found: %s" % value)
if (timestamp + self.expiration) < time.time():
debug("cache.get: expired")
del self.cache[key]
raise KeyError
return value
except KeyError:
debug("cache.get: found nothing")
return None
finally:
self.lock.release()

View File

@ -0,0 +1,27 @@
from Logging import *
from SendmailSocketMapHandler import smmapBaseHandlerWorker
from SendmailSocketMapHandler import smmapBaseHandlerContainer
from SendmailSocketMapHandler import MyPermanentVerifierException
from SendmailSocketMapHandler import MyTemporaryVerifierException
from VerifierHandler import MySMTP
class MyLMTP(MySMTP):
def lhlo(self, param):
return self.docmd("lhlo " + param)
class CyrusCheckerWorker(smmapBaseHandlerWorker):
OK = "OK"
NOK = "NOK"
TEMPNOK = "TEMPNOK"
def execute(self, data):
debug("data " + data)
host, address = data.split('|')
debug("host: (%s), address: (%s)" % (host, address))
return "<OK>"

View File

@ -0,0 +1,14 @@
import syslog
config = None
def log(data):
syslog.syslog(syslog.LOG_INFO, data)
def debug(data):
syslog.syslog(syslog.LOG_DEBUG, data)
def openlog(c):
config = c
syslog.openlog(config.get('Logging', 'ApplID'), syslog.LOG_PID, syslog.LOG_MAIL)

View File

@ -0,0 +1,8 @@
Cache.py
Logging.py
SendmailSocketMapHandler.py
VerifierHandler.py
setup.py
smmapd
smmapd.ini
verifysender.m4

View File

@ -0,0 +1,190 @@
import SocketServer
import time
from Logging import *
class NetStringError(ValueError): pass
def NetStringDecode(s):
try:
length, data = s.split(':')
except ValueError:
raise NetStringError, "Separator not found"
try:
length = int(length)
except ValueError:
raise NetStringError, "Can not read length"
if len(data) != length+1:
raise NetStringError, "Data has unexpected length"
if data[-1] != ',':
raise NetStringError, "End-delimiter not found"
return data[:-1]
def NetStringEncode(s):
return str(len(s)) + ":" + s + ","
class MyPermanentVerifierException(ValueError): pass
class MyTemporaryVerifierException(ValueError): pass
class MyBaseRequestHandler(SocketServer.BaseRequestHandler):
def handle(self):
debug("Connected from " + str(self.client_address))
self.localSetup()
while 1:
receivedData = self.request.recv(8192)
if (receivedData == None) or (len(receivedData) == 0): break
debug("Data: (%s)" % receivedData)
self.request.sendall(self.process(receivedData))
self.request.close();
self.localFinish()
debug("Disconnected")
def process(self, data):
debug("MyBaseRequestHandler.process")
return data
def localSetup(self): pass
def localfinish(self): pass
class SendmailAdaptor:
PERM = "PERM "
OK = "OK "
NOTFOUND = "NOTFOUND "
TEMP = "TEMP "
def preProcess(self, data):
try:
data = NetStringDecode(data)
klass, data = data.split(' ')
return klass, data
except NetStringError, arg:
raise MyPermanentVerifierException, arg
except ValueError:
raise MyPermanentVerifierException, "<class> <data> expected, only one found"
def postProcess(self, data):
return NetStringEncode(data)
def process(self, data):
startTime = time.time()
try:
klass, data2 = self.preProcess(data)
arg = self.execute(klass, data2)
code = SendmailAdaptor.OK
except MyPermanentVerifierException, arg:
code, arg = SendmailAdaptor.PERM, str(arg)
except MyTemporaryVerifierException, arg:
code, arg = SendmailAdaptor.TEMP, str(arg)
endTime = time.time()
log("Class: %s, Data: %s, Code: %s, Arg: %s, Delay: %f" % (klass, data2, code, arg, endTime-startTime))
return self.postProcess(code + arg)
def execute(self, data):
return data
class NullAdaptor(SendmailAdaptor):
def preProcess(self, data):
return re.compile(r'^(.*?)[\r\n]{1,2}$').match(data).group(1)
def postProcess(self, data):
return data + "\n"
class SendmailDispatcher(SendmailAdaptor, MyBaseRequestHandler):
pluginContainerObjects = {}
def registerAll(config):
for section in config.get('Daemon', 'Plugins').split(','):
SendmailDispatcher.register(section, config)
registerAll = staticmethod(registerAll)
def register(section, config):
cfg = Config(section, config)
className = cfg.get('ContainerClass')
moduleName = cfg.get('ContainerModule')
if className == None:
className = 'smmapBaseHandlerContainer'
else:
if moduleName == None:
moduleName == className
m = __import__(moduleName)
log("Registering %s, %s" % (section, className))
klass = eval("m.%s" % className)
containerObject = klass(cfg)
containerObject.setup()
SendmailDispatcher.pluginContainerObjects[section] = containerObject
register = staticmethod(register)
def localSetup(self):
self.pluginWorkerObjects = {}
def localFinish(self):
for o in self.pluginWorkerObjects.values():
o.finish()
def execute(self, klass, data):
if not self.pluginContainerObjects.has_key(klass):
raise MyPermanentVerifierException, "Class %s not implemented" % klass
elif not self.pluginWorkerObjects.has_key(klass):
debug("Instantiate worker %s" % klass)
self.pluginWorkerObjects[klass] = self.pluginContainerObjects[klass].getWorker()
return self.pluginWorkerObjects[klass].execute(data)
class Config(object):
def __init__(self, section, config):
self.section = section
self.config = config
def getSection(self):
return self.section
def get(self, item):
return self.config.get(self.section, item)
class smmapBaseHandlerWorker(object):
def __init__(self, container):
self.container = container
def setup(self): pass
def finish(self): pass
def execute(self, data):
raise NotImplementedError
class smmapBaseHandlerContainer(object):
def __init__(self, cfg):
self.config = cfg
workerClassName = cfg.get('WorkerClass')
workerModuleName = cfg.get('WorkerModule')
if workerModuleName == None:
workerModuleName = workerClassName
m = __import__(workerModuleName)
self.workerClass = eval("m.%s" % workerClassName)
def setup(self): pass
def finish(self): pass
def getWorker(self):
worker = self.workerClass(self)
worker.setup()
return worker

View File

@ -0,0 +1,240 @@
import threading
import socket
import Queue
import re
import time
#import timeoutsocket
import DNS
from Logging import *
from Cache import Cache
from SendmailSocketMapHandler import smmapBaseHandlerWorker
from SendmailSocketMapHandler import smmapBaseHandlerContainer
from SendmailSocketMapHandler import MyPermanentVerifierException
from SendmailSocketMapHandler import MyTemporaryVerifierException
class VerifierHandlerContainer(smmapBaseHandlerContainer):
def setup(self):
DNS.ParseResolvConf()
if self.config.get('EnableCaching').lower() in ('true', 'yes', '1'):
debug("enabling cache")
self.cache = Cache(int(self.config.get('CacheExpiration')))
else:
debug("disabling cache")
self.cache = None
class VerifierHandlerWorker(smmapBaseHandlerWorker):
OK = "OK"
NOK = "NOK"
TEMPNOK = "TEMPNOK"
def setup(self):
self.zombies = []
class checker(threading.Thread):
def __init__(self, ready, config, host, address):
threading.Thread.__init__(self)
self.ready = ready
self.config = config
self.host = host
self.address = address
def checkAddressAvailability(self):
try:
debug("Trying " + self.host)
s = MySMTP(self.host, float(self.config.get('SMTPTimeOut')))
s.helo(self.config.get('SMTPHeloParam'))
s.mail(self.config.get('SMTPCheckSender'))
s.rcpt(self.address.getAddress())
s.quit()
result = VerifierHandlerWorker.OK
except MySMTPTemporaryException:
result = VerifierHandlerWorker.TEMPNOK
except MySMTPPermanentException:
result = VerifierHandlerWorker.NOK
except socket.timeout:
result = VerifierHandlerWorker.TEMPNOK
except socket.error:
result = VerifierHandlerWorker.TEMPNOK
return result
def run(self):
self.result = self.checkAddressAvailability()
self.ready.put(self.getName())
debug("NOTIFIED Host %s, Result %s" % (self.host, self.result))
def getResult(self):
return self.result
def getHost(self):
return self.host
def getAddress(self):
return self.address
def checkAvailability(self, mxes, address):
ready = Queue.Queue()
checkerThreads = {}
for m in mxes:
checkerThread = VerifierHandlerWorker.checker(ready, self.container.config, m, address)
checkerThread.start()
checkerThreads[checkerThread.getName()] = checkerThread
result = VerifierHandlerWorker.TEMPNOK
while 1:
debug("%i threads left" % len(checkerThreads))
if len(checkerThreads) == 0:
debug("no threads left ...")
break
if result != VerifierHandlerWorker.TEMPNOK:
debug("got a permanent result ...")
break
debug("Waiting for results ...")
name = ready.get()
checkerThread = checkerThreads[name]
checkerThread.join()
tempResult = checkerThread.getResult()
debug("success, result is " + str(tempResult))
if [VerifierHandlerWorker.OK, VerifierHandlerWorker.NOK].count(tempResult) != 0:
result = tempResult
del checkerThreads[name]
self.zombies.extend(checkerThreads.values())
return result
def finish(self):
while 1:
debug("finish: %i zombies left" % len(self.zombies))
for z in self.zombies:
if not z.isAlive():
debug("finish: thread %s for %s, %s terminated" % (z.getName(), z.getHost(), z.getAddress().getAddress()))
self.zombies.remove(z)
for z in self.zombies:
debug("finish: left over %s for %s, %s" % (z.getName(), z.getHost(), z.getAddress().getAddress()))
if len(self.zombies) == 0:
debug("finish: no zombie left ...")
break
debug("finish: WAITING")
time.sleep(5)
debug("finish: CONTINUE")
debug("finish: all threads terminated")
def execute(self, address):
debug("address " + address)
address = EMailAddress(address)
bestmxes = address.getBestMX()
if not bestmxes:
return "<NOK> <no bestmx found>"
if self.container.cache == None:
debug("no caching")
result = self.checkAvailability(bestmxes, address)
else:
result = self.container.cache.get(address.getAddress())
if result == None:
debug("not found in cache")
result = self.checkAvailability(bestmxes, address)
if result != VerifierHandlerWorker.TEMPNOK:
self.container.cache.put(address.getAddress(), result)
else:
debug("found in cache")
if result == VerifierHandlerWorker.OK:
return "<OK>"
elif result == VerifierHandlerWorker.NOK:
return "<NOK> <home server sent a permanent negative answer>"
else:
raise MyTemporaryVerifierException, "no mx reachable"
class MySMTPPermanentException(ValueError): pass
class MySMTPTemporaryException(ValueError): pass
class MySMTP(object):
def __init__(self, host, timeout, port=25):
self.host = host
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(timeout)
self.socket.connect((host, port))
self.socket.recv(8192)
self.resPattern = re.compile(r'[\w\W]*?^(\d{3,3}) (.*?)[\r\n]{1,2}$', re.MULTILINE)
def checkResult(self, r):
code, text = r
code = code / 100
if code == 2:
return;
elif code == 4:
raise MySMTPTemporaryException, text
elif code == 5:
raise MySMTPPermanentException, text
else:
raise MySMTPPermanentException, "unknown code: " + str(code) + ", text: " + str(text)
def docmd(self, cmd):
debug("docmd: %s, cmd: %s " % (self.host, cmd))
self.socket.sendall(cmd + "\r\n")
res = self.socket.recv(8192)
debug("docmd: result: (%s)" % res)
m = self.resPattern.match(res)
return self.checkResult((int(m.group(1)), m.group(2)))
def helo(self, param):
return self.docmd("helo " + param)
def mail(self, sender):
if sender[0] != '<' and sender[-1] != '>': sender = '<' + sender + '>'
return self.docmd("mail from:" + sender)
def rcpt(self, recipient):
return self.docmd("rcpt to:<%s>" % recipient)
def quit(self):
self.docmd("quit")
self.socket.close()
class EMailAddress(object):
def __init__(self, address):
self.address = address
if self.address[0] == '<' and self.address[-1] == '>': self.address = self.address[1:-1]
try:
self.userpart, self.domain = self.address.split('@')
except ValueError:
raise MyPermanentVerifierException, "excepted email address, found not at-sign"
def getUserPart(self):
return self.userpart
def getDomain(self):
return self.domain
def getAddress(self):
return self.address
def getBestMX(self):
if self.domain[0] == '[' and self.domain[-1] == ']':
bestmx2 = [self.domain[1:-1]]
else:
bestmx = DNS.mxlookup(self.domain)
pref = None
bestmx2 = []
for mx in bestmx:
if pref == None: pref = mx[0]
if pref == mx[0]:
bestmx2.append(mx[1])
else:
break
debug("bestmx " + str(bestmx2))
return bestmx2

327
smmapd_prototype/index.html Normal file
View File

@ -0,0 +1,327 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
<title>Sender Address Verifier for Sendmail</title>
<meta name="generator" content="emacs-wiki.el">
<meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1">
<link rev="made" href="mailto:woho@hottis.de">
<link rel="stylesheet" type="text/css" href="/web/default.css" />
</head>
<body>
<h1>Sender Address Verifier for Sendmail</h1>
<!-- Page published by Emacs Wiki begins here -->
<p>
Author: Wolfgang Hottgenroth <<a href="mailto:woho@hottis.de">woho@hottis.de</a>>, 2004-05-17
</p>
<p>
This is the prototype of a sender address verifier for sendmail-8.13.
</p>
<p>
It consists of a m4 file containing a FEATURE to be included in your
<code>sendmail.mc</code> and a verifier daemon in a bit of python code.
</p>
<p>
By including the FEATURE in your sendmail.mc file and running the
verifier daemon, sendmail file verify either
</p>
<ul>
<li>all sender addresses (with certain exceptions) or
</li>
<li>only certain sender addresses
</li>
</ul>
<p>
This will be done by connecting to the best MX servers of the
particular domain, trying to send a mail to the particular address and
collect the replies.
</p>
<p>
Actually only the <code>HELO</code>, <code>MAIL</code> and <code>RCPT</code> commands are issued.
</p>
<p>
If a positive reply was found, the mail is considered as valid.
</p>
<p>
If a permanent negative reply was found, the mail is considered as
invalid.
</p>
<p>
If no MX entry was found, the mail is considered as invalid.
</p>
<p>
If a temporary negative reply was found, the mail is considered as
temporary invalid.
</p>
<p>
If there is more than one best MX server all of these servers are
connected in parallel and the first permanent reply (either positive
or negative) is returned.
</p>
<h3>Download</h3>
<p>
The complete sources: <a href="./download/">download</a>
</p>
<p>
Have a look into the sources: <a href="http://www.hottis.de/cgi-bin/cvsweb.cgi/sender_verifier/">sources</a>
</p>
<h3>Requirements</h3>
<h4>sendmail</h4>
<p>
sendmail-8.13 is required, since this thing uses the fresh introduced
socket map.
</p>
<p>
Find it <a href="http://www.sendmail.org">here</a> on the sendmail homepage.
</p>
<p>
Your need to build sendmail with support for the socket map. Include
</p>
<pre class="example">
APPENDDEF(`confMAPDEF',`-DSOCKETMAP')
</pre>
<p>
in your <code>site.config.m4</code>.
</p>
<h4>Python</h4>
<p>
Python 2.2 or 2.3 is required. If you have Python 2.3 you must delete
the <code>import timeoutsocket</code> line from <code>verifier.py</code>.
</p>
<p>
Additionally the python package <code>python-dns</code> is required. Find it
<a href="http://pydns.sourceforge.net/">http://pydns.sourceforge.net</a>.
</p>
<h3>Configuration of sendmail</h3>
<p>
Include the FEATURE in your <code>sendmail.mc</code> file. You need to give two
parameters:
</p>
<pre class="example">
FEATURE(`verifysender', `mode', `return')
</pre>
<p>
For <code>mode</code> you must give either <code>white</code> or <code>black</code>.
</p>
<dl>
<dt><code>white</code></dt>
<dd>
All sender addresses but those mentioned in the whitelist
file are verified. Complete addresses or just domains can be listed in
the file. The default location of the whitelist is
<code>/etc/mail/verify-white-list</code>. If you need a different location,
define it to <code>confVERIFIER_WHITELIST</code>.
</dd>
<dt><code>black</code></dt>
<dd>
only addresses or addresses within domains listed in the
blacklist file are verified. It is obviously only useful to mention
domains in the blacklist. The default location of the blacklist is
<code>/etc/mail/verify-black-list</code>. If you need a different location,
define it to <code>confVERIFIER_BLACKLIST</code>.
</dd>
</dl>
<p>
Both the blacklist and the whitelist file are maps, they must be
created with <code>makemap</code>. Therefore the entries need a LHS (the address
or domain) and a RHS. The actual content of the RHS has NO meaning at
all.
</p>
<p>
The FEATURE defines a socket map. The default target of the map is
<code>inet:8884@127.0.0.1</code>, according to the default setting in
<code>Config.py</code>. If you need something different, define it to
<code>confVERIFIER_MAP</code>, but don't forget to also adjust <code>Config.py</code>.
</p>
<h3>Configuration of the verification daemon</h3>
<p>
The configuration of the daemon is done in the file <code>Config.py</code>.
</p>
<p>
This is the default of this file:
</p>
<pre class="example">
[Daemon]
Address: 127.0.0.1
Port: 8884
PidFile: smmapd.pid
Plugins: Verifier,Verifier2
[Logging]
ApplId: smmapd
[Verifier]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: 1
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: local
SMTPCheckSender: &lt;&gt;
[Verifier2]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: 1
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: hottis.de
SMTPCheckSender: &lt;postmaster@hottis.de&gt;</pre>
<p>
<code>Port</code> and <code>Address</code> are specifying the socket the daemon should
listen to for communication with sendmail. These settings must be
reflected in the <code>confVERIFIER_MAP</code> if you change it.
</p>
<p>
<code>SMTPTimeOut</code> is the timeout for the communication with the MX servers
when verifying addresses.
</p>
<p>
<code>SMTPHeloParam</code> is the parameter the verifier will use with the <code>HELO</code>
command when verifying.
</p>
<p>
<code>SMTPCheckSender</code> is the sender address used during
verifications. You should not change it unless you know what you do to
avoid verification loops.
</p>
<p>
Since the verification is a time and resource consuming process,
results can be cached, which is enabled by default. Set
<code>EnableCaching</code> to 0 to disable it.
</p>
<p>
<code>CacheExpiration</code> is the time in seconds an entry in the cache is
considered as valid. It should be much higher.
</p>
<h3>Operation</h3>
<p>
Configure sendmail and the daemon according to your needs. Start the
daemon:
</p>
<pre class="example">
./verifier.py
</pre>
<h3>Changes</h3>
<ul>
<li>According to a comment in comp.mail.sendmail I've introduced a class
<code>verifier_fix_white</code> in the FEATURE file, currently containing only
the string <code>postmaster</code>. Addresses with userpart in this class will
never ever be verified to avoid infinite verifying loops.
</li>
</ul>
<h4>2004-05-17</h4>
<ul>
<li>support plugins
</li>
<li>separate container and worker object, thereby enable multiple
instances of the same plugins
</li>
</ul>
<!-- Page published by Emacs Wiki ends here -->
<div class="navfoot">
<hr>
<table width="100%" border="0" summary="Footer navigation">
<tr>
<td width="33%" align="left">
<span class="footdate">UPDATED: 2004-05-17</span>
</td>
<td width="34%" align="center">
<span class="foothome">
</span>
</td>
<td width="33%" align="right">
</td>
</tr>
</table>
</div>
</body>
</html>

195
smmapd_prototype/index.wiki Normal file
View File

@ -0,0 +1,195 @@
#title Sender Address Verifier for Sendmail
#style /web/default.css
Author: Wolfgang Hottgenroth <woho@hottis.de>, 2004-05-17
This is the prototype of a sender address verifier for sendmail-8.13.
It consists of a m4 file containing a FEATURE to be included in your
=sendmail.mc= and a verifier daemon in a bit of python code.
By including the FEATURE in your sendmail.mc file and running the
verifier daemon, sendmail file verify either
- all sender addresses (with certain exceptions) or
- only certain sender addresses
This will be done by connecting to the best MX servers of the
particular domain, trying to send a mail to the particular address and
collect the replies.
Actually only the =HELO=, =MAIL= and =RCPT= commands are issued.
If a positive reply was found, the mail is considered as valid.
If a permanent negative reply was found, the mail is considered as
invalid.
If no MX entry was found, the mail is considered as invalid.
If a temporary negative reply was found, the mail is considered as
temporary invalid.
If there is more than one best MX server all of these servers are
connected in parallel and the first permanent reply (either positive
or negative) is returned.
** Download
The complete sources: [[./download/][download]]
Have a look into the sources: [[http://www.hottis.de/cgi-bin/cvsweb.cgi/sender_verifier/][sources]]
** Requirements
*** sendmail
sendmail-8.13 is required, since this thing uses the fresh introduced
socket map.
Find it [[http://www.sendmail.org][here]] on the sendmail homepage.
Your need to build sendmail with support for the socket map. Include
<example>
APPENDDEF(`confMAPDEF',`-DSOCKETMAP')
</example>
in your =site.config.m4=.
*** Python
Python 2.2 or 2.3 is required. If you have Python 2.3 you must delete
the =import timeoutsocket= line from =verifier.py=.
Additionally the python package =python-dns= is required. Find it
[[http://pydns.sourceforge.net/][http://pydns.sourceforge.net]].
** Configuration of sendmail
Include the FEATURE in your =sendmail.mc= file. You need to give two
parameters:
<example>
FEATURE(`verifysender', `mode', `return')
</example>
For =mode= you must give either =white= or =black=.
=white= :: All sender addresses but those mentioned in the whitelist
file are verified. Complete addresses or just domains can be listed in
the file. The default location of the whitelist is
=/etc/mail/verify-white-list=. If you need a different location,
define it to =confVERIFIER_WHITELIST=.
=black= :: only addresses or addresses within domains listed in the
blacklist file are verified. It is obviously only useful to mention
domains in the blacklist. The default location of the blacklist is
=/etc/mail/verify-black-list=. If you need a different location,
define it to =confVERIFIER_BLACKLIST=.
Both the blacklist and the whitelist file are maps, they must be
created with =makemap=. Therefore the entries need a LHS (the address
or domain) and a RHS. The actual content of the RHS has NO meaning at
all.
The FEATURE defines a socket map. The default target of the map is
=inet:8884@127.0.0.1=, according to the default setting in
=Config.py=. If you need something different, define it to
=confVERIFIER_MAP=, but don't forget to also adjust =Config.py=.
** Configuration of the verification daemon
The configuration of the daemon is done in the file =Config.py=.
This is the default of this file:
<example>
[Daemon]
Address: 127.0.0.1
Port: 8884
PidFile: smmapd.pid
Plugins: Verifier,Verifier2
[Logging]
ApplId: smmapd
[Verifier]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: 1
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: local
SMTPCheckSender: <>
[Verifier2]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: 1
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: hottis.de
SMTPCheckSender: <postmaster@hottis.de></example>
=Port= and =Address= are specifying the socket the daemon should
listen to for communication with sendmail. These settings must be
reflected in the =confVERIFIER_MAP= if you change it.
=SMTPTimeOut= is the timeout for the communication with the MX servers
when verifying addresses.
=SMTPHeloParam= is the parameter the verifier will use with the =HELO=
command when verifying.
=SMTPCheckSender= is the sender address used during
verifications. You should not change it unless you know what you do to
avoid verification loops.
Since the verification is a time and resource consuming process,
results can be cached, which is enabled by default. Set
=EnableCaching= to 0 to disable it.
=CacheExpiration= is the time in seconds an entry in the cache is
considered as valid. It should be much higher.
** Operation
Configure sendmail and the daemon according to your needs. Start the
daemon:
<example>
./verifier.py
</example>
** Changes
- According to a comment in comp.mail.sendmail I've introduced a class
=verifier_fix_white= in the FEATURE file, currently containing only
the string =postmaster=. Addresses with userpart in this class will
never ever be verified to avoid infinite verifying loops.
*** 2004-05-17
- support plugins
- separate container and worker object, thereby enable multiple
instances of the same plugins

19
smmapd_prototype/setup.py Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python
from distutils.core import setup
setup(name="smmapd",
version="0.1",
description="Framework for sendmail SocketMap handlers",
long_description = """
A framework to build handlers for the sendmail 8.13 SocketMap,
together with an implementation of a sender-address verifier,
together we a sendmail m4 feature file""",
author="Wolfgang Hottgenroth",
author_email="woho@hottis.de",
url="http://www.hottis.de/web/verifier/index.html",
py_modules = [ 'SendmailSocketMapHandler', 'Cache', 'Logging', 'VerifierHandler' ],
scripts = [ 'smmapd' ],
data_files = [('config', ['smmapd.ini']),
('cf/feature', ['verifysender.m4'])]
)

65
smmapd_prototype/smmapd Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/python
import SocketServer
import socket
import os
import ConfigParser
import getopt
import sys
from Logging import *
from SendmailSocketMapHandler import SendmailDispatcher
def usage():
print "Usage"
try:
opts, args = getopt.getopt(sys.argv[1:], "hC:F", ["help", "ConfigFile=", "ForeGround"])
except getopt.GetoptError:
usage()
sys.exit(2)
configFile = '/etc/smmapd.ini'
foreGround = None
for o, a in opts:
if o in ("-h", "--help"):
usage()
sys.exit()
if o in ("-C", "--ConfigFile"):
configFile = a
if o in ("-F", "--ForeGround"):
foreGround = 1
config = ConfigParser.ConfigParser()
config.read(configFile)
openlog(config)
if foreGround:
pid = 0
else:
pid = os.fork()
if pid:
pidFile = file(config.get('Daemon', 'PidFile'), mode='w')
pidFile.write("%i\n" % pid)
pidFile.close()
print "daemon started with pid ", pid
else:
log("daemon started")
SendmailDispatcher.registerAll(config)
try:
address = config.get('Daemon', 'Address')
port = int(config.get('Daemon', 'Port'))
srv = SocketServer.ThreadingTCPServer((address, port), SendmailDispatcher)
srv.serve_forever()
except socket.error, arg:
log("got a socket error: %s" % str(arg))
log("daemon died")

View File

@ -0,0 +1,37 @@
[Daemon]
Address: 127.0.0.1
Port: 8884
PidFile: smmapd.pid
Plugins: verifier,verifier2,cyrusChecker
[Logging]
ApplId: smmapd
[verifier]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: yes
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: local
SMTPCheckSender: <>
[verifier2]
ContainerModule: VerifierHandler
ContainerClass: VerifierHandlerContainer
WorkerModule: VerifierHandler
WorkerClass: VerifierHandlerWorker
EnableCaching: yes
CacheExpiration: 20
SMTPTimeOut: 20
SMTPHeloParam: hottis.de
SMTPCheckSender: <postmaster@hottis.de>
[cyrusChecker]
ContainerModule: SendmailSocketMapHandler
ContainerClass: smmapBaseHandlerContainer
WorkerModule: CyrusChecker
WorkerClass: CyrusCheckerWorker

View File

@ -0,0 +1,424 @@
####
# Copyright 2000,2001 by Timothy O'Malley <timo@alum.mit.edu>
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software
# and its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# Timothy O'Malley not be used in advertising or publicity
# pertaining to distribution of the software without specific, written
# prior permission.
#
# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR
# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
####
"""Timeout Socket
This module enables a timeout mechanism on all TCP connections. It
does this by inserting a shim into the socket module. After this module
has been imported, all socket creation goes through this shim. As a
result, every TCP connection will support a timeout.
The beauty of this method is that it immediately and transparently
enables the entire python library to support timeouts on TCP sockets.
As an example, if you wanted to SMTP connections to have a 20 second
timeout:
import timeoutsocket
import smtplib
timeoutsocket.setDefaultSocketTimeout(20)
The timeout applies to the socket functions that normally block on
execution: read, write, connect, and accept. If any of these
operations exceeds the specified timeout, the exception Timeout
will be raised.
The default timeout value is set to None. As a result, importing
this module does not change the default behavior of a socket. The
timeout mechanism only activates when the timeout has been set to
a numeric value. (This behavior mimics the behavior of the
select.select() function.)
This module implements two classes: TimeoutSocket and TimeoutFile.
The TimeoutSocket class defines a socket-like object that attempts to
avoid the condition where a socket may block indefinitely. The
TimeoutSocket class raises a Timeout exception whenever the
current operation delays too long.
The TimeoutFile class defines a file-like object that uses the TimeoutSocket
class. When the makefile() method of TimeoutSocket is called, it returns
an instance of a TimeoutFile.
Each of these objects adds two methods to manage the timeout value:
get_timeout() --> returns the timeout of the socket or file
set_timeout() --> sets the timeout of the socket or file
As an example, one might use the timeout feature to create httplib
connections that will timeout after 30 seconds:
import timeoutsocket
import httplib
H = httplib.HTTP("www.python.org")
H.sock.set_timeout(30)
Note: When used in this manner, the connect() routine may still
block because it happens before the timeout is set. To avoid
this, use the 'timeoutsocket.setDefaultSocketTimeout()' function.
Good Luck!
"""
__version__ = "$Revision$"
__author__ = "Timothy O'Malley <timo@alum.mit.edu>"
#
# Imports
#
import select, string
import socket
if not hasattr(socket, "_no_timeoutsocket"):
_socket = socket.socket
else:
_socket = socket._no_timeoutsocket
#
# Set up constants to test for Connected and Blocking operations.
# We delete 'os' and 'errno' to keep our namespace clean(er).
# Thanks to Alex Martelli and G. Li for the Windows error codes.
#
import os
if os.name == "nt":
_IsConnected = ( 10022, 10056 )
_ConnectBusy = ( 10035, )
_AcceptBusy = ( 10035, )
else:
import errno
_IsConnected = ( errno.EISCONN, )
_ConnectBusy = ( errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK )
_AcceptBusy = ( errno.EAGAIN, errno.EWOULDBLOCK )
del errno
del os
#
# Default timeout value for ALL TimeoutSockets
#
_DefaultTimeout = None
def setDefaultSocketTimeout(timeout):
global _DefaultTimeout
_DefaultTimeout = timeout
def getDefaultSocketTimeout():
return _DefaultTimeout
#
# Exceptions for socket errors and timeouts
#
Error = socket.error
class Timeout(Exception):
pass
#
# Factory function
#
from socket import AF_INET, SOCK_STREAM
def timeoutsocket(family=AF_INET, type=SOCK_STREAM, proto=None):
if family != AF_INET or type != SOCK_STREAM:
if proto:
return _socket(family, type, proto)
else:
return _socket(family, type)
return TimeoutSocket( _socket(family, type), _DefaultTimeout )
# end timeoutsocket
#
# The TimeoutSocket class definition
#
class TimeoutSocket:
"""TimeoutSocket object
Implements a socket-like object that raises Timeout whenever
an operation takes too long.
The definition of 'too long' can be changed using the
set_timeout() method.
"""
_copies = 0
_blocking = 1
def __init__(self, sock, timeout):
self._sock = sock
self._timeout = timeout
# end __init__
def __getattr__(self, key):
return getattr(self._sock, key)
# end __getattr__
def get_timeout(self):
return self._timeout
# end set_timeout
def set_timeout(self, timeout=None):
self._timeout = timeout
# end set_timeout
def setblocking(self, blocking):
self._blocking = blocking
return self._sock.setblocking(blocking)
# end set_timeout
def connect_ex(self, addr):
errcode = 0
try:
self.connect(addr)
except Error, why:
errcode = why[0]
return errcode
# end connect_ex
def connect(self, addr, port=None, dumbhack=None):
# In case we were called as connect(host, port)
if port != None: addr = (addr, port)
# Shortcuts
sock = self._sock
timeout = self._timeout
blocking = self._blocking
# First, make a non-blocking call to connect
try:
sock.setblocking(0)
sock.connect(addr)
sock.setblocking(blocking)
return
except Error, why:
# Set the socket's blocking mode back
sock.setblocking(blocking)
# If we are not blocking, re-raise
if not blocking:
raise
# If we are already connected, then return success.
# If we got a genuine error, re-raise it.
errcode = why[0]
if dumbhack and errcode in _IsConnected:
return
elif errcode not in _ConnectBusy:
raise
# Now, wait for the connect to happen
# ONLY if dumbhack indicates this is pass number one.
# If select raises an error, we pass it on.
# Is this the right behavior?
if not dumbhack:
r,w,e = select.select([], [sock], [], timeout)
if w:
return self.connect(addr, dumbhack=1)
# If we get here, then we should raise Timeout
raise Timeout("Attempted connect to %s timed out." % str(addr) )
# end connect
def accept(self, dumbhack=None):
# Shortcuts
sock = self._sock
timeout = self._timeout
blocking = self._blocking
# First, make a non-blocking call to accept
# If we get a valid result, then convert the
# accept'ed socket into a TimeoutSocket.
# Be carefult about the blocking mode of ourselves.
try:
sock.setblocking(0)
newsock, addr = sock.accept()
sock.setblocking(blocking)
timeoutnewsock = self.__class__(newsock, timeout)
timeoutnewsock.setblocking(blocking)
return (timeoutnewsock, addr)
except Error, why:
# Set the socket's blocking mode back
sock.setblocking(blocking)
# If we are not supposed to block, then re-raise
if not blocking:
raise
# If we got a genuine error, re-raise it.
errcode = why[0]
if errcode not in _AcceptBusy:
raise
# Now, wait for the accept to happen
# ONLY if dumbhack indicates this is pass number one.
# If select raises an error, we pass it on.
# Is this the right behavior?
if not dumbhack:
r,w,e = select.select([sock], [], [], timeout)
if r:
return self.accept(dumbhack=1)
# If we get here, then we should raise Timeout
raise Timeout("Attempted accept timed out.")
# end accept
def send(self, data, flags=0):
sock = self._sock
if self._blocking:
r,w,e = select.select([],[sock],[], self._timeout)
if not w:
raise Timeout("Send timed out")
return sock.send(data, flags)
# end send
def recv(self, bufsize, flags=0):
sock = self._sock
if self._blocking:
r,w,e = select.select([sock], [], [], self._timeout)
if not r:
raise Timeout("Recv timed out")
return sock.recv(bufsize, flags)
# end recv
def makefile(self, flags="r", bufsize=-1):
self._copies = self._copies +1
return TimeoutFile(self, flags, bufsize)
# end makefile
def close(self):
if self._copies <= 0:
self._sock.close()
else:
self._copies = self._copies -1
# end close
# end TimeoutSocket
class TimeoutFile:
"""TimeoutFile object
Implements a file-like object on top of TimeoutSocket.
"""
def __init__(self, sock, mode="r", bufsize=4096):
self._sock = sock
self._bufsize = 4096
if bufsize > 0: self._bufsize = bufsize
if not hasattr(sock, "_inqueue"): self._sock._inqueue = ""
# end __init__
def __getattr__(self, key):
return getattr(self._sock, key)
# end __getattr__
def close(self):
self._sock.close()
self._sock = None
# end close
def write(self, data):
self.send(data)
# end write
def read(self, size=-1):
_sock = self._sock
_bufsize = self._bufsize
while 1:
datalen = len(_sock._inqueue)
if datalen >= size >= 0:
break
bufsize = _bufsize
if size > 0:
bufsize = min(bufsize, size - datalen )
buf = self.recv(bufsize)
if not buf:
break
_sock._inqueue = _sock._inqueue + buf
data = _sock._inqueue
_sock._inqueue = ""
if size > 0 and datalen > size:
_sock._inqueue = data[size:]
data = data[:size]
return data
# end read
def readline(self, size=-1):
_sock = self._sock
_bufsize = self._bufsize
while 1:
idx = string.find(_sock._inqueue, "\n")
if idx >= 0:
break
datalen = len(_sock._inqueue)
if datalen >= size >= 0:
break
bufsize = _bufsize
if size > 0:
bufsize = min(bufsize, size - datalen )
buf = self.recv(bufsize)
if not buf:
break
_sock._inqueue = _sock._inqueue + buf
data = _sock._inqueue
_sock._inqueue = ""
if idx >= 0:
idx = idx + 1
_sock._inqueue = data[idx:]
data = data[:idx]
elif size > 0 and datalen > size:
_sock._inqueue = data[size:]
data = data[:size]
return data
# end readline
def readlines(self, sizehint=-1):
result = []
data = self.read()
while data:
idx = string.find(data, "\n")
if idx >= 0:
idx = idx + 1
result.append( data[:idx] )
data = data[idx:]
else:
result.append( data )
data = ""
return result
# end readlines
def flush(self): pass
# end TimeoutFile
#
# Silently replace the socket() builtin function with
# our timeoutsocket() definition.
#
if not hasattr(socket, "_no_timeoutsocket"):
socket._no_timeoutsocket = socket.socket
socket.socket = timeoutsocket
del socket
socket = timeoutsocket
# Finis

View File

@ -0,0 +1,110 @@
VERSIONID(`$Id$')
divert(-1)
define(`_USAGE_', `dnl
errprint(`*** ERROR: missing argument for FEATURE(verifysender):
Usage: FEATURE(`verifysender', `_mode_', `_return_')
_mode_: black or white
_return_: temp or perm
found: $1
')')
ifelse(_ARG_, `black', `', `
ifelse(_ARG_, `white', `', `
_USAGE_(`_mode_: ('_ARG_`)
')
')')
ifelse(_ARG2_, `temp', `', `
ifelse(_ARG2_, `perm', `', `
_USAGE_(`_return_: ('_ARG2_`)
')
')')
define(`_mode_', _ARG_)
define(`_return_', _ARG2_)
errprint(`*** _mode_: '_mode_`
')
errprint(`*** _return_: '_return_`
')
define(`_T_DSN_', `4.1.0')
define(`_T_REPLY', `451')
ifelse(_return_, `temp', `
define(`_DSN_', _T_DSN_)
define(`_REPLY_', _T_REPLY)', `dnl
define(`_DSN_', `5.1.0')
define(`_REPLY_', `550')')
ifelse(defn(`confVERIFIER_MAP'), `', `
define(`_VERIFIER_MAP_', `inet:8884@127.0.0.1')', `
define(`_VERIFIER_MAP_', confVERIFIER_MAP)')
ifelse(defn(`confVERIFIER_BLACKLIST'), `', `
define(`_VERIFIER_BLACKLIST_', `/etc/mail/verifier-black-list')', `
define(`_VERIFIER_BLACKLIST_', confVERIFIER_BLACKLIST)')
ifelse(defn(`confVERIFIER_WHITELIST'), `', `
define(`_VERIFIER_WHITELIST_', `/etc/mail/verifier-white-list')', `
define(`_VERIFIER_WHITELIST_', confVERIFIER_WHITELIST)')
divert(0)
LOCAL_CONFIG
# Adjust the port
Kverifier socket -T<temp> _VERIFIER_MAP_
ifelse(_mode_, `white', `dnl
Kverifier_helper hash -o _VERIFIER_WHITELIST_', `dnl
Kverifier_helper hash _VERIFIER_BLACKLIST_')
C{verifier_fix_white} postmaster
LOCAL_RULESETS
# This ruleset can be used to test the verifier in -bt mode
Svt
R$+ $: < $(verifier $1 $:none $) >
Sverifier0
R< $={verifier_fix_white} @ $+ > $@ < ok >
R< $+ @ $+ > $: < $2 > < $(verifier_helper $1 @ $2 $: $) >
R< $+ > < > $: < $(verifier_helper $1 $: $) >
ifelse(_mode_, `white', `dnl
dnl if we found nothing in the whitelist, we continue with checking
R< > $@ < cont >
dnl if we found something in the whitelist, we skip further verifications
R< $+ > $@ < ok >', `dnl
dnl if we found nothing in the blacklist, we skip further verifications
R< > $@ < ok >
dnl if we found something in the blacklist, we continue with checking
R< $+ > $@ < cont >')
Sverifier1
R< $+ > $: < $(verifier $1 $:none $) >
R< $* < temp > > $#error $@ _T_DSN_ $: "_T_REPLY_ Sender Address could currently not be verified (1)"
R< none > $#error $@ _T_DSN_ $: "_T_REPLY_ Sender Address could currently not be verified (2)"
R< <OK> $* > $@ < ok >
R< <NOK> < $* > > $#error $@ _DSN_ $: "_REPLY_ Sender Address was verified as bad: " $1
R< <TNOK> < $* > > $#error $@ _T_DSN_ $: "_T_REPLY_ Sender Address could currently not be verified (3): " $1
dnl if we get here, some is wrong with our code
R$* $#error $@ 4.7.1 $: "451 Local configuration error <sv1>"
SLocal_check_mail
dnl MAILER-DAEMON address must not be verified
R<> $@ <>
dnl try to focus
R$+ $: <> $>3 $1
R<> $+ < @ $+ . > $: < $1 @ $2 >
R<> $+ < @ $+ > $: < $1 @ $2 >
dnl if unable to focus, rest of check_mail should take care (may be we should reject)
R<> $* $@ OK
R< $+ @ $+ > $: < $1 @ $2 > $>verifier0 < $1 @ $2 >
R< $+ @ $+ > < cont > $: < $1 @ $2 > $>verifier1 < $1 @ $2 >
R< $+ @ $+ > $# $* $# $3
R< $+ @ $+ > < ok > $@ OK
dnl if we get here, some is wrong with our code
R$* $#error $@ 4.7.1 $: "451 Local configuration error <sv2>"