Compare commits

..

14 Commits

Author SHA1 Message Date
Sandro 20751ea706 Set C standard to C17 explicitely (#70)
GCC 15 uses C23 by default. But `libmilter` is not compatible, yet.
This breaks the build as `bool` is a keyword in C23 (issue #68).
2025-03-12 20:05:10 -04:00
dotlambda 7197b82ed6 thread module has been renamed to _thread in Python 3 (#64) 2025-03-12 20:00:12 -04:00
Stuart D. Gathman 39a1fc78d8 Merge branch 'master' of github.com:sdgathman/pymilter 2024-10-15 19:43:54 -04:00
Stuart D. Gathman 5ad23e468d bsddb changed nulls in access file policy 2024-10-15 19:42:05 -04:00
Sandro 6eedaf7717 Python 3.13: Replace deprecated makeSuite() (#65)
The function has been deprecated in Python 3.11 and is no longer
available in Python 3.13.
2024-10-14 14:36:04 -04:00
Jean-Yves 4a8018c2de Welcome __NetBSD__ to the required header include. (#60)
Same rule applies for NetBSD as FreeBSD, <arpa/inet.h> include is
needed to provide inet_nto*() prototypes.
2024-06-05 08:18:15 -04:00
Stuart D. Gathman 1212a0ef59 Forgot to bump internal tags 2024-05-29 18:45:09 -04:00
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
15 changed files with 92 additions and 59 deletions
+3
View File
@@ -1,8 +1,11 @@
*.pyc *.pyc
*.tar.gz
build/ build/
test/*.out test/*.out
test/*.tstout test/*.tstout
test/*.log test/*.log
test/*.db
test.db test.db
dist dist
log
MANIFEST MANIFEST
+1 -1
View File
@@ -9,7 +9,7 @@
# This code is under the GNU General Public License. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
from __future__ import print_function from __future__ import print_function
__version__ = '1.0.5' __version__ = '1.0.6'
import os import os
import re import re
+3
View File
@@ -1,7 +1,10 @@
from __future__ import print_function from __future__ import print_function
import time import time
import shelve import shelve
try:
import thread import thread
except:
import _thread as thread
import logging import logging
import urllib import urllib
+24 -5
View File
@@ -1,4 +1,7 @@
try: try:
try:
from berkeleydb import db
except:
from bsddb3 import db from bsddb3 import db
class DB(object): class DB(object):
def open(self,fname,mode): def open(self,fname,mode):
@@ -6,6 +9,8 @@ try:
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:
+24 -27
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,26 +104,26 @@ 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): class ipv6awareMilter(Milter.Milter):
...
def connect(self,hostname,family,hostaddr): def connect(self,hostname,family,hostaddr):
if family==socket.AF_INET: if family==socket.AF_INET:
ipaddress, port = hostaddr ipaddress, port = hostaddr
@@ -134,7 +131,7 @@ argument. A quick example showing this follows:
ip6address, port, flowinfo, scopeid = hostaddr ip6address, port, flowinfo, scopeid = hostaddr
elif family==socket.AF_UNIX: elif family==socket.AF_UNIX:
socketpath = hostaddr 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 -1
View File
@@ -4,7 +4,7 @@ web:
rsync -ravKk doc/html/ pymilter.org:/var/www/html/milter/pymilter rsync -ravKk doc/html/ pymilter.org:/var/www/html/milter/pymilter
cd doc/html; zip -r ../../doc . cd doc/html; zip -r ../../doc .
VERSION=1.0.5 VERSION=1.0.6
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
+1 -1
View File
@@ -72,7 +72,7 @@ $ python setup.py help
* published. Unfortunately I know of no good way to do this * published. Unfortunately I know of no good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#if defined(__FreeBSD__) || defined(__linux__) || defined(__sun__) || defined(__GLIBC__) || (defined(__APPLE__) && defined(__MACH__)) #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__linux__) || defined(__sun__) || defined(__GLIBC__) || (defined(__APPLE__) && defined(__MACH__))
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
+5 -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",
@@ -35,7 +36,10 @@ setup(name = "pymilter", version = '1.0.5',
# set MAX_ML_REPLY to 1 for sendmail < 8.13 # set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ], define_macros = [ ('MAX_ML_REPLY',32) ],
# save lots of debugging time testing rfc2553 compliance # save lots of debugging time testing rfc2553 compliance
extra_compile_args = [ "-Werror=implicit-function-declaration" ] extra_compile_args = [
"-Werror=implicit-function-declaration",
"-std=gnu17",
]
), ),
], ],
keywords = ['sendmail','milter'], keywords = ['sendmail','milter'],
+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
+1 -1
View File
@@ -11,7 +11,7 @@ class ConfigTestCase(unittest.TestCase):
miltersrs = cp.getboolean('srsmilter','miltersrs') miltersrs = cp.getboolean('srsmilter','miltersrs')
self.assertFalse(miltersrs) self.assertFalse(miltersrs)
def suite(): return unittest.makeSuite(ConfigTestCase,'test') def suite(): return unittest.TestLoader().loadTestsFromTestCase(ConfigTestCase)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+1 -1
View File
@@ -49,7 +49,7 @@ class GreylistTestCase(unittest.TestCase):
grey.close() grey.close()
def suite(): def suite():
s = unittest.makeSuite(GreylistTestCase,'test') s = unittest.TestLoader().loadTestsFromTestCase(GreylistTestCase)
return s return s
if __name__ == '__main__': if __name__ == '__main__':
+1 -1
View File
@@ -234,7 +234,7 @@ class MimeTestCase(unittest.TestCase):
#print(msg + filter.msg) #print(msg + filter.msg)
self.assertTrue(result.getvalue() == msg + filter.msg) self.assertTrue(result.getvalue() == msg + filter.msg)
def suite(): return unittest.makeSuite(MimeTestCase,'test') def suite(): return unittest.TestLoader().loadTestsFromTestCase(MimeTestCase)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 2: if len(sys.argv) < 2:
+7 -1
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 = False # FIXME: test old and new bsddb
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,8 +38,11 @@ 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.TestLoader().loadTestsFromTestCase(PolicyTestCase)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 2: if len(sys.argv) < 2:
+4 -4
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")
@@ -142,7 +142,7 @@ class BMSMilterTestCase(unittest.TestCase):
f.write(fp.getvalue()) f.write(fp.getvalue())
milter.close() milter.close()
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test') def suite(): return unittest.TestLoader().loadTestsFromTestCase(BMSMilterTestCase)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+3 -3
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'])
@@ -54,7 +54,7 @@ class AddrCacheTestCase(unittest.TestCase):
self.assertEqual(s,('WRONG', 'a@b')) self.assertEqual(s,('WRONG', 'a@b'))
def suite(): def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test') s = unittest.TestLoader().loadTestsFromTestCase(AddrCacheTestCase)
s.addTest(doctest.DocTestSuite(Milter.utils)) s.addTest(doctest.DocTestSuite(Milter.utils))
s.addTest(doctest.DocTestSuite(Milter.dynip)) s.addTest(doctest.DocTestSuite(Milter.dynip))
s.addTest(doctest.DocTestSuite(Milter.pyip6)) s.addTest(doctest.DocTestSuite(Milter.pyip6))