Compare commits

..

7 Commits

Author SHA1 Message Date
Stuart D. Gathman 5675adeb3c Work with berkeleydb and try importing it first. 2024-05-29 18:08:30 -04:00
Stuart D. Gathman 35416dfc46 Support MTAs with colon separator 2024-05-29 11:15:26 -04:00
Stuart D. Gathman c33de064ee Merge branch 'master' of github.com:sdgathman/pymilter 2024-05-29 11:14:19 -04:00
Jaime Marquínez Ferrándiz 1c05080768 Remove calls to the deprecated method "assertEquals" (#57)
It has been removed in python 3.12
2024-03-11 22:27:18 -04:00
Stuart D. Gathman dce7c0080a Adapt to MTAs that use ':' as key terminator and/or add null char to
key/value.
2022-07-15 19:00:41 -04:00
Stuart D. Gathman 7deec90a59 Drop paragraph about python 2.0 compatibility 2021-12-31 00:56:54 -05:00
Stuart D. Gathman c73b533acb Update README.md to satisfy PiPy 2021-12-31 00:49:54 -05:00
7 changed files with 68 additions and 44 deletions
+25 -6
View File
@@ -1,11 +1,16 @@
try: try:
from bsddb3 import db try:
from berkeleydb import db
except:
from bsddb3 import db
class DB(object): class DB(object):
def open(self,fname,mode): def open(self,fname,mode):
if mode == 'r': flags = db.DB_RDONLY if mode == 'r': flags = db.DB_RDONLY
else: raise RuntimeException('unsupported mode') else: raise RuntimeException('unsupported mode')
self.f = db.DB() self.f = db.DB()
self.f.open(fname,flags=flags) self.f.open(fname,flags=flags)
def __contains__(self,key):
return not not self.f.get(key)
def __getitem__(self,key): def __getitem__(self,key):
v = self.f.get(key) v = self.f.get(key)
if not v: raise KeyError(key) if not v: raise KeyError(key)
@@ -27,6 +32,10 @@ class MTAPolicy(object):
if not access_file: if not access_file:
access_file = conf.access_file access_file = conf.access_file
self.use_nulls = conf.access_file_nulls self.use_nulls = conf.access_file_nulls
try:
self.use_colon = conf.access_file_colon
except:
self.use_colon = True
self.sender = sender self.sender = sender
self.domain = sender.split('@')[-1].lower() self.domain = sender.split('@')[-1].lower()
self.acf = None self.acf = None
@@ -52,14 +61,24 @@ class MTAPolicy(object):
if not acf: return None if not acf: return None
if self.use_nulls: sfx = b'\x00' if self.use_nulls: sfx = b'\x00'
else: sfx = b'' else: sfx = b''
pfx = pfx.encode() + b'!' if self.use_colon:
try: sep = b':'
else:
sep = b'!'
pfx = pfx.encode() + sep
try: # try with localpart@domain
return acf[pfx + self.sender.encode() + sfx].rstrip(b'\x00').decode() return acf[pfx + self.sender.encode() + sfx].rstrip(b'\x00').decode()
except KeyError: except KeyError:
try: try: # try with domain
return acf[pfx + self.domain.encode() + sfx].rstrip(b'\x00').decode() d = self.domain.encode()
k = pfx + d + sfx
while not k in acf and b'.' in d:
# check partial domains
d = b'.'.join(d.split(b'.')[1:])
k = pfx + b'.' + d + sfx
return acf[k].rstrip(b'\x00').decode()
except KeyError: except KeyError:
try: try: # try bare prefix
return acf[pfx + sfx].rstrip(b'\x00').decode() return acf[pfx + sfx].rstrip(b'\x00').decode()
except KeyError: except KeyError:
try: try:
+31 -34
View File
@@ -11,9 +11,11 @@ sending DSNs or doing CBVs.
# Requirements # Requirements
Python milter extension: https://pypi.python.org/pypi/pymilter/ Python milter extension: https://pypi.org/project/pymilter/
Python: http://www.python.org Python: http://www.python.org
Sendmail: http://www.sendmail.org Sendmail: http://www.sendmail.org
or
Postfix: http://www.postfix.org/MILTER_README.html
# Quick Installation # Quick Installation
@@ -21,10 +23,10 @@ Sendmail: http://www.sendmail.org
2. Build and install Python, enabling threading. 2. Build and install Python, enabling threading.
3. Install this module: python setup.py --help 3. Install this module: python setup.py --help
4. Add these two lines to sendmail.cf[a]: 4. Add these two lines to sendmail.cf[a]:
```
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock Xpythonfilter, S=local:/home/username/pythonsock
```
5. Run the sample.py example milter with: python sample.py 5. Run the sample.py example milter with: python sample.py
Note that milters should almost certainly not run as root. Note that milters should almost certainly not run as root.
@@ -36,9 +38,9 @@ milter used in production.
[a] This is for a quick test. Your sendmail.cf in most distros will get [a] This is for a quick test. Your sendmail.cf in most distros will get
overwritten whenever sendmail.mc is updated. To make a milter permanent, overwritten whenever sendmail.mc is updated. To make a milter permanent,
add something like: add something like:
```
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
```
to sendmail.mc instead. to sendmail.mc instead.
# Not-so-quick Installation # Not-so-quick Installation
@@ -54,18 +56,13 @@ Install this miltermodule package; DistUtils Automatic Installation:
$ python setup.py --help $ python setup.py --help
For versions of python prior to 2.0, you will need to download distutils
separately or build manually. You will need to download unittest
separately to run the test programs. The bdist_rpm distutils option seems
not to work for python 2.0; upgrade to at least 2.1.1.
Now that everything is installed, we need to tell sendmail that we're going Now that everything is installed, we need to tell sendmail that we're going
to filter incoming email. Add lines similar to the following to to filter incoming email. Add lines similar to the following to
sendmail.cf: sendmail.cf:
```
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock Xpythonfilter, S=local:/home/username/pythonsock
```
The "O" line tells sendmail which filters to use in what order; here we're The "O" line tells sendmail which filters to use in what order; here we're
telling sendmail to use the filter named "pythonfilter". telling sendmail to use the filter named "pythonfilter".
@@ -79,14 +76,14 @@ NB: The name is specified in two places: here, in sendmail's cf file, and
in the milter itself. Make sure the two match. in the milter itself. Make sure the two match.
NB: The above lines can be added in your .mc file with this line: NB: The above lines can be added in your .mc file with this line:
```
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
```
For versions of sendmail prior to 8.12, you will need to enable For versions of sendmail prior to 8.12, you will need to enable
_FFR_MILTER for the cf macros. For example, `_FFR_MILTER` for the cf macros. For example,
```
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
```
# IPv6 Notes # IPv6 Notes
The IPv6 protocol is supported if your operation system supports it The IPv6 protocol is supported if your operation system supports it
@@ -94,9 +91,9 @@ and if sendmail was compiled with IPv6 support. To determine if your
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6 sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
compilation option. To compile sendmail with IPv6 support, add this compilation option. To compile sendmail with IPv6 support, add this
declaration to your site.config.m4 before building it: declaration to your site.config.m4 before building it:
```
APPENDDEF(`confENVDEF', `-DNETINET6=1') APPENDDEF(`confENVDEF', `-DNETINET6=1')
```
IPv6 support can show up in two places; the communications socket IPv6 support can show up in two places; the communications socket
between the milter and sendmail processes and in the host address between the milter and sendmail processes and in the host address
argument to the connect() callback method. argument to the connect() callback method.
@@ -107,34 +104,34 @@ want to allow both IPv4 and IPv6 connections, some operating systems
will require that each listens to different port numbers. For an will require that each listens to different port numbers. For an
IPv6-only setup, your sendmail configuration should contain a line IPv6-only setup, your sendmail configuration should contain a line
similar to (first line is for sendmail.mc, second is sendmail.cf): similar to (first line is for sendmail.mc, second is sendmail.cf):
```
DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25') DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25')
O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25 O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25
```
To allow sendmail and the milter process to communicate with each To allow sendmail and the milter process to communicate with each
other over IPv6, you may use the "inet6" socket name prefix, as in: other over IPv6, you may use the "inet6" socket name prefix, as in:
```
Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c
```
The connect() callback method in the milter class will pass the The connect() callback method in the milter class will pass the
IPv6-specific information in the 'hostaddr' argument as a tuple. Note IPv6-specific information in the 'hostaddr' argument as a tuple. Note
that the type of this value is dependent upon the protocol family, and that the type of this value is dependent upon the protocol family, and
is not compatible with IPv4 connections. Therefore you should always is not compatible with IPv4 connections. Therefore you should always
check the family argument before attempting to use the hostaddr check the family argument before attempting to use the hostaddr
argument. A quick example showing this follows: argument. A quick example showing this follows:
```
import socket import socket
...
class ipv6awareMilter(Milter.Milter):
...
def connect(self,hostname,family,hostaddr):
if family==socket.AF_INET:
ipaddress, port = hostaddr
elif family==socket.AF_INET6:
ip6address, port, flowinfo, scopeid = hostaddr
elif family==socket.AF_UNIX:
socketpath = hostaddr
class ipv6awareMilter(Milter.Milter):
def connect(self,hostname,family,hostaddr):
if family==socket.AF_INET:
ipaddress, port = hostaddr
elif family==socket.AF_INET6:
ip6address, port, flowinfo, scopeid = hostaddr
elif family==socket.AF_UNIX:
socketpath = hostaddr
```
The hostname argument is always safe to use without interpreting the The hostname argument is always safe to use without interpreting the
protocol family. For IPv6 connections for which the hostname can not protocol family. For IPv6 connections for which the hostname can not
be determined the hostname will appear similar to the string be determined the hostname will appear similar to the string
+1
View File
@@ -20,6 +20,7 @@ modules = ["mime"]
setup(name = "pymilter", version = '1.0.5', setup(name = "pymilter", version = '1.0.5',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown',
author="Jim Niemira", author="Jim Niemira",
author_email="urmane@urmane.org", author_email="urmane@urmane.org",
maintainer="Stuart D. Gathman", maintainer="Stuart D. Gathman",
+1
View File
@@ -7,3 +7,4 @@ SMTP-Auth:good@example.com OK
SMTP-Auth:example.com REJECT SMTP-Auth:example.com REJECT
SMTP-Auth:bad@localhost.localdomain REJECT SMTP-Auth:bad@localhost.localdomain REJECT
SMTP-Test: REJECT SMTP-Test: REJECT
SMTP-Test:.baz.com WILDCARD
+6
View File
@@ -8,6 +8,7 @@ class Config(object):
def __init__(self): def __init__(self):
self.access_file='test/access.db' self.access_file='test/access.db'
self.access_file_nulls=True self.access_file_nulls=True
self.access_file_colon = False
class PolicyTestCase(unittest.TestCase): class PolicyTestCase(unittest.TestCase):
@@ -23,6 +24,8 @@ class PolicyTestCase(unittest.TestCase):
print("Missing test/access") print("Missing test/access")
def testPolicy(self): def testPolicy(self):
self.config.access_file_colon = False
self.config.access_file_nulls = True
with MTAPolicy('good@example.com',conf=self.config) as p: with MTAPolicy('good@example.com',conf=self.config) as p:
pol = p.getPolicy('smtp-auth') pol = p.getPolicy('smtp-auth')
self.assertEqual(pol,'OK') self.assertEqual(pol,'OK')
@@ -35,6 +38,9 @@ class PolicyTestCase(unittest.TestCase):
with MTAPolicy('any@random.com',conf=self.config) as p: with MTAPolicy('any@random.com',conf=self.config) as p:
pol = p.getPolicy('smtp-test') pol = p.getPolicy('smtp-test')
self.assertEqual(pol,'REJECT') self.assertEqual(pol,'REJECT')
with MTAPolicy('foo@bar.baz.com',conf=self.config) as p:
pol = p.getPolicy('smtp-test')
self.assertEqual(pol,'WILDCARD')
def suite(): return unittest.makeSuite(PolicyTestCase,'test') def suite(): return unittest.makeSuite(PolicyTestCase,'test')
+3 -3
View File
@@ -31,7 +31,7 @@ class BMSMilterTestCase(unittest.TestCase):
count = 10 count = 10
while count > 0: while count > 0:
rc = ctx._connect(helo='milter-template.example.org') rc = ctx._connect(helo='milter-template.example.org')
self.assertEquals(rc,Milter.CONTINUE) self.assertEqual(rc,Milter.CONTINUE)
with open('test/'+fname,'rb') as fp: with open('test/'+fname,'rb') as fp:
rc = ctx._feedFile(fp) rc = ctx._feedFile(fp)
milter = ctx.getpriv() milter = ctx.getpriv()
@@ -46,7 +46,7 @@ class BMSMilterTestCase(unittest.TestCase):
ctx._setsymval('{auth_type}','batcomputer') ctx._setsymval('{auth_type}','batcomputer')
ctx._setsymval('j','mailhost') ctx._setsymval('j','mailhost')
rc = ctx._connect() rc = ctx._connect()
self.assertEquals(rc,Milter.CONTINUE) self.assertEqual(rc,Milter.CONTINUE)
with open('test/'+fname,'rb') as fp: with open('test/'+fname,'rb') as fp:
rc = ctx._feedFile(fp) rc = ctx._feedFile(fp)
milter = ctx.getpriv() milter = ctx.getpriv()
@@ -69,7 +69,7 @@ class BMSMilterTestCase(unittest.TestCase):
milter = ctx.getpriv() milter = ctx.getpriv()
# self.assertTrue(milter.user == 'batman',"getsymval failed: "+ # self.assertTrue(milter.user == 'batman',"getsymval failed: "+
# "%s != %s"%(milter.user,'batman')) # "%s != %s"%(milter.user,'batman'))
self.assertEquals(milter.user,'batman') self.assertEqual(milter.user,'batman')
self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed") self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
self.assertTrue(rc == Milter.ACCEPT) self.assertTrue(rc == Milter.ACCEPT)
self.assertTrue(ctx._bodyreplaced,"Message body not replaced") self.assertTrue(ctx._bodyreplaced,"Message body not replaced")
+2 -2
View File
@@ -24,11 +24,11 @@ class AddrCacheTestCase(unittest.TestCase):
self.assertTrue(cache.has_key('foo@bar.com')) self.assertTrue(cache.has_key('foo@bar.com'))
self.assertTrue(not cache.has_key('hello@bar.com')) self.assertTrue(not cache.has_key('hello@bar.com'))
self.assertTrue('baz@bar.com' in cache) self.assertTrue('baz@bar.com' in cache)
self.assertEquals(cache['temp@bar.com'],'testing') self.assertEqual(cache['temp@bar.com'],'testing')
s = open(self.fname).readlines() s = open(self.fname).readlines()
self.assertTrue(len(s) == 2) self.assertTrue(len(s) == 2)
self.assertTrue(s[0].startswith('foo@bar.com ')) self.assertTrue(s[0].startswith('foo@bar.com '))
self.assertEquals(s[1].strip(),'baz@bar.com') self.assertEqual(s[1].strip(),'baz@bar.com')
# check that new result overrides old # check that new result overrides old
cache['temp@bar.com'] = None cache['temp@bar.com'] = None
self.assertTrue(not cache['temp@bar.com']) self.assertTrue(not cache['temp@bar.com'])