Skip to content

Commit

Permalink
Add service and test cases for srsmilter, fix some bugs, update sampl…
Browse files Browse the repository at this point in the history
…e config.
  • Loading branch information
sdgathman committed Nov 14, 2017
1 parent 880b72a commit ba626ea
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 36 deletions.
7 changes: 5 additions & 2 deletions SRS/DB.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Python itself.

import bsddb3
try:
import bsddb3 as bsddb
except:
import bsddb
import time
import SRS
from .Base import Base
Expand Down Expand Up @@ -53,7 +56,7 @@ class DB(Base):
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 = bsddb3.btopen(database,'c')
self.dbm = bsddb.btopen(database,'c')

def compile(self,sendhost,senduser,srshost=None):
ts = time.time()
Expand Down
2 changes: 1 addition & 1 deletion envfrom2srs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import SRS
import re
from configparser import ConfigParser, DuplicateSectionError
from ConfigParser import ConfigParser, DuplicateSectionError

# get SRS parameters from milter configuration
cp = ConfigParser({
Expand Down
11 changes: 11 additions & 0 deletions pysrs.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@
;srs = otherdomain.com
# do not rewrite mail to these domains
;nosrs = braindeadmail.com
#
[srsmilter]
;datadir=/var/lib/milter
socketname = /var/run/milter/srsmilter
miltername = pysrsfilter
# reject DSNs to unsigned recipients (bounce spam)
reject_spoofed = true
;trusted_relay = 1.2.3.4
internal_connect = 192.168.*.*,127.0.0.1,::1
# Enable outgoing SRS via CHGFROM (see code for limitations)
miltersrs = false
7 changes: 6 additions & 1 deletion pysrs.spec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Summary: Python SRS (Sender Rewriting Scheme) library
Name: %{pythonbase}-pysrs
Version: 1.0.2
Version: 1.0.3
Release: 1%{?dist}
Source0: pysrs-%{version}.tar.gz
License: Python license
Expand Down Expand Up @@ -73,9 +73,11 @@ cp pysrsprog.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT%{_libexecdir}/milter
cp -p pysrs.py $RPM_BUILD_ROOT%{_libexecdir}/milter/pysrs
cp -p srsmilter.py $RPM_BUILD_ROOT%{_libexecdir}/milter/srsmilter
%if %{use_systemd}
mkdir -p $RPM_BUILD_ROOT%{_unitdir}
cp -p pysrs.service $RPM_BUILD_ROOT%{_unitdir}
cp -p srsmilter.service $RPM_BUILD_ROOT%{_unitdir}
%else
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/pysrs
Expand Down Expand Up @@ -140,6 +142,9 @@ fi
%endif

%changelog
* Mon Nov 13 2017 Stuart Gathman <[email protected]> 1.0.3-1
- Include srsmilter

* Tue Nov 3 2017 Stuart Gathman <[email protected]> 1.0.2-1
- Fix daemon to run in python2
- Move daemons to /usr/libexec/milter so they get bin_t selinux label
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#-- Package description
name = 'pysrs',
license = 'Python license',
version = '1.0.2',
version = '1.0.3',
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.
Expand Down
2 changes: 1 addition & 1 deletion srs2envtol.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import SRS
import re
from configparser import ConfigParser, DuplicateSectionError
from ConfigParser import ConfigParser, DuplicateSectionError

# get SRS parameters from milter configuration
cp = ConfigParser({
Expand Down
77 changes: 49 additions & 28 deletions srsmilter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/python2
#
# A simple SRS milter for Sendmail-8.14/Postfix-?

#
# INCOMPLETE!!
# NOTE: use with pysrs socketmap and sendmail-cf macro to handle
# multiple recipients.
#
# The logical problem is that a milter gets to change MFROM only once for
# multiple recipients. When there is a conflict between recipients, we
Expand All @@ -12,17 +14,18 @@

# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html

# Author: Stuart D. Gathman <stuart@bmsi.com>
# Author: Stuart D. Gathman <stuart@gathman.org>
# Copyright 2007 Business Management Systems, Inc.
# Copyright 2017 Stuart D. Gathman
# 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
import re
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr

Expand All @@ -33,27 +36,29 @@ class Config(object):
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')
if cp.has_option('srsmilter','datadir'):
os.chdir(cp.get('srsmilter','datadir')) # FIXME: side effect!
conf.socketname = cp.getdefault('srsmilter','socketname',
'/var/run/milter/srsmilter')
conf.miltername = cp.getdefault('srsmilter','name','pysrsfilter')
conf.trusted_relay = cp.getlist('srsmilter','trusted_relay')
conf.miltersrs = cp.getboolean('srsmilter','miltersrs')
conf.internal_connect = cp.getlist('srsmilter','internal_connect')
conf.srs_reject_spoofed = cp.getboolean('srsmilter','reject_spoofed')
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:
if conf.database:
global SRS
import SRS.DB
conf.srs = SRS.DB.DB(database=conf.database,secret=conf.secret,
maxage=conf.maxage,hashlength=conf.hashlength,separator=conf.separator)
Expand Down Expand Up @@ -167,12 +172,12 @@ def del_recipient(self,rcpt):
self.discard_list.append(rcpt)

## Accumulate added recipients to be applied in eom callback.
def add_recipient(self,rcpt):
def add_recipient(self,rcpt,params):
rcpt = rcpt.lower()
if not rcpt in self.redirect_list:
self.redirect_list.append(rcpt)
if not rcpt in (r[0] for r in self.redirect_list):
self.redirect_list.append((rcpt,params))

def envrcpt(self,to,*str):
def envrcpt(self,to,*params):
conf = self.conf
t = parse_addr(to)
if len(t) == 2:
Expand All @@ -193,10 +198,11 @@ def envrcpt(self,to,*str):
newaddr = srs.reverse(oldaddr)
self.log("srs rcpt:",newaddr)
self.del_recipient(to)
self.add_recipient('<%s>',newaddr)
self.add_recipient('<%s>',newaddr,params)
except:
# no valid SRS signature
if not (self.internal_connection or self.trusted_relay):
# reject specific recipients with bad sig
if self.srsre.match(oldaddr):
self.log("REJECT: srs spoofed:",oldaddr)
self.setreply('550','5.7.1','Invalid SRS signature')
Expand All @@ -205,30 +211,45 @@ def envrcpt(self,to,*str):
self.log("REJECT: ses spoofed:",oldaddr)
self.setreply('550','5.7.1','Invalid SES signature')
return Milter.REJECT
# reject message for any missing sig
self.data_allowed = not conf.srs_reject_spoofed
else:
# sign "outgoing" from
if domain in nosrsdomain:
if domain in self.conf.nosrsdomain:
self.nosrsrcpt.append(to)
else:
self.srsrcpt.append(to)
else: # no SRS for unqualified recipients
self.nosrsrcpt.append(to)
return Milter.CONTINUE

def eoh(self):
if not self.data_allowed:
return Milter.REJECT
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
# apply recipient changes
for to in self.discard_list:
self.delrcpt(to)
for to,p in self.redirect_list:
self.addrcpt(to,p)
# optionally, do outgoing SRS for all recipients
if self.conf.miltersrs and self.srsrcpt:
newaddr = self.make_srs(self.canon_from)
if newaddr != self.canon_from:
self.chgfrom(newaddr)
return Milter.CONTINUE

if __name__ == "__main__":
global config
config = Config(['srsmilter.cfg','/etc/mail/srsmilter.cfg'])
Milter.factory = srsMilter
if config.miltersrs:
flags = Milter.CHGFROM + Milter.DELRCPT
else:
flags = Milter.DELRCPT
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:
Expand All @@ -239,5 +260,5 @@ def eom(self):
See the sendmail README for libmilter.
sample srsmilter startup""" % (miltername,miltername,socketname))
sys.stdout.flush()
Milter.runmilter("pysrsfilter",socketname,240)
print("sample srsmilter shutdown")
Milter.runmilter(miltername,socketname,240)
print("srsmilter shutdown")
15 changes: 15 additions & 0 deletions srsmilter.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=Python SRS milter
Wants=network.target
After=network-online.target sendmail.service

[Service]
Type=simple
WorkingDirectory=/var/log/milter
User=mail
Group=mail
SyslogIdentifier=srsmilter
ExecStart=/usr/libexec/milter/srsmilter

[Install]
WantedBy=multi-user.target
68 changes: 66 additions & 2 deletions testSRS.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,80 @@
# Translated to Python by [email protected]
# http://bmsi.com/python/milter.html
#
# Copyright (c) 2017 Stuart Gathman All rights reserved.
# Portions Copyright (c) 2004 Shevek. All rights reserved.
# Portions Copyright (c) 2004 Business Management Systems. All rights reserved.
# Portions Copyright (c) 2004,2006 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 Milter
from Milter.test import TestBase
from SRS.Guarded import Guarded
from SRS.DB import DB
from SRS.Reversible import Reversible
from SRS.Daemon import Daemon
import srsmilter
import SRS
import threading
import socket
try:
from StringIO import StringIO
except:
from io import StringIO

class TestMilter(TestBase,srsmilter.srsMilter):
def __init__(self):
TestBase.__init__(self)
srsmilter.config = srsmilter.Config(['pysrs.cfg'])
srsmilter.srsMilter.__init__(self)
self.setsymval('j','test.milter.org')

class SRSMilterTestCase(unittest.TestCase):

msg = '''From: [email protected]
Subject: test
test
'''

## Test rejecting bounce spam
def testReject(self):
milter = TestMilter()
milter.conf.srs_domain = set(['example.com'])
milter.conf.srs_reject_spoofed = False
fp = StringIO(self.msg)
rc = milter.connect('testReject',ip='192.0.3.1')
self.assertEqual(rc,Milter.CONTINUE)
rc = milter.feedFile(fp,sender='',rcpt='[email protected]')
self.assertEqual(rc,Milter.CONTINUE)
milter.conf.srs_reject_spoofed = True
fp.seek(0)
rc = milter.feedFile(fp,sender='',rcpt='[email protected]')
self.assertEqual(rc,Milter.REJECT)
milter.close()

## Test SRS coding of MAIL FROM
def testSign(self):
milter = TestMilter()
milter.conf.signdomain = set(['example.com'])
milter.conf.miltersrs = True
fp = StringIO(self.msg)
rc = milter.connect('testSign',ip='192.0.3.1')
self.assertEqual(rc,Milter.CONTINUE)
fp.seek(0)
rc = milter.feedFile(fp,sender='[email protected]',rcpt='[email protected]')
self.assertEqual(rc,Milter.CONTINUE)
s = milter.conf.srs.reverse(milter._sender[1:-1])
self.assertEqual(s,'[email protected]')
# check that it doesn't happen when disabled
milter.conf.miltersrs = False
fp.seek(0)
rc = milter.feedFile(fp,sender='[email protected]',rcpt='[email protected]')
self.assertEqual(rc,Milter.CONTINUE)
self.assertEqual(milter._sender,'<[email protected]>')
milter.close()

class SRSTestCase(unittest.TestCase):

Expand Down Expand Up @@ -191,7 +251,11 @@ def testProgMap(self):
self.assertEqual(addr2,orig)
self.assertTrue(self.case_smashed)

def suite(): return unittest.makeSuite(SRSTestCase,'test')
def suite():
s = unittest.makeSuite(SRSTestCase,'test')
s.addTest(makeSuite(SRSMilterTestCase,'test'))
#s.addTest(doctest.DocTestSuite(bms))
return s

if __name__ == '__main__':
unittest.main()

0 comments on commit ba626ea

Please sign in to comment.