pysrs-1.0/ 0000775 0001606 0000145 00000000000 11555576327 011436 5 ustar stuart bms pysrs-1.0/CHANGES 0000664 0001606 0000145 00000001270 10375005567 012421 0 ustar stuart bms 0.30.11 Support SRS signing mode.
0.30.10 Support SES
0.30.9 Support sendmail socket map.
0.30.8 Use HMAC instead of straight SHA to match reference implementation.
0.30.7 Pass SRS_DOMAIN to envfrom2srs.py
Put SRS rewriting rule at end of EnvFromSMTP in pysrs.m4.
Fix regex for is_srs macro in pysrs.m4.
0.30.6 set alwaysrewrite=True in envfrom2srs.py since pysrs.m4 skips local
domains.
Incorporate m4 macro from Alain Knaff for cleaner sendmail support.
0.30.5 Make sendmail maps use config file in /etc/mail/pysrs.cfg,
so there is only only source to modify to change options.
Add missing import for SRS.new()
0.30.4 Move global options to package module. Rename Base.SRS to Base.Base.
pysrs-1.0/pysrs.cfg 0000664 0001606 0000145 00000001171 10553236567 013273 0 ustar stuart bms # sample SRS configuration
[srs]
;secret="shhhh!"
;maxage=21
;hashlength=5
# if defined, SRS uses a database for opaque rewriting
;database=/var/log/milter/srsdata
# sign these domains using SES to prevent forged bounces instead of SRS
;ses = localdomain1.com, localdomain2.org
# sign these domains using SRS in signing mode to prevent forged bounces
;sign = localdomain1.com, localdomain2.org
# rewrite all other domains to this domain using SRS
;fwdomain = mydomain.com
# reject unsigned mail to these domains in pymilter (used by pymilter)
;srs = otherdomain.com
# do not rewrite mail to these domains
;nosrs = braindeadmail.com
pysrs-1.0/pysrs.html 0000664 0001606 0000145 00000007772 11350251055 013476 0 ustar stuart bms
Python SRS
Sender Rewriting Scheme in Python
This web page is written by Stuart D. Gathman
and
sponsored by
Business Management Systems, Inc.
Last updated Jul 31, 2006
This is a Python implementation of
the Sender Rewriting Scheme.
It is a fairly direct translation of the
draft implementation in Perl
by Shevek. It includes a test suite, which currently checks four
levels of forwarding and subsequent reversal for the Guarded, DB, and
Reversible implementations.
- This package includes scripts to be used as sendmail program maps. See
sendmail integration
for an explanation and instructions on incorporating SRS into
sendmail.cf
, substituting envfrom2srs.py
and
srs2envtol.py
for the perl scripts. Even simpler, use the
supplied sendmail m4 hack with sendmail.mc
.
- SRS.Daemon.Daemon() provides a simple socket daemon suitable
for use with the Exim mailer.
- RPM now includes a sendmail socketmap daemon. The program map is
no longer recommended. It is slow and a security risk. Prior to socketmaps,
it was all that was available for a custom map. Socketmap is available
in sendmail 8.13.
- For best results, use with Python milter
to reject unsigned recipients.
Sendmail integration
Add the following lines to your /etc/mail/sendmail.mc (RedHat /
Fedora) after any MAILER():
dnl #
dnl # File listing domains we do not SRS encode for when sending to
dnl #
define(`NO_SRS_FILE',`/etc/mail/no-srs-mailers')dnl
dnl #
dnl # Uncomment the following if you do not wish to SRS encode mail from
dnl # local domains. Only non-local domains need to be SRS encoded to
dnl # satisfy SPF. But encoding all outgoing mail can detect bounce forgeries.
dnl #
dnl define(`NO_SRS_FROM_LOCAL')dnl
dnl #
HACK(`pysrs',`/var/run/milter/pysrs')dnl
If you cannot install a version of sendmail with socketmap support, then
the original program map is still available as HACK(pysrsprog)
.
- NO_SRS_FILE is the path of a file containing the recipient
MTA's for which you won't do SRS (typically, primary MXes for
which you are secondary). Just leave this away, if you are
secondary for nobody. The no-srs-mailers file is a simple text
file which has one recipient MTA per line.
- The argument to pysrs is the socket where the socketmap daemon
is listening. This must match
/etc/mail/pysrs.cfg
or
the default of /var/run/milter/pysrs
.
- NO_SRS_FROM_LOCAL : if this is set (define line present), then
no SRS is done if sender is local (i.e. his domain is in
/etc/mail/local-host-names)
- The argument to pysrsprog is the domain that your SRS addresses bear
(i.e. if your SRS addresses are srs0=mumble-jumble-toto@mydomain.com,
then the argument is mydomain.com). This overrides fwdomain in
/etc/mail/pysrs.cfg.
Downloads
Goto Sourceforge file release.
pysrs-1.0/pysrs.py 0000775 0001606 0000145 00000011261 11555576263 013173 0 ustar stuart bms #!/usr/bin/python2.4
# Sendmail socket server daemon
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
import SES
import re
import os
from ConfigParser import ConfigParser, DuplicateSectionError
import SocketMap
import time
import sys
class SRSHandler(SocketMap.Handler):
def log(self,*msg):
# print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
print "%s" % (time.strftime('%Y%b%d %H:%M:%S'),),
for i in msg: print i,
print
sys.stdout.flush()
bracketRE = re.compile(r'[<>]')
traildotRE = re.compile(r'\.$')
# Our original envelope-from may look funny on entry
# of this Ruleset:
#
# admin<@asarian-host.net.>
#
# We need to preprocess it some:
def _handle_make_srs(self,old_address):
a = old_address.split('\x9b')
if len(a) == 2:
h,old_address = a
self.log('h =',h)
else:
h = True
nosrsdomain = self.server.nosrsdomain
if old_address == '<@>' or not h or h in nosrsdomain:
return old_address
srs = self.server.srs
ses = self.server.ses
fwdomain = self.server.fwdomain
if not fwdomain:
fwdomain = self.fwdomain
sesdomain = self.server.sesdomain
signdomain = self.server.signdomain
use_address = self.bracketRE.sub('',old_address)
use_address = self.traildotRE.sub('',use_address)
# Ok, first check whether we already have a signed SRS address;
# if so, just return the old address: we do not want to double-sign
# by accident!
#
# Else, gimme a valid SRS signed address, munge it back the way
# sendmail wants it at this point; or just return the old address,
# in case nothing went.
try:
new_address = srs.reverse(use_address)
return old_address
except:
try:
senduser,sendhost = use_address.split('@')
shl = sendhost.lower()
if shl in sesdomain:
new_address = ses.sign(use_address)
elif shl in signdomain:
new_address = srs.sign(use_address)
else:
new_address = srs.forward(use_address,fwdomain)
return new_address.replace('@','<@',1)+'.>'
except:
return old_address
def _handle_reverse_srs(self,old_address):
# Munge ParseLocal recipient in the same manner as required
# in EnvFromSMTP.
use_address = self.bracketRE.sub('',old_address)
use_address = self.traildotRE.sub('',use_address)
# Just try and reverse the address. If we succeed, return this
# new address; else, return the old address (quoted if it was
# a piped alias).
srs = self.server.srs
ses = self.server.ses
try:
a = ses.verify(use_address)
if len(a) > 1:
return a[0].replace('@','<@',1)+'.>'
use_address = srs.reverse(use_address)
while True:
try:
use_address = srs.reverse(use_address)
except: break
return use_address.replace('@','<@',1)+'.>'
except:
if use_address.startswith('|'):
return '"%s"' % old_address
else:
return old_address
def main(args):
# get SRS parameters from milter configuration
cp = ConfigParser({
'secret': 'shhhh!',
'maxage': '8',
'hashlength': '8',
'separator': '=',
'socket': '/var/run/milter/pysrs'
})
cp.read(["/etc/mail/pysrs.cfg"])
try:
cp.add_section('srs')
except DuplicateSectionError:
pass
secret = [cp.get('srs','secret')]
for old in ('secret.0','secret.1', 'secret.2'):
if not cp.has_option('srs',old): break
secret.append(cp.get('srs',old))
srs = SRS.new(secret,
maxage=cp.getint('srs','maxage'),
hashlength=cp.getint('srs','hashlength'),
separator=cp.get('srs','separator'),
alwaysrewrite=True # pysrs.m4 can skip calling us for local domains
)
ses = SES.new(secret, expiration=cp.getint('srs','maxage'))
socket = cp.get('srs','socket')
try:
os.remove(socket)
except: pass
daemon = SocketMap.Daemon(socket,SRSHandler)
daemon.server.fwdomain = cp.get('srs','fwdomain',None)
daemon.server.sesdomain = ()
daemon.server.signdomain = ()
daemon.server.nosrsdomain = ()
if cp.has_option('srs','ses'):
daemon.server.sesdomain = [
q.strip() for q in cp.get('srs','ses').split(',')]
if cp.has_option('srs','sign'):
daemon.server.signdomain = [
q.strip() for q in cp.get('srs','sign').split(',')]
if cp.has_option('srs','nosrs'):
daemon.server.nosrsdomain = [
q.strip() for q in cp.get('srs','nosrs').split(',')]
daemon.server.srs = srs
daemon.server.ses = ses
print "%s pysrs startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush()
daemon.run()
print "%s pysrs shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__":
main(sys.argv[1:])
pysrs-1.0/SocketMap.py 0000775 0001606 0000145 00000004776 11350251055 013674 0 ustar stuart bms # Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
#
# Base class for sendmail socket servers
import SocketServer
class MapError(Exception):
def __init__(self,code,reason):
self.code = code
self.reason = reason
class Handler(SocketServer.StreamRequestHandler):
def write(self,s):
"write netstring to socket"
self.wfile.write('%d:%s,' % (len(s),s))
self.log(s)
def _readlen(self,maxlen=8):
"read netstring length from socket"
n = ""
file = self.rfile
ch = file.read(1)
while ch != ":":
if not ch:
raise EOFError
if not ch in "0123456789":
raise ValueError
if len(n) >= maxlen:
raise OverflowError
n += ch
ch = file.read(1)
return int(n)
def read(self, maxlen=None):
"Read a netstring from the socket, and return the extracted netstring."
n = self._readlen()
if maxlen and n > maxlen:
raise OverflowError
file = self.rfile
s = file.read(n)
ch = file.read(1)
if ch == ',':
return s
if ch == "":
raise EOFError
raise ValueError
def handle(self):
#self.log("connect")
while True:
try:
line = self.read()
self.log(line)
args = line.split(' ',1)
map = args.pop(0).replace('-','_')
meth = getattr(self, '_handle_' + map, None)
if not map:
raise ValueError("Unrecognized map: %s" % map)
res = meth(*args)
self.write('OK ' + res)
except EOFError:
#self.log("Ending connection")
return
except MapError,x:
if code in ('PERM','TIMEOUT','NOTFOUND','OK','TEMP'):
self.write("%s %s"%(x.code,x.reason))
else:
self.write("%s %s %s"%('PERM',x.code,x.reason))
except LookupError,x:
self.write("NOTFOUND")
except Exception,x:
#print x
self.write("TEMP %s"%x)
# PERM,TIMEOUT
# Application should subclass SocketMap.Daemon, and define
# a _handler_map_name method for each sendmail socket map handled
# by this server. The socket is a unix domain socket which must match
# the socket defined in sendmail.cf.
#
# Socket maps in sendmail.cf look like this:
# Kmy_map socket local:/tmp/sockd
class Daemon(object):
def __init__(self,socket,handlerfactory):
self.socket = socket
try:
os.unlink(socket)
except: pass
self.server = SocketServer.ThreadingUnixStreamServer(socket,handlerfactory)
self.server.daemon = self
def run(self):
self.server.serve_forever()
pysrs-1.0/LICENSE.sendmail 0000664 0001606 0000145 00000010031 11350306627 014214 0 ustar stuart bms SENDMAIL LICENSE
The following license terms and conditions apply, unless a different
license is obtained from Sendmail, Inc., 6425 Christie Ave, Fourth Floor,
Emeryville, CA 94608, USA, or by electronic mail at license@sendmail.com.
License Terms:
Use, Modification and Redistribution (including distribution of any
modified or derived work) in source and binary forms is permitted only if
each of the following conditions is met:
1. Redistributions qualify as "freeware" or "Open Source Software" under
one of the following terms:
(a) Redistributions are made at no charge beyond the reasonable cost of
materials and delivery.
(b) Redistributions are accompanied by a copy of the Source Code or by an
irrevocable offer to provide a copy of the Source Code for up to three
years at the cost of materials and delivery. Such redistributions
must allow further use, modification, and redistribution of the Source
Code under substantially the same terms as this license. For the
purposes of redistribution "Source Code" means the complete compilable
and linkable source code of sendmail including all modifications.
2. Redistributions of source code must retain the copyright notices as they
appear in each source code file, these license terms, and the
disclaimer/limitation of liability set forth as paragraph 6 below.
3. Redistributions in binary form must reproduce the Copyright Notice,
these license terms, and the disclaimer/limitation of liability set
forth as paragraph 6 below, in the documentation and/or other materials
provided with the distribution. For the purposes of binary distribution
the "Copyright Notice" refers to the following language:
"Copyright (c) 1998-2004 Sendmail, Inc. All rights reserved."
4. Neither the name of Sendmail, Inc. nor the University of California nor
the names of their contributors may be used to endorse or promote
products derived from this software without specific prior written
permission. The name "sendmail" is a trademark of Sendmail, Inc.
5. All redistributions must comply with the conditions imposed by the
University of California on certain embedded code, whose copyright
notice and conditions for redistribution are as follows:
(a) Copyright (c) 1988, 1993 The Regents of the University of
California. All rights reserved.
(b) Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
(i) Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
(ii) Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
(iii) Neither the name of the University nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
6. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY
SENDMAIL, INC. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
NO EVENT SHALL SENDMAIL, INC., THE REGENTS OF THE UNIVERSITY OF
CALIFORNIA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
$Revision: 1.1 $, Last updated $Date: 2010/03/18 02:19:03 $
pysrs-1.0/makefile 0000664 0001606 0000145 00000000403 11534024151 013107 0 ustar stuart bms web:
doxygen
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
VERSION=1.0
CVSTAG=pysrs-1_0
PKG=pysrs-$(VERSION)
SRCTAR=$(PKG).tar.gz
$(SRCTAR):
cvs export -r$(CVSTAG) -d $(PKG) pysrs
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
cvstar: $(SRCTAR)
pysrs-1.0/SRS/ 0000775 0001606 0000145 00000000000 11555576327 012105 5 ustar stuart bms pysrs-1.0/SRS/Daemon.py 0000775 0001606 0000145 00000005220 10247754403 013652 0 ustar stuart bms # Exim compatible socket server for SRS
# $Log: Daemon.py,v $
# Revision 1.1.1.2 2005/06/03 04:13:55 customdesigned
# Support sendmail socketmap
#
# Revision 1.3 2004/08/26 03:31:38 stuart
# Introduce sendmail socket map
#
# Revision 1.2 2004/03/23 18:46:38 stuart
# support commandline args for key
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
from Guarded import Guarded
import os
import os.path
import SocketServer
SRSSOCKET = '/tmp/srsd';
class EximHandler(SocketServer.StreamRequestHandler):
def handle(self):
srs = self.server.srs
sock = self.rfile
try:
line = self.rfile.readline()
#print "Read '%s' on %s\n" % (line.strip(),self.request)
args = line.split()
cmd = args.pop(0).upper()
if cmd == 'FORWARD':
res = srs.forward(*args)
elif cmd == 'REVERSE':
res = srs.reverse(*args)
else:
raise ValueError("Invalid command %s" % cmd)
except Exception,x:
res = "ERROR: %s"%x
self.wfile.write(res+'\n')
class Daemon(object):
def __init__(self,secret=None,secretfile=None,socket=SRSSOCKET,
*args,**kw):
secrets = []
if secret: secrets += secret
if secretfile and os.path.exists(secretfile):
assert os.path.isfile(secretfile) and os.access(secretfile,os.R_OK), \
"Secret file $secretfile not readable"
FH = open(secretfile)
for ln in FH:
if not ln: continue
if ln.startswith('#'): continue
secrets += ln
FH.close()
assert secrets, \
"""No secret or secretfile given. Use --secret or --secretfile,
and ensure the secret file is not empty."""
# Preserve the pertinent original arguments, mostly for fun.
self.secret = secret
self.secretfile = secretfile
self.socket = socket
self.srs = Guarded(secret=secrets,*args,**kw)
try:
os.unlink(socket)
except:
pass
self.server = SocketServer.UnixStreamServer(socket,EximHandler)
self.server.srs = self.srs
def run(self):
self.server.serve_forever()
def main(args):
from getopt import getopt
opts,args = getopt(args,'',['secret=','secretfile='])
kw = dict([(opt[2:],val) for opt,val in opts])
server = Daemon(*args,**kw)
server.run()
if __name__ == '__main__':
import sys
main(sys.argv[1:])
pysrs-1.0/SRS/Reversible.py 0000664 0001606 0000145 00000003002 10375005313 014531 0 ustar stuart bms # $Log: Reversible.py,v $
# Revision 1.2 2006/02/16 05:16:59 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.1 2005/06/03 04:13:18 customdesigned
# Initial import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
from Shortcut import Shortcut
class Reversible(Shortcut):
"""A fully reversible Sender Rewriting Scheme
See SRS for details of the standard SRS subclass interface.
This module provides the methods compile() and parse(). It operates
without store."""
def compile(self,sendhost,senduser,srshost=None):
timestamp = self.timestamp_create()
# This has to be done in compile, because we might need access
# to it for storing in a database.
hash = self.hash_create(timestamp,sendhost,senduser)
if sendhost == srshost:
sendhost = ''
# Note that there are 4 fields here and that sendhost may
# not contain a + sign. Therefore, we do not need to escape
# + signs anywhere in order to reverse this transformation.
return SRS.SRS0TAG + self.separator + \
SRS.SRSSEP.join((hash,timestamp,sendhost,senduser))
pysrs-1.0/SRS/DB.py 0000664 0001606 0000145 00000004531 10375005313 012724 0 ustar stuart bms # $Log: DB.py,v $
# Revision 1.2 2006/02/16 05:16:59 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.1 2005/06/03 04:13:18 customdesigned
# Initial import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import bsddb
import time
import SRS
from Base import Base
from cPickle import dumps, loads
class DB(Base):
"""A MLDBM based Sender Rewriting Scheme
SYNOPSIS
from SRS.DB import DB
srs = DB(Database='/var/run/srs.db', ...)
DESCRIPTION
See Base.py for details of the standard SRS subclass interface.
This module provides the methods compile() and parse().
This module requires one extra parameter to the constructor, a filename
for a Berkeley DB_File database.
BUGS
This code relies on not getting collisions in the cryptographic
hash. This can and should be fixed.
The database is not garbage collected."""
def __init__(self,database='/var/run/srs.db',hashlength=24,*args,**kw):
Base.__init__(self,hashlength=hashlength,*args,**kw)
assert database, "No database specified for SRS.DB"
self.dbm = bsddb.btopen(database,'c')
def compile(self,sendhost,senduser,srshost=None):
ts = time.time()
data = dumps((ts,sendhost,senduser))
# We rely on not getting collisions in this hash.
hash = self.hash_create(sendhost,senduser)
self.dbm[hash] = data
# Note that there are 4 fields here and that sendhost may
# not contain a + sign. Therefore, we do not need to escape
# + signs anywhere in order to reverse this transformation.
return SRS.SRS0TAG + self.separator + hash
def parse(self,user,srshost=None):
user,m = self.srs0re.subn('',user,1)
assert m, "Reverse address does not match %s." % self.srs0re.pattern
hash = user
data = self.dbm[hash]
ts,sendhost,senduser = loads(data)
assert self.hash_verify(hash,sendhost,senduser), "Invalid hash"
assert self.time_check(ts), "Invalid timestamp"
return (sendhost, senduser)
pysrs-1.0/SRS/Shortcut.py 0000664 0001606 0000145 00000006420 10375005313 014251 0 ustar stuart bms # $Log: Shortcut.py,v $
# Revision 1.2 2006/02/16 05:16:59 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.1 2005/06/03 04:13:18 customdesigned
# Initial import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
from Base import Base
class Shortcut(Base):
"""SRS.Shortcut - A shortcutting Sender Rewriting Scheme
SYNOPSIS
import SRS.Shortcut
srs = SRS.Shortcut(...)
DESCRIPTION
WARNING: Using the simple Shortcut strategy is a very bad idea. Use the
Guarded strategy instead. The weakness in the Shortcut strategy is
documented at http://www.anarres.org/projects/srs/
See Mail::SRS for details of the standard SRS subclass interface.
This module provides the methods compile() and parse(). It operates
without store, and shortcuts around all middleman resenders."""
def compile(self,sendhost,senduser,srshost=None):
senduser,m = self.srs0re.subn('',senduser,1)
if m:
# This duplicates effort in Guarded.pm but makes this file work
# standalone.
# We just do the split because this was hashed with someone
# else's secret key and we can't check it.
# hash, timestamp, host, user
undef,undef,sendhost,senduser = senduser.split(SRS.SRSSEP,3)
# We should do a sanity check. After all, it might NOT be
# an SRS address, unlikely though that is. We are in the
# presence of malicious agents. However, this code is
# never reached if the Guarded subclass is used.
else:
senduser,m = self.srs1re.subn('',senduser,1)
if m:
# This should never be hit in practice. It would be bad.
# Introduce compatibility with the guarded format?
# SRSHOST, hash, timestamp, host, user
sendhost,senduser = senduser.split(SRS.SRSSEP,5)[-2:]
timestamp = self.timestamp_create()
hash = self.hash_create(timestamp, sendhost, senduser)
if sendhost == srshost:
sendhost = ''
# Note that there are 5 fields here and that sendhost may
# not contain a valid separator. Therefore, we do not need to
# escape separators anywhere in order to reverse this
# transformation.
return SRS.SRS0TAG + self.separator + \
SRS.SRSSEP.join((hash,timestamp,sendhost,senduser))
def parse(self,user,srshost=None):
user,m = self.srs0re.subn('',user,1)
# We should deal with SRS1 addresses here, just in case?
assert m, "Reverse address does not match %s." % self.srs0re.pattern
# The 4 here matches the number of fields we encoded above. If
# there are more separators, then they belong in senduser anyway.
hash,timestamp,sendhost,senduser = user.split(SRS.SRSSEP,3)[-4:]
if not sendhost and srshost:
sendhost = srshost
# Again, this must match as above.
assert self.hash_verify(hash,timestamp,sendhost,senduser), "Invalid hash"
assert self.timestamp_check(timestamp), "Invalid timestamp"
return sendhost,senduser
pysrs-1.0/SRS/Base.py 0000664 0001606 0000145 00000025326 11534330365 013324 0 ustar stuart bms # $Log: Base.py,v $
# Revision 1.5 2011/03/05 03:41:41 customdesigned
# 2.4 compatibility
#
# Revision 1.4 2011/03/03 23:46:49 customdesigned
# Release 1.0
#
# Revision 1.3 2008/02/13 18:20:18 customdesigned
# Handle quoted localpart.
#
# Revision 1.2 2006/02/16 05:16:59 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.2 2005/06/03 04:13:55 customdesigned
# Support sendmail socketmap
#
# Revision 1.3 2004/06/09 00:29:25 stuart
# Use hmac instead of straight sha
#
# Revision 1.2 2004/03/22 18:20:19 stuart
# Missing import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import time
import hmac
try: from hashlib import sha1 as sha
except: import sha
import base64
import re
import SRS
import sys
BASE26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
BASE32 = BASE26 + '234567'
BASE64 = BASE26 + BASE26.lower() + '0123456789+/'
# We have two options. We can either encode an send date or an expiry
# date. If we encode a send date, we have the option of changing
# the expiry date later. If we encode an expiry date, we can send
# different expiry dates for different sources/targets, and we don't
# have to store them.
# Do NOT use BASE64 since the timestamp_check routine now explicit
# smashes case in the timestamp just in case there was a problem.
BASE = BASE32
# This checks for more than one bit set in the size.
# i.e. is the size a power of 2?
base = len(BASE)
if base & (base - 1):
raise ValueError("Invalid base array of size %d" % base)
PRECISION = 60 * 60 * 24 # One day
TICKSLOTS = base * base # Two chars
def parse_addr(sender):
quotes = ''
try:
pos = sender.rindex('@')
senduser = sender[:pos]
sendhost = sender[pos+1:]
if senduser.startswith('"') and senduser.endswith('"'):
senduser = senduser[1:-1]
quotes = '"'
except ValueError:
raise ValueError("Sender '%s' must contain exactly one @" % sender)
return quotes,senduser,sendhost
class Base(object):
def __init__(self,secret=None,maxage=SRS.SRSMAXAGE,
hashlength=SRS.SRSHASHLENGTH,
hashmin=None,separator='=',alwaysrewrite=False,ignoretimestamp=False,
allowunsafesrs=False):
if type(secret) == str:
self.secret = (secret,)
else:
self.secret = secret
self.maxage = maxage
self.hashlength =hashlength
if hashmin: self.hashmin = hashmin
else: self.hashmin = hashlength
self.separator = separator
if not separator in ('-','+','='):
raise ValueError('separator must be = - or +, not %s' % separator)
self.alwaysrewrite = alwaysrewrite
self.ignoretimestamp = ignoretimestamp
self.allowunsafesrs = allowunsafesrs
self.srs0re = re.compile(r'^%s[-+=]' % SRS.SRS0TAG,re.IGNORECASE)
self.srs1re = re.compile(r'^%s[-+=]' % SRS.SRS1TAG,re.IGNORECASE)
#self.ses0re = re.compile(r'^%s[-+=]' % SRS.SES0TAG,re.IGNORECASE)
def warn(self,*msg):
print >>sys.stderr,'WARNING: ',' '.join(msg)
def sign(self,sender):
"""srsaddress = srs.sign(sender)
Map a sender address into the same sender and a cryptographic cookie.
Returns an SRS address to use for preventing bounce abuse.
There are alternative subclasses, some of which will return SRS
compliant addresses, some will simply return non-SRS but valid RFC821
addresses. """
quotes,senduser,sendhost = parse_addr(sender)
# Subclasses may override the compile() method.
srsdata = self.compile(sendhost,senduser,srshost=sendhost)
return '%s%s%s@%s' % (quotes,srsdata,quotes,sendhost)
def forward(self,sender,alias,sign=False):
"""srsaddress = srs.forward(sender, alias)
Map a sender address into a new sender and a cryptographic cookie.
Returns an SRS address to use as the new sender.
There are alternative subclasses, some of which will return SRS
compliant addresses, some will simply return non-SRS but valid RFC821
addresses. """
quotes,senduser,sendhost = parse_addr(sender)
# We don't require alias to be a full address, just a domain will do
aliashost = alias.split('@')[-1]
if aliashost.lower() == sendhost.lower() and not self.alwaysrewrite:
return '%s%s%s@%s' % (quotes,senduser,quotes,sendhost)
# Subclasses may override the compile() method.
if sign:
srsdata = self.compile(sendhost,senduser,srshost=aliashost)
else:
srsdata = self.compile(sendhost,senduser)
return '%s%s%s@%s' % (quotes,srsdata,quotes,aliashost)
def reverse(self,address):
"""sender = srs->reverse(srsaddress)
Reverse the mapping to get back the original address. Validates all
cryptographic and timestamp information. Returns the original sender
address. This method will die if the address cannot be reversed."""
quotes,user,host = parse_addr(address)
sendhost,senduser = self.parse(user,srshost=host)
return '%s%s%s@%s' % (quotes,senduser,quotes,sendhost)
def compile(self,sendhost,senduser):
"""srsdata = srs.compile(host,user)
This method, designed to be overridden by subclasses, takes as
parameters the original host and user and must compile a new username
for the SRS transformed address. It is expected that this new username
will be joined on SRS.SRSSEP, and will contain a hash generated from
self.hash_create(...), and possibly a timestamp generated by
self.timestamp_create()."""
raise NotImplementedError()
def parse(self,srsuser):
"""host,user = srs.parse(srsuser)
This method, designed to be overridden by subclasses, takes an
SRS-transformed username as an argument, and must reverse the
transformation produced by compile(). It is required to verify any
hash and timestamp in the parsed data, using self.hash_verify(hash,
...) and self->timestamp_check(timestamp)."""
raise NotImplementedError()
def timestamp_create(self,ts=None):
"""timestamp = srs.timestamp_create(time)
Return a two character timestamp representing 'today', or time if
given. time is a Unix timestamp (seconds since the aeon).
This Python function has been designed to be agnostic as to base,
and in practice, base32 is used since it can be reversed even if a
remote MTA smashes case (in violation of RFC2821 section 2.4). The
agnosticism means that the Python uses division instead of rightshift,
but in Python that doesn't matter. C implementors should implement this
operation as a right shift by 5."""
if not ts:
ts = time.time()
# Since we only mask in the bottom few bits anyway, we
# don't need to take this modulo anything (e.g. @BASE^2).
ts = int(ts // PRECISION)
# print "Time is $time\n";
mask = base - 1
out = BASE[ts & mask]
ts //= base # Use right shift.
return BASE[ts & mask]+out
def timestamp_check(self,timestamp):
"""srs.timestamp_check(timestamp)
Return True if a timestamp is valid, False otherwise. There are 4096
possible timestamps, used in a cycle. At any time, $srs->{MaxAge}
timestamps in this cycle are valid, the last one being today. A
timestamp from the future is not valid, neither is a timestamp from
too far into the past. Of course if you go far enough into the future,
the cycle wraps around, and there are valid timestamps again, but the
likelihood of a random timestamp being valid is 4096/$srs->{MaxAge},
which is usually quite small: 1 in 132 by default."""
if self.ignoretimestamp: return True
ts = 0
for d in timestamp.upper(): # LOOK OUT - USE BASE32
ts = ts * base + BASE.find(d)
now = (time.time() // PRECISION) % TICKSLOTS
# print "Time is %d, Now is %d" % (ts,now)
while now < ts: now += TICKSLOTS
if now <= ts + self.maxage: return True
return False
def time_check(self,ts):
"""srs.time_check(time)
Similar to srs.timestamp_check(timestamp), but takes a Unix time, and
checks that an alias created at that Unix time is still valid. This is
designed for use by subclasses with storage backends."""
return time.time() <= (ts + (self.maxage * PRECISION))
def hash_create(self,*data):
"""srs.hash_create(data,...)
Returns a cryptographic hash of all data in data. Any piece of data
encoded into an address which must remain inviolate should be hashed,
so that when the address is reversed, we can check that this data has
not been tampered with. You must provide at least one piece of data
to this method (otherwise this system is both cryptographically weak
and there may be collision problems with sender addresses)."""
secret = self.get_secret()
assert secret, "Cannot create a cryptographic MAC without a secret"
h = hmac.new(secret[0],'',sha)
for i in data:
h.update(i.lower())
hash = base64.encodestring(h.digest())
return hash[:self.hashlength]
def hash_verify(self,hash,*data):
"""srs.hash_verify(hash,data,...)
Verify that data has not been tampered with, given the cryptographic
hash previously output by srs->hash_create(). Returns True or False.
All known secrets are tried in order to see if the hash was created
with an old secret."""
if len(hash) < self.hashmin: return False
secret = self.get_secret()
assert secret, "Cannot create a cryptographic MAC without a secret"
hashes = []
for s in secret:
h = hmac.new(s,'',sha)
for i in data:
h.update(i.lower())
valid = base64.encodestring(h.digest())[:len(hash)]
# We test all case sensitive matches before case insensitive
# matches. While the risk of a case insensitive collision is
# quite low, we might as well be careful.
if valid == hash: return True
hashes.append(valid) # lowercase it later
hash = hash.lower()
for h in hashes:
if hash == h.lower():
self.warn("""SRS: Case insensitive hash match detected.
Someone smashed case in the local-part.""")
return True
return False;
def set_secret(self,*args):
"""srs.set_secret(new,old,...)
Add a new secret to the rewriter. When an address is returned, all
secrets are tried to see if the hash can be validated. Don't use "foo",
"secret", "password", "10downing", "god" or "wednesday" as your secret."""
self.secret = args
def get_secret(self):
"Return the list of secrets. These are secret. Don't publish them."
return self.secret
def separator(self):
"""srs.separator()
Return the initial separator, which follows the SRS tag. This is only
used as the initial separator, for the convenience of administrators
who wish to make srs0 and srs1 users on their mail servers and require
to use + or - as the user delimiter. All other separators in the SRS
address must be C<=>."""
return self.separator
pysrs-1.0/SRS/Guarded.py 0000664 0001606 0000145 00000006667 10375005313 014026 0 ustar stuart bms # $Log: Guarded.py,v $
# Revision 1.2 2006/02/16 05:16:59 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.1 2005/06/03 04:13:18 customdesigned
# Initial import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import re
import SRS
from Shortcut import Shortcut
class Guarded(Shortcut):
"""This is the default subclass of SRS. An instance of this subclass
is actually constructed when "new SRS" is called.
Note that allowing variable separators after the SRS\d token means that
we must preserve this separator in the address for a possible reversal.
SRS1 does not need to understand the SRS0 address, just preserve it,
on the assumption that it is valid and that the host doing the final
reversal will perform cryptographic tests. It may therefore strip just
the string SRS0 and not the separator. This explains the appearance
of a double separator in SRS1=.
See Mail::SRS for details of the standard SRS subclass interface.
This module provides the methods compile() and parse(). It operates
without store, and guards against gaming the shortcut system."""
def __init__(self,*args,**kw):
self.srs0rek = re.compile(r'^%s(?=[-+=])' % SRS.SRS0TAG,re.IGNORECASE)
Shortcut.__init__(self,*args,**kw)
def compile(self,sendhost,senduser,srshost=None):
senduser,m = self.srs1re.subn('',senduser,1)
if m:
# We could do a sanity check. After all, it might NOT be
# an SRS address, unlikely though that is. We are in the
# presence of malicious agents. However, since we don't need
# to interpret it, it doesn't matter if it isn't an SRS
# address. Our malicious SRS0 party gets back the garbage
# he spat out.
# Actually, it turns out that we can simplify this
# function considerably, although it should be borne in mind
# that this address is not opaque to us, even though we didn't
# actually process or generate it.
# hash, srshost, srsuser
undef,srshost,srsuser = senduser.split(SRS.SRSSEP,2)
hash = self.hash_create(srshost,srsuser)
return SRS.SRS1TAG + self.separator + \
SRS.SRSSEP.join((hash,srshost,srsuser))
senduser,m = self.srs0rek.subn('',senduser,1)
if m:
hash = self.hash_create(sendhost, senduser)
return SRS.SRS1TAG + self.separator + \
SRS.SRSSEP.join((hash,sendhost,senduser))
return Shortcut.compile(self,sendhost,senduser,srshost=srshost)
def parse(self,user,srshost=None):
user,m = self.srs1re.subn('',user,1)
if m:
hash,srshost,srsuser = user.split(SRS.SRSSEP, 2)[-3:]
if hash.find('.') >= 0:
assert self.allowunsafesrs, \
"Hashless SRS1 address received when AllowUnsafeSrs is not set"
# Reconstruct the parameters as they were in the old format.
srsuser = srshost + SRS.SRSSEP + srsuser
srshost = hash
else:
assert srshost and srsuser, "Invalid SRS1 address"
assert self.hash_verify(hash,srshost,srsuser), "Invalid hash"
return srshost, SRS.SRS0TAG + srsuser
return Shortcut.parse(self,user,srshost=srshost)
pysrs-1.0/SRS/__init__.py 0000664 0001606 0000145 00000003322 10553236567 014212 0 ustar stuart bms # $Log: __init__.py,v $
# Revision 1.4 2007/01/16 21:02:47 customdesigned
# Support logging recipient host and nosrs in pysrs.cfg
#
# Revision 1.3 2006/02/16 05:21:25 customdesigned
# Support SRS signing mode.
#
# Revision 1.2 2005/08/11 23:35:32 customdesigned
# SES support.
#
# Revision 1.1.1.2 2005/06/03 04:13:56 customdesigned
# Support sendmail socketmap
#
# Revision 1.6 2004/08/26 03:31:38 stuart
# Introduce sendmail socket map
#
# Revision 1.5 2004/06/09 00:32:05 stuart
# Release 0.30.8
#
# Revision 1.4 2004/03/24 23:59:42 stuart
# Release 0.30.7
#
# Revision 1.3 2004/03/23 20:36:39 stuart
# Version 0.30.6
#
# Revision 1.2 2004/03/22 18:20:19 stuart
# Missing import
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
__version__ = '0.30.12'
__all__= [
'Base',
'Guarded',
'Shortcut',
'Reversible',
'Daemon',
'DB',
'new',
'SRS0TAG',
'SRS1TAG',
'SRSSEP',
'SRSHASHLENGTH',
'SRSMAXAGE',
'__version__'
]
SRS0TAG = 'SRS0'
SRS1TAG = 'SRS1'
SRSSEP = '='
SRSHASHLENGTH = 4
SRSMAXAGE = 21
#from Base import SRS
#from Guarded import Guarded
#from Shortcut import Shortcut
#from Reversible import Reversible
#from Daemon import Daemon
#from DB import DB
import Guarded
def new(secret=None,*args,**kw):
return Guarded.Guarded(secret,*args,**kw)
pysrs-1.0/srs2envtol.py 0000664 0001606 0000145 00000003244 11350251056 014112 0 ustar stuart bms #!/usr/bin/python2.3
# sendmail program map for SRS
#
# Use only if absolutely necessary. It is *very* inefficient and
# a security risk.
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
import re
from ConfigParser import ConfigParser, DuplicateSectionError
# get SRS parameters from milter configuration
cp = ConfigParser({
'secret': 'shhhh!',
'maxage': '8',
'hashlength': '8',
'separator': '='
})
cp.read(["/etc/mail/pysrs.cfg"])
try:
cp.add_section('srs')
except DuplicateSectionError:
pass
srs = SRS.new(
secret=cp.get('srs','secret'),
maxage=cp.getint('srs','maxage'),
hashlength=cp.getint('srs','hashlength'),
separator=cp.get('srs','separator')
)
srs.warn = lambda x: x # ignore case smash warning
del cp
def reverse(old_address):
# Munge ParseLocal recipient in the same manner as required
# in EnvFromSMTP.
use_address = re.compile(r'[<>]').sub('',old_address)
use_address = re.compile(r'\.$').sub('',use_address)
# Just try and reverse the address. If we succeed, return this
# new address; else, return the old address (quoted if it was
# a piped alias).
try:
use_address = srs.reverse(use_address)
while True:
try:
use_address = srs.reverse(use_address)
except: break
return use_address.replace('@','<@',1)+'.>'
except:
if use_address.startswith('|'):
return '"%s"' % old_address
else:
return old_address
if __name__ == "__main__":
import sys
# No funny business in our output, please
sys.stderr.close()
print reverse(sys.argv[1])
pysrs-1.0/srsmilter.py 0000775 0001606 0000145 00000020427 11555576263 014043 0 ustar stuart bms # A simple SRS milter for Sendmail-8.14/Postfix-?
#
# INCOMPLETE!!
#
# The logical problem is that a milter gets to change MFROM only once for
# multiple recipients. When there is a conflict between recipients, we
# either have to punt (all SRS or all no-SRS) or resubmit some of the
# recipients to "split" the message.
#
# The sendmail cf package, in contrast, gets invoked for every recipient.
# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html
# Author: Stuart D. Gathman
# Copyright 2007 Business Management Systems, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
import SES
import sys
import Milter
import spf
import syslog
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr
syslog.openlog('srsmilter',0,syslog.LOG_MAIL)
class Config(object):
"Hold configuration options."
def __init__(conf,cfglist):
cp = MilterConfigParser()
cp.read(cfglist)
if cp.has_option('milter','datadir'):
os.chdir(cp.get('milter','datadir')) # FIXME: side effect!
conf.socketname = cp.getdefault('milter','socketname',
'/var/run/milter/pysrs')
conf.miltername = cp.getdefault('milter','name','pysrsfilter')
conf.trusted_relay = cp.getlist('milter','trusted_relay')
conf.internal_connect = cp.getlist('milter','internal_connect')
conf.trusted_forwarder = cp.getlist('srs','trusted_forwarder')
conf.secret = cp.getdefault('srs','secret','shhhh!')
conf.maxage = cp.getintdefault('srs','maxage',21)
conf.hashlength = cp.getintdefault('srs','hashlength',5)
conf.separator = cp.getdefault('srs','separator','=')
conf.database = cp.getdefault('srs','database')
conf.srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
conf.nosrsdomain = cp.getlist('srs','nosrs') # no SRS rcpt
conf.banned_users = cp.getlist('srs','banned_users')
conf.srs_domain = set(cp.getlist('srs','srs')) # check rcpt
conf.sesdomain = set(cp.getlist('srs','ses')) # sign from with ses
conf.signdomain = set(cp.getlist('srs','sign')) # sign from with srs
conf.fwdomain = cp.getdefault('srs','fwdomain',None) # forwarding domain
if database:
import SRS.DB
conf.srs = SRS.DB.DB(database=conf.database,secret=conf.secret,
maxage=conf.maxage,hashlength=conf.hashlength,separator=conf.separator)
else:
conf.srs = SRS.Guarded.Guarded(secret=conf.secret,
maxage=conf.maxage,hashlength=conf.hashlength,separator=conf.separator)
if SES:
conf.ses = SES.new(secret=conf.secret,expiration=conf.maxage)
conf.srs_domain = set(conf.sesdomain)
conf.srs_domain.update(conf.srs_domain)
else:
conf.srs_domain = set(conf.srs_domain)
conf.srs_domain.update(conf.signdomain)
if conf.fwdomain:
conf.srs_domain.add(conf.fwdomain)
class srsMilter(Milter.Base):
"Milter to check SRS. Each connection gets its own instance."
def log(self,*msg):
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
def __init__(self):
self.mailfrom = None
self.id = Milter.uniqueID()
# we don't want config used to change during a connection
self.conf = config
bracketRE = re.compile(r'^<|>$|\.>$')
srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE)
def make_srs(self,old_address):
h = self.receiver
nosrsdomain = self.conf.nosrsdomain
if old_address == '<>' or not h or h in nosrsdomain:
return old_address
srs = self.conf.srs
ses = self.conf.ses
fwdomain = self.conf.fwdomain
sesdomain = self.conf.sesdomain
signdomain = self.conf.signdomain
use_address = self.bracketRE.sub('',old_address)
# Ok, first check whether we already have a signed SRS address;
# if so, just return the old address: we do not want to double-sign
# by accident!
#
# Else, gimme a valid SRS signed address, munge it back the way
# sendmail wants it at this point; or just return the old address,
# in case nothing went.
try:
new_address = srs.reverse(use_address)
return old_address
except:
try:
senduser,sendhost = use_address.split('@')
shl = sendhost.lower()
if shl in sesdomain:
new_address = ses.sign(use_address)
elif shl in signdomain:
new_address = srs.sign(use_address)
else:
new_address = srs.forward(use_address,fwdomain)
return '<%s>'%new_address
except:
return old_address
@Milter.noreply
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
if iniplist(ipaddr,self.conf.internal_connect):
self.internal_connection = True
if iniplist(ipaddr,self.conf.trusted_relay):
self.trusted_relay = True
else: ipaddr = ''
self.connectip = ipaddr
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if self.trusted_relay:
connecttype += ' TRUSTED'
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
return Milter.CONTINUE
@Milter.noreply
def envfrom(self,f,*str):
self.log("mail from",f,str)
self.mailfrom = f
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
self.srsrcpt = []
self.nosrsrcpt = []
self.redirect_list = []
self.discard_list = []
self.is_bounce = (f == '<>' or t[0].lower() in self.conf.banned_users)
self.data_allowed = True
return Milter.CONTINUE
## Accumulate deleted recipients to be applied in eom callback.
def del_recipient(self,rcpt):
rcpt = rcpt.lower()
if not rcpt in self.discard_list:
self.discard_list.append(rcpt)
## Accumulate added recipients to be applied in eom callback.
def add_recipient(self,rcpt):
rcpt = rcpt.lower()
if not rcpt in self.redirect_list:
self.redirect_list.append(rcpt)
def envrcpt(self,to,*str):
conf = self.conf
t = parse_addr(to)
if len(t) == 2:
t[1] = t[1].lower()
user,domain = t
if self.is_bounce and domain in conf.srs_domain:
# require valid signed recipient
oldaddr = '@'.join(parse_addr(to))
try:
if conf.ses:
newaddr = ses.verify(oldaddr)
else:
newaddr = oldaddr,
if len(newaddr) > 1:
newaddr = newaddr[0]
self.log("ses rcpt:",newaddr)
else:
newaddr = srs.reverse(oldaddr)
self.log("srs rcpt:",newaddr)
self.del_recipient(to)
self.add_recipient('<%s>',newaddr)
except:
# no valid SRS signature
if not (self.internal_connection or self.trusted_relay):
if self.srsre.match(oldaddr):
self.log("REJECT: srs spoofed:",oldaddr)
self.setreply('550','5.7.1','Invalid SRS signature')
return Milter.REJECT
if oldaddr.startswith('SES='):
self.log("REJECT: ses spoofed:",oldaddr)
self.setreply('550','5.7.1','Invalid SES signature')
return Milter.REJECT
self.data_allowed = not conf.srs_reject_spoofed
else:
# sign "outgoing" from
if domain in nosrsdomain:
self.nosrsrcpt.append(to)
else:
self.srsrcpt.append(to)
else: # no SRS for unqualified recipients
self.nosrsrcpt.append(to)
return Milter.CONTINUE
def eom(self):
for name,val,idx in self.new_headers:
try:
self.addheader(name,val,idx)
except:
self.addheader(name,val) # older sendmail can't insheader
return Milter.CONTINUE
if __name__ == "__main__":
Milter.factory = srsMilter
Milter.set_flags(Milter.CHGFROM + Milter.DELRCPT)
global config
config = Config(['spfmilter.cfg','/etc/mail/spfmilter.cfg'])
miltername = config.miltername
socketname = config.socketname
print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=%s
X%s, S=local:%s
See the sendmail README for libmilter.
sample srsmilter startup""" % (miltername,miltername,socketname)
sys.stdout.flush()
Milter.runmilter("pysrsfilter",socketname,240)
print "sample srsmilter shutdown"
pysrs-1.0/LICENSE.python 0000664 0001606 0000145 00000004535 11350306627 013755 0 ustar stuart bms PSF LICENSE AGREEMENT FOR PYTHON 2.4
------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using Python 2.4 software in source or binary form and its
associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 2.4
alone or in any derivative version, provided, however, that PSF's
License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved"
are retained in Python 2.4 alone or in any derivative version prepared
by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 2.4 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 2.4.
4. PSF is making Python 2.4 available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.4 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.4,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python 2.4, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
pysrs-1.0/envfrom2srs.py 0000664 0001606 0000145 00000004000 11350251055 014245 0 ustar stuart bms #!/usr/bin/python2.3
# sendmail program map for SRS
#
# Use only if absolutely necessary. It is *very* inefficient and
# a security risk.
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import SRS
import re
from ConfigParser import ConfigParser, DuplicateSectionError
# get SRS parameters from milter configuration
cp = ConfigParser({
'secret': 'shhhh!',
'maxage': '8',
'hashlength': '8',
'fwdomain': 'mydomain.com',
'separator': '='
})
cp.read(["/etc/mail/pysrs.cfg"])
try:
cp.add_section('srs')
except DuplicateSectionError:
pass
srs = SRS.new(
secret=cp.get('srs','secret'),
maxage=cp.getint('srs','maxage'),
hashlength=cp.getint('srs','hashlength'),
separator=cp.get('srs','separator'),
alwaysrewrite=True # pysrs.m4 can skip calling us for local domains
)
fwdomain = cp.get('srs','fwdomain')
del cp
# Our original envelope-from may look funny on entry
# of this Ruleset:
#
# admin<@asarian-host.net.>
#
# We need to preprocess it some:
def forward(old_address):
if old_address == '<@>':
return old_address
use_address = re.compile(r'[<>]').sub('',old_address)
use_address = re.compile(r'\.$').sub('',use_address)
# Ok, first check whether we already have a signed SRS address;
# if so, just return the old address: we do not want to double-sign
# by accident!
#
# Else, gimme a valid SRS signed address, munge it back the way
# sendmail wants it at this point; or just return the old address,
# in case nothing went.
try:
new_address = srs.reverse(use_address)
return old_address
except:
try:
new_address = srs.forward(use_address,fwdomain)
return new_address.replace('@','<@',1)+'.>'
except:
return old_address
if __name__ == "__main__":
import sys
# No funny business in our output, please
sys.stderr.close()
if len(sys.argv) > 2:
fwdomain = sys.argv[-2]
print forward(sys.argv[-1])
pysrs-1.0/setup.cfg 0000664 0001606 0000145 00000000214 10276760304 013241 0 ustar stuart bms [install]
compile = 1
optimize = 1
[bdist_rpm]
group = Development/Libraries
python=python2.4
packager=Stuart D. Gathman
pysrs-1.0/testSRS.py 0000664 0001606 0000145 00000014240 10754632342 013346 0 ustar stuart bms # $Log: testSRS.py,v $
# Revision 1.3 2008/02/13 18:20:18 customdesigned
# Handle quoted localpart.
#
# Revision 1.2 2006/02/16 05:16:58 customdesigned
# Support SRS signing mode.
#
# Revision 1.1.1.2 2005/06/03 04:13:55 customdesigned
# Support sendmail socketmap
#
# Revision 1.4 2004/08/26 03:31:38 stuart
# Introduce sendmail socket map
#
# Revision 1.3 2004/03/25 00:02:21 stuart
# FIXME where case smash test depends on day
#
# Revision 1.2 2004/03/22 18:20:00 stuart
# Read config for sendmail maps from /etc/mail/pysrs.cfg
#
# Revision 1.1.1.1 2004/03/19 05:23:13 stuart
# Import to CVS
#
#
# AUTHOR
# Shevek
# CPAN ID: SHEVEK
# cpan@anarres.org
# http://www.anarres.org/projects/
#
# Translated to Python by stuart@bmsi.com
# http://bmsi.com/python/milter.html
#
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import unittest
from SRS.Guarded import Guarded
from SRS.DB import DB
from SRS.Reversible import Reversible
from SRS.Daemon import Daemon
import SRS
import threading
import socket
class SRSTestCase(unittest.TestCase):
def setUp(self):
# make sure user modified tag works
SRS.SRS0TAG = 'ALT0'
SRS.SRS1TAG = 'ALT1'
def warn(self,*msg):
self.case_smashed = True
# There and back again
def testGuarded(self):
srs = Guarded()
self.assertRaises(AssertionError,srs.forward,
'mouse@disney.com','mydomain.com')
srs.set_secret('shhhh!')
srs.separator = '+'
sender = '"Blah blah"@orig.com'
srsaddr = srs.forward(sender,sender)
self.assertEqual(srsaddr,sender)
srsaddr = srs.forward(sender,'second.com')
self.failUnless(srsaddr.startswith('"'+SRS.SRS0TAG),srsaddr)
srsaddr1 = srs.forward(srsaddr,'third.com')
#print srsaddr1
self.failUnless(srsaddr1.startswith('"'+SRS.SRS1TAG))
srsaddr2 = srs.forward(srsaddr1,'fourth.com')
#print srsaddr2
self.failUnless(srsaddr2.startswith('"'+SRS.SRS1TAG))
addr = srs.reverse(srsaddr2)
self.assertEqual(srsaddr,addr)
addr = srs.reverse(srsaddr1)
self.assertEqual(srsaddr,addr)
addr = srs.reverse(srsaddr)
self.assertEqual(sender,addr)
def testSign(self):
srs = Guarded()
srs.set_secret('shhhh!')
srs.separator = '+'
sender = 'mouse@orig.com'
sig = srs.sign(sender)
addr = srs.reverse(sig)
self.assertEqual(sender,addr)
sender = 'mouse@ORIG.com'
sig = srs.sign(sender)
addr = srs.reverse(sig)
self.assertEqual(sender,addr)
addr = srs.reverse(sig.lower())
self.assertEqual(sender.lower(),addr)
def testCaseSmash(self):
srs = SRS.new(secret='shhhhh!',separator='+')
# FIXME: whether case smashing occurs depends on what day it is.
sender = 'mouse@fickle1.com'
srsaddr = srs.forward(sender,'second.com')
self.failUnless(srsaddr.startswith(SRS.SRS0TAG))
self.case_smashed = False
srs.warn = self.warn
addr = srs.reverse(srsaddr.lower())
self.failUnless(self.case_smashed) # check that warn was called
self.assertEqual(sender,addr)
def testReversible(self):
srs = Reversible()
self.assertRaises(AssertionError,srs.forward,
'mouse@disney.com','mydomain.com')
srs.set_secret('shhhh!')
srs.separator = '+'
sender = 'mouse@orig.com'
srsaddr = srs.forward(sender,sender)
self.assertEqual(srsaddr,sender)
srsaddr = srs.forward(sender,'second.com')
#print srsaddr
self.failUnless(srsaddr.startswith(SRS.SRS0TAG))
srsaddr1 = srs.forward(srsaddr,'third.com')
#print srsaddr1
self.failUnless(srsaddr1.startswith(SRS.SRS0TAG))
srsaddr2 = srs.forward(srsaddr1,'fourth.com')
#print srsaddr2
self.failUnless(srsaddr2.startswith(SRS.SRS0TAG))
addr = srs.reverse(srsaddr2)
self.assertEqual(srsaddr1,addr)
addr = srs.reverse(srsaddr1)
self.assertEqual(srsaddr,addr)
addr = srs.reverse(srsaddr)
self.assertEqual(sender,addr)
def testDB(self,database='/tmp/srstest'):
srs = DB(database=database)
self.assertRaises(AssertionError,srs.forward,
'mouse@disney.com','mydomain.com')
srs.set_secret('shhhh!')
sender = 'mouse@orig.com'
srsaddr = srs.forward(sender,sender)
self.assertEqual(srsaddr,sender)
srsaddr = srs.forward(sender,'second.com')
#print srsaddr
self.failUnless(srsaddr.startswith(SRS.SRS0TAG))
srsaddr1 = srs.forward(srsaddr,'third.com')
#print srsaddr1
self.failUnless(srsaddr1.startswith(SRS.SRS0TAG))
srsaddr2 = srs.forward(srsaddr1,'fourth.com')
#print srsaddr2
self.failUnless(srsaddr2.startswith(SRS.SRS0TAG))
addr = srs.reverse(srsaddr2)
self.assertEqual(srsaddr1,addr)
addr = srs.reverse(srsaddr1)
self.assertEqual(srsaddr,addr)
addr = srs.reverse(srsaddr)
self.assertEqual(sender,addr)
def run2(self): # handle two requests
self.daemon.server.handle_request()
self.daemon.server.handle_request()
def sendcmd(self,*args):
sock = socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
sock.connect(self.sockname)
sock.send(' '.join(args)+'\n')
res = sock.recv(128).strip()
sock.close()
return res
def testExim(self,sockname='/tmp/srsd',secret="shhhh!"):
self.sockname = sockname
self.daemon = Daemon(socket=sockname,secret=secret)
server = threading.Thread(target=self.run2,name='srsd')
server.start()
sender = 'mouse@orig.com'
srsaddr = self.sendcmd('FORWARD',sender,'second.com')
addr = self.sendcmd('REVERSE',srsaddr)
server.join()
self.assertEqual(sender,addr)
def testProgMap(self):
import envfrom2srs
import srs2envtol
orig = 'mickey<@orig.com.>'
newaddr = envfrom2srs.forward(orig)
self.failUnless(newaddr.endswith('.>'))
addr2 = srs2envtol.reverse(newaddr)
self.assertEqual(addr2,orig)
# check case smashing by braindead mailers
self.case_smashed = False
srs2envtol.srs.warn = self.warn
addr2 = srs2envtol.reverse(newaddr.lower())
self.assertEqual(addr2,orig)
self.failUnless(self.case_smashed)
def suite(): return unittest.makeSuite(SRSTestCase,'test')
if __name__ == '__main__':
unittest.main()
pysrs-1.0/testd.py 0000664 0001606 0000145 00000000622 11350251056 013111 0 ustar stuart bms # Simple test daemon
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
from socket import *
import sys
sock = socket(AF_UNIX,SOCK_STREAM)
sock.connect('/tmp/srsd')
sock.send(' '.join(sys.argv[1:])+'\n')
res = sock.recv(128).strip()
print res
sock.close()
pysrs-1.0/COPYING 0000664 0001606 0000145 00000001303 11555576263 012465 0 ustar stuart bms Python SRS is a rewritten version of the Mail::SRS Perl package by Shevek.
AUTHOR
Shevek
CPAN ID: SHEVEK
cpan@anarres.org
http://www.anarres.org/projects/
Translated to Python by stuart@bmsi.com
http://bmsi.com/python/milter.html
Portions Copyright (c) 2004 Shevek. All rights reserved.
Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the same terms as Python itself:
All the python code is licensed under the PSF license, a copy of which is in
LICENSE.python.
The sendmail m4 macros (pysrs.m4 and pysrsprog.m4) are licensed under
the Sendmail license, a copy of which is in LICENSE.sendmail.
pysrs-1.0/pysrs.m4 0000664 0001606 0000145 00000003660 11350251056 013043 0 ustar stuart bms divert(-1)
#
# Copyright (c) 2004 Alain Knaff (derived from work by asarian-host.net)
# All rights reserved.
# Copyright (c) 1988, 1993
# The Regents of the University of California. All rights reserved.
# Portions Copyright (c) 2004-2009
# Business Management Systems, Inc All rights reserved.
#
# By using this file, you agree to the terms and conditions set
# forth in the LICENSE file which can be found at the top level of
# the sendmail distribution.
#
#
divert(0)
VERSIONID(`$Id: pysrs.m4,v 1.5 2010/03/17 22:05:34 customdesigned Exp $')
ifdef(`_MAILER_DEFINED_',,`errprint(`*** WARNING: MAILER() should be before HACK(pysrs)
')')
ifdef(`_ARG_',,`errprint(`*** WARNING: HACK(pysrs,sockname) requires sockname
')')
ifelse(defn(`_ARG_'),`',,`define(`SRS_SOCKET',_ARG_)')
LOCAL_CONFIG
# Forward SRS map
Kmake_srs socket SRS_SOCKET
# Reverse SRS map
Kreverse_srs socket SRS_SOCKET
# "To" address is SRS
Kis_srs regex ^(SRS[01]|SES)[+=-].*
ifdef(`NO_SRS_FILE', `dnl
# Class of destination mailers not needing SRS
F{noSrsMailers}-o NO_SRS_FILE %[^\#]
')dnl
MAILER_DEFINITIONS
SEnvFromSMTP
R$+ $: $>MakeSrs $1 make SRS
LOCAL_RULESETS
SIsSrs
# Answers YES or NO whether the address in parameter is srs or not
R$* $: $( is_srs $1 $)
R$@ $@ YES
R$* $@ NO
SMakeSrs
ifdef(`NO_SRS_FROM_LOCAL',`dnl
#
# Prevent SRS encapsulation if "From" address is local
# (With a local from address, the forwarder mail will pass any SPF checks
# anyways, so why bother with SRS?)
R$* < @ $=w > $* $@ $1 < @ $2 > $3
R$* < @ $=w . > $* $@ $1 < @ $2 . > $3
')dnl
R$* $: $&h $| $1
ifdef(`NO_SRS_FILE',`dnl
#
# If destination mailer is in non-SRS list, do not apply SRS
# This is intended for handling communication between secondary MX and
# primary MX
R$={noSrsMailers} $| $* $@ $2
')dnl
#R$* $| $* $: $2
R$* $: $(make_srs $1 $)
SReverseSrs
R$* $: $1 $>IsSrs $1
R$* NO $@ $1
R$* YES $@ $(reverse_srs $1 $)
LOCAL_RULE_0
R$* $: $>ReverseSrs $1
pysrs-1.0/pysrs.rc 0000775 0001606 0000145 00000002462 11534030341 013125 0 ustar stuart bms #!/bin/bash
#
# pysrs This shell script takes care of starting and stopping pysrs.
#
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: pysrs
# config: /etc/mail/pysrs.cfg
# pidfile: /var/run/milter/pysrs.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} pysrs.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
test -x /usr/lib/pymilter/pysrs.py&&test -x /usr/lib/pymilter/start.sh||exit 0
RETVAL=0
prog="pysrs"
start() {
# Start daemons.
echo -n "Starting $prog: "
daemon --check pysrs --user mail /usr/lib/pymilter/start.sh pysrs
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/pysrs
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc pysrs
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/pysrs
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/pysrs ]; then
stop
start
RETVAL=$?
fi
;;
status)
status pysrs
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
pysrs-1.0/pysrsprog.m4 0000664 0001606 0000145 00000004037 10400741761 013734 0 ustar stuart bms divert(-1)
#
# Copyright (c) 2004 Alain Knaff (derived from work by asarian-host.net)
# All rights reserved.
# Copyright (c) 1988, 1993
# The Regents of the University of California. All rights reserved.
#
# By using this file, you agree to the terms and conditions set
# forth in the LICENSE file which can be found at the top level of
# the sendmail distribution.
#
#
divert(0)
VERSIONID(`$Id: pysrsprog.m4,v 1.2 2006/02/28 03:30:57 customdesigned Exp $')
ifdef(`_MAILER_DEFINED_',,`errprint(`*** WARNING: MAILER() should be before HACK(pysrs)
')')
ifelse(defn(`_ARG_'),`',,`define(`SRS_DOMAIN',_ARG_)')
LOCAL_CONFIG
# Forward SRS program map
Kmake_srs program /usr/bin/envfrom2srs.py ifdef(`SRS_DOMAIN',SRS_DOMAIN)
# Reverse SRS program map
Kreverse_srs program /usr/bin/srs2envtol.py
# "To" address is SRS
ifdef(`SRS_DOMAIN',`dnl
Kis_srs regex CONCAT(`^?$')
',`dnl
Kis_srs regex ^MakeSrs $1 make SRS
LOCAL_RULESETS
SIsSrs
# Answers YES or NO whether the address in parameter is srs or not
R$* $: $( is_srs $1 $)
R$@ $@ YES
R$* $@ NO
SMakeSrs
#
# Prevent SRS encapsulation if "To" address is SRS
R$* $: $1 $>IsSrs $&u
R$* YES $@ $1
R$* NO $: $1
ifdef(`NO_SRS_FROM_LOCAL',`dnl
#
# Prevent SRS encapsulation if "From" address is local
# (With a local from address, the forwarder mail will pass any SPF checks
# anyways, so why bother with SRS?)
R$* < @ $=w > $* $@ $1 < @ $2 > $3
R$* < @ $=w . > $* $@ $1 < @ $2 . > $3
')dnl
ifdef(`NO_SRS_FILE',`dnl
#
# If destination mailer is in non-SRS list, do not apply SRS
# This is intended for handling communication between secondary MX and
# primary MX
R$* $: $&h. $| $1
R$={noSrsMailers} $| $* $@ $2
R$* $| $* $: $2
')dnl
R$* $: $(make_srs $1 $)
SReverseSrs
R$* $: $1 $>IsSrs $1
R$* NO $@ $1
R$* YES $@ $(reverse_srs $1 $)
LOCAL_RULE_0
R$* $: $>ReverseSrs $1
pysrs-1.0/SES/ 0000775 0001606 0000145 00000000000 11555576327 012070 5 ustar stuart bms pysrs-1.0/SES/ses.py 0000664 0001606 0000145 00000021161 11534330365 013220 0 ustar stuart bms #
# Class to sign and verify sender addresses with message ID.
#
# $Log: ses.py,v $
# Revision 1.4 2011/03/05 03:41:41 customdesigned
# 2.4 compatibility
#
# Revision 1.3 2011/03/03 23:52:21 customdesigned
# Release 1.0
#
# Revision 1.2 2010/03/17 22:05:34 customdesigned
# License updates. Python code is Python license, except srsmilter.py.
# M4 sendmail macros are sendmail license.
#
# Revision 1.1 2005/06/18 21:44:40 customdesigned
# Changes since 0.30.9. Begin SES support.
#
# Revision 1.8 2004/08/13 17:20:22 stuart
# Limit validations.
#
# Revision 1.7 2004/08/13 17:09:06 stuart
# support server id and fixed sigs
#
# Revision 1.6 2004/08/13 16:25:12 stuart
# bitpack function hopefully makes things clearer
#
# Revision 1.5 2004/08/04 21:58:11 stuart
# Tolerate case smashed tag.
#
# Revision 1.4 2004/08/04 15:43:25 stuart
# Drop Stuart's proposal. Implement Seth's correctly, but with
# message id in high order bits.
#
# Revision 1.3 2004/08/03 13:05:20 stuart
# Base 38. 1/2 day timecode and fixed length hash for Seth.
#
# Revision 1.2 2004/08/02 18:50:04 stuart
# Implement Seth's format as well.
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
#
import time
import hmac
try: from hashlib import sha1 as sha
except: import sha
import struct
DAY = 24*60*60 # size of day
# default encoding chars: base 38
BASE='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
def longbits(hash,n):
"Return leading n bits of hash digest converted to long."
hashbits = 0
h = 0L
for b in hash:
h = (h << 8) + ord(b)
hashbits += 8
if hashbits >= n:
return h >> (hashbits - n)
return h
def bitpack(flds,*data):
bits = 0L
for n,v in zip(flds,data):
bits = (bits << n) | v
return bits
def bitunpack(flds,bits):
a = []
f = list(flds[1:])
f.reverse()
for n in f:
mask = (1L << n) - 1
a.insert(0,bits & mask)
bits >>= n
a.insert(0,bits)
return a
# Seth's proposal is to divide message ids into a fixed length fractional day
# plus a sequentially assigned id encoded variable length with leading
# zero supression. This allows shorter ids for low volume sites without
# additional configuration. The entire bitstring consisting of
# HMAC,ts,msgid is encoded as a block.
#
# If turns out to be much simpler to put the variable length msgid at
# the high order end, so I depart from Seth on that point, and encode
# the bitstring msgid,ts,HMAC as a block. That way we don't have to
# worry about calculating the bit size of the encoded block based on
# the number of encoded chars.
class SES(object):
def __init__(self,secret,hashbits=80,expiration=10,fbits=2,chars=BASE,
nservers=1,server=0,maxval=3):
if type(secret) == str:
self.secret = (secret,)
else:
self.secret = secret
self.hashbits = hashbits
self.chars = chars
self.last_id = 0L
self.frac_day = DAY >> fbits
self.last_ts = int(time.time() / self.frac_day)
tsbits = 1
while (1 << tsbits) - 1 <= expiration:
tsbits += 1
tsbits += fbits # bits needed for timecode
self.expiration = expiration << fbits # expiration in frac days
self.flds = (0,tsbits,hashbits)
self.tcmask = (1 << tsbits) - 1
self.nservers = nservers
self.server = server
self.valtrack = {} # track validation attempts
self.maxval = maxval # maximum times a sig can be validated
def timecode_as_secs(self,tc):
"Return timecode converted to format compatible with time.time()."
return tc * self.frac_day
def get_timecode(self,s=None):
"Return timecode from time.time() compatible value or current time."
if s is None: s = time.time()
return int(s / self.frac_day)
def warn(self,*msg):
print >>sys.stderr,'WARNING:',' '.join(msg)
def set_secret(self,*args):
"""ses.set_secret(new,old,...)
Add a new secret to the rewriter. When an address is returned, all
secrets are tried to see if the hash can be validated. Don't use "foo",
"secret", "password", "10downing", "god" or "wednesday" as your secret."""
self.secret = args
def get_secret(self):
"Return the list of secrets. These are secret. Don't publish them."
return self.secret
def create_message_id(self):
"Assign timestamped message id. Return timecode,msgid"
# FIXME: synchronize for multithreading, make persistent
ts = self.get_timecode()
if ts == self.last_ts: # if still same fractional day
msgid = self.last_id + 1 # assign next sequential id
else:
msgid = 1L
self.last_ts,self.last_id = ts,msgid
return ts,msgid * self.nservers + self.server
def encode(self,bits):
"Convert sig bits to base n chars."
chars = self.chars
base = len(chars)
t = []
while bits > 0:
bits,c = divmod(bits,base)
t.append(chars[c])
t.reverse()
return ''.join(t)
def decode(self,s):
"Convert encoded chars to sig bits."
chars = self.chars
if chars == chars.upper():
s = s.upper()
base = len(chars)
m = 0
for c in s:
m = m * base + chars.index(c)
return m
def hash_create(self,*data):
"""ses.hash_create(data,...)
Returns a cryptographic hash of all data in data as a long with
self.hashbits bits. Any piece of data encoded into an address
which must remain inviolate should be hashed, so that when the
address is reversed, we can check that this data has not been
tampered with. You must provide at least one piece of data to
this method (otherwise this system is both cryptographically weak
and there may be collision problems with sender addresses)."""
secret = self.get_secret()
assert secret, "Cannot create a cryptographic MAC without a secret"
h = hmac.new(secret[0],'',sha)
for i in data:
h.update(i)
return longbits(h.digest(),self.hashbits)
def hash_verify(self,hash,*data):
"""ses.hash_verify(hash,data,...)
Verify that data has not been tampered with, given the cryptographic
hash previously output by srs->hash_create(). Returns True or False.
All known secrets are tried in order to see if the hash was created
with an old secret."""
secret = self.get_secret()
assert secret, "Cannot verify a cryptographic MAC without a secret"
hashes = []
for s in secret:
h = hmac.new(s,'',sha)
for i in data:
h.update(i)
if hash == longbits(h.digest(),self.hashbits):
return True
return False;
def sig_create(self,msgid,ts,h):
"""Return encoded signature.
msgid - long integer id unique for timecode
ts - 32 bit timecode: day fractions since epoch
h - long with high order self.hashbits bits of hash digest"""
if ts:
return self.encode(bitpack(self.flds,msgid,ts % self.tcmask,h))
else:
return self.encode(bitpack(self.flds,msgid,self.tcmask,h))
def sig_extract(self,sig,ts):
"""Return msgid,timecode,hash extracted from sig.
sig - encoded sig returned by sig_create
ts - the current timecode
"""
msgid,tc,hash = bitunpack(self.flds,self.decode(sig))
tcmask = self.tcmask
if tc != tcmask:
tc = ts // tcmask * tcmask + int(tc)
if tc > ts:
tc -= tcmask
else:
tc = 0
return msgid,tc,hash
def sign(self,address,msgid=None):
"""Return signed address.
if msgid is supplied, a fixed signature is generated."""
local,domain = address.split('@',1)
if msgid: # fixed sig
ts = 0
else:
ts,msgid = self.create_message_id()
h = self.hash_create(struct.pack('>QQ',ts,msgid),local,'@',domain.lower())
t = self.sig_create(msgid,ts,h)
return 'SES=%s=%s@%s' % (t,local,domain)
def verify(self,address):
"""Return unsigned_address,timecode,message_id. Return (address,)
unchanged if signature is invalid."""
if address.upper().startswith('SES='):
try:
local,domain = address.split('@',1)
tag,sig,user = local.split('=')
except ValueError:
raise ValueError("Invalid SES signature format: %s" % local)
ts = self.get_timecode()
msgid,tc,h = self.sig_extract(sig,ts)
if not tc or ts - tc < self.expiration and self.hash_verify(h,
struct.pack('>QQ',tc,msgid),user,'@',domain.lower()):
if tc: # count validations
self.valtrack[msgid] = cnt = self.valtrack.get(msgid,0) + 1
if cnt > self.maxval:
raise RuntimeError(
"Too many validations of signature: %s" % address)
return user + '@' + domain,tc,msgid
return address,
if __name__ == '__main__':
import sys
ses = SES('shhhh!')
for a in sys.argv[1:]:
if a.startswith('-m'):
ses.last_id = long(a[2:])
elif a.startswith('SES'):
print ses.verify(a)
else:
print ses.sign(a)
pysrs-1.0/SES/testses.py 0000664 0001606 0000145 00000006572 11350251056 014124 0 ustar stuart bms # Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
#
import unittest
import ses
import time
class SESTestCase(unittest.TestCase):
def setUp(self):
self.ses = ses.SES('shhh!')
def testEncode(self):
for bits in (0L,1L,1234L,123738493059846347859040389523479L):
s = self.ses.encode(bits)
self.assertEqual(bits,self.ses.decode(s))
def testTimecode(self):
tc = self.ses.get_timecode()
for tc in (0,1,50000,60000,tc):
secs = self.ses.timecode_as_secs(tc)
self.assertEqual(tc,self.ses.get_timecode(secs))
def testHash(self):
data = ('now','is','the','time')
h = self.ses.hash_create(*data)
self.failUnless(self.ses.hash_verify(h,*data))
self.failUnless(not self.ses.hash_verify(h,'some','other','data'))
def testMessageID(self):
while True:
tc,msgid = self.ses.create_message_id()
tc2,msgid2 = self.ses.create_message_id()
if tc2 == tc: break
# message ID increments when timecode is unchanged
self.assertEqual(msgid+1,msgid2)
# message ID resets when timecode changes
self.ses.last_ts -= 1
tc2,msgid2 = self.ses.create_message_id()
self.assertEqual(tc,tc2)
self.assertEqual(1,msgid2)
def testSigpack(self):
tc = 50000
h = self.ses.hash_create('some','data')
for msgid in (1L,100000L,12345657423784L):
sig = self.ses.sig_create(msgid,tc,h)
ts = tc + 30
msgid2,tc2,h2 = self.ses.sig_extract(sig,ts)
self.failUnless(tc2 <= ts)
self.assertEqual(msgid,msgid2)
self.assertEqual(tc,tc2)
self.assertEqual(h,h2)
ts = tc + 200
msgid2,tc2,h2 = self.ses.sig_extract(sig,ts)
self.failUnless(tc2 <= ts)
self.assertEqual(msgid,msgid2)
self.failIfEqual(tc,tc2)
self.assertEqual(h,h2)
def get_timecode(self):
"Provide a deterministic timecode for testing."
return self.timecode
def testSign(self):
self.ses.get_timecode = self.get_timecode
self.timecode = 60000
a = 'mickey@Mouse.com'
sig = self.ses.sign(a)
self.failUnless(sig.endswith(a))
self.failUnless(sig.startswith('SES='))
res = self.ses.verify(sig)
self.assertEqual(res,(a,self.timecode,self.ses.last_id))
res2 = self.ses.verify(sig.lower())
self.assertEqual(res2,(a.lower(),res[1],res[2]))
def testValtrack(self):
a = 'mickey@Mouse.com'
sig = self.ses.sign(a)
res = self.ses.verify(sig)
res = self.ses.verify(sig)
res = self.ses.verify(sig)
try:
res = self.ses.verify(sig)
self.fail("Failed to limit validations")
except:
pass
def testFixed(self):
self.ses.get_timecode = self.get_timecode
self.timecode = 60000
a = 'mickey@Mouse.com'
# sigs are normaly always unique
sig = self.ses.sign(a)
sig2 = self.ses.sign(a)
self.assertNotEqual(sig,sig2)
# but passing a msgid generates a fixed sig
msgid = 12345678L
sig = self.ses.sign(a,msgid)
sig2 = self.ses.sign(a,msgid)
self.assertEqual(sig,sig2)
# that is unchanging with time as well
self.timecode = 70000
sig = self.ses.sign(a,msgid)
self.assertEqual(sig,sig2)
a2,tc,msgid2 = self.ses.verify(sig)
self.assertEqual(a2,a)
self.assertEqual(tc,0)
self.assertEqual(msgid2,msgid)
if __name__ == '__main__':
unittest.main()
pysrs-1.0/SES/__init__.py 0000664 0001606 0000145 00000000524 11350251056 014160 0 ustar stuart bms #
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
__version__ = '0.30.10'
__all__= [
'new',
'__version__'
]
import ses
def new(secret=None,*args,**kw):
return ses.SES(secret,*args,**kw)
pysrs-1.0/setup.py 0000664 0001606 0000145 00000003452 11350251056 013132 0 ustar stuart bms #! /usr/bin/env python
#
# $Id: setup.py,v 1.6 2010/03/17 22:05:34 customdesigned Exp $
#
# Copyright (c) 2004-2010 Business Management Systems. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.
import sys,os
sys.path.insert(0,os.getcwd())
from distutils.core import setup
import SRS
setup(
#-- Package description
name = 'pysrs',
license = 'Python license',
version = '0.30.12',
description = 'Python SRS (Sender Rewriting Scheme) library',
long_description = """Python SRS (Sender Rewriting Scheme) library.
As SPF is implemented, MTAs that check SPF must account for any forwarders.
One way to handle forwarding is to have the forwarding MTA rewrite envfrom to a
domain they are authorized to use.
See http://www.openspf.org/SRS for details.
The Perl reference implementation and a C implementation are at
http://www.libsrs2.org/
""",
author = 'Stuart Gathman (Perl version by Shevek)',
author_email = 'stuart@bmsi.com',
url = 'http://bmsi.com/python/pysrs.html',
py_modules = ['SocketMap'],
packages = ['SRS','SES'],
scripts = ['envfrom2srs.py','srs2envtol.py'],
keywords = ['SPF','SRS','SES'],
classifiers = [
'Development Status :: 5 - Production/Stable',
'Environment :: No Input/Output (Daemon)',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Python License (CNRI Python License)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Communications :: Email',
'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)
pysrs-1.0/MANIFEST.in 0000664 0001606 0000145 00000000522 11350307072 013151 0 ustar stuart bms include testSRS.py
include testd.py
include pysrs.html
include envfrom2srs.py
include srs2envtol.py
include pysrs.py
include SocketMap.py
include setup.py
include setup.cfg
include pysrs.cfg
include pysrs.rc
include pysrs.rc7
include *.m4
include pysrs.spec
include CHANGES
include COPYING
include LICENSE.python
include LICENSE.sendmail
pysrs-1.0/pysrs.spec 0000664 0001606 0000145 00000010405 11534327520 013454 0 ustar stuart bms %define sysvinit pysrs.rc
%define __python python2.6
%define pythonbase python26
Summary: Python SRS (Sender Rewriting Scheme) library
Name: %{pythonbase}-pysrs
Version: 1.0
Release: 1
Source0: pysrs-%{version}.tar.gz
#Patch0: %{name}-%{version}.patch
License: Python license
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Vendor: Stuart Gathman (Perl version by Shevek)
Packager: Stuart D. Gathman
Requires: chkconfig, %{pythonbase}
Url: http://bmsi.com/python/pysrs.html
%description
Python SRS (Sender Rewriting Scheme) library.
As SPF is implemented, mail forwarders must rewrite envfrom for domains
they are not authorized to send from.
See http://www.openspf.org/SRS for details.
The Perl reference implementation is at http://srs-socketmap.info/
SRS is also useful for detecting forged DSNs (bounces). SES (Signed
Envelope Sender) is a variation that is more compact for this purpose,
and in conjuction with some kind of replay protection can also be
used as a form of authentication.
%prep
%setup -n pysrs-%{version}
#%patch -p1
%build
%{__python} setup.py build
%install
%{__python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/etc/mail
cp pysrs.cfg $RPM_BUILD_ROOT/etc/mail
cat >$RPM_BUILD_ROOT/etc/mail/no-srs-mailers <<'EOF'
# no-srs-mailers - list hosts (RHS) we should not SRS encode for when we
# send to them. E.g. primary MX servers for which we are a secondary.
# NOTE - mailertable can change the RHS for delivery purposes, you
# must match the mailertable RHS in that case.
#
EOF
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
cp pysrs.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
cp pysrsprog.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
# We use same log dir as milter since we also are a sendmail add-on
mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT/var/run/milter
mkdir -p $RPM_BUILD_ROOT/usr/lib/pymilter
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/pysrs
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/pysrs <<'EOF'
/^python=/
c
python="%{__python}"
.
w
q
EOF
cp -p pysrs.py $RPM_BUILD_ROOT/usr/lib/pymilter
# logfile rotation
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
cat >$RPM_BUILD_ROOT/etc/logrotate.d/pysrs <<'EOF'
/var/log/milter/pysrs.log {
copytruncate
compress
}
EOF
%clean
rm -rf $RPM_BUILD_ROOT
%post
#echo "Syntax of HACK(pysrs) has changed. Update sendmail.mc."
/sbin/chkconfig --add pysrs
%preun
if [ $1 = 0 ]; then
/sbin/chkconfig --del pysrs
fi
%files -f INSTALLED_FILES
%doc COPYING LICENSE.python LICENSE.sendmail CHANGES
%defattr(-,root,root)
%config(noreplace) /etc/mail/pysrs.cfg
%config(noreplace) /etc/mail/no-srs-mailers
/etc/logrotate.d/pysrs
/etc/rc.d/init.d/pysrs
/usr/share/sendmail-cf/hack/*
/usr/lib/pymilter/pysrs.py
%changelog
* Wed May 20 2009 Stuart Gathman 1.0-1
- Foundation for python milter envfrom rewriting (in progress)
- Python 2.6
- Depend on pymilter for start.sh and dirs, even though we don't
really need it for anything else until envfrom rewriting is done.
* Tue Jan 16 2007 Stuart Gathman 0.30.12-1
- Support logging recipient host, and nosrs in pysrs.cfg
* Wed Feb 15 2006 Stuart Gathman 0.30.11-1
- support SRS signing mode
* Tue Jul 05 2005 Stuart Gathman 0.30.10-1
- support SES
* Sun Sep 19 2004 Stuart Gathman 0.30.9-2
- chkconfig --add pysrs
* Thu Aug 26 2004 Stuart Gathman 0.30.9-1
- Sendmail Socketmap Daemon
* Wed Mar 24 2004 Stuart Gathman 0.30.8-1
- Use HMAC instead of straight sha
* Wed Mar 24 2004 Stuart Gathman 0.30.7-1
- Pass SRS_DOMAIN to envfrom2srs.py
* Wed Mar 24 2004 Stuart Gathman 0.30.6-4
- Put SRS rewriting rule at end of EnvFromSMTP in pysrs.m4
* Tue Mar 23 2004 Stuart Gathman 0.30.6-3
- Fix regex for is_srs macro in pysrs.m4
* Tue Mar 23 2004 Stuart Gathman 0.30.6-2
- set alwaysrewrite=True in envfrom2srs.py since pysrs.m4 skips local domains
- Incorporate m4 macro from Alain Knaff for cleaner sendmail support
* Mon Mar 22 2004 Stuart Gathman 0.30.5-1
- Make sendmail map use config in /etc/mail/pysrs.cfg