Compare commits

..

13 Commits

Author SHA1 Message Date
Scott Kitterman 539d50325b Fix typo in log message 2020-01-15 09:17:53 -05:00
Scott Kitterman 04dd916ab2 Add release date for 1.0.3 2019-11-22 20:40:13 -05:00
Scott Kitterman 34b2edbb50 - Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
   will do this correctly on its own based on umask, the milter doesn't need
   to do it) (LP: #1849712)
2019-10-29 07:34:26 -04:00
Scott Kitterman d117330113 Make error logging more explicit to aid debugging 2019-10-18 23:33:36 -04:00
Scott Kitterman 2528632ba6 Release 1.0.2 2019-10-07 00:14:20 -04:00
Scott Kitterman 3ea22f1529 Spelling fix 2019-10-07 00:14:03 -04:00
Scott Kitterman 097e053309 Update README now that sysv init is tested 2019-10-06 00:15:30 -04:00
Scott Kitterman 419d2b54ea Set version in setup.py to 1.0.2~rc1 2019-10-05 21:51:43 -04:00
Scott Kitterman 3ff685205c Fix sysv init so it works (LP: #1839487) 2019-10-05 21:50:47 -04:00
Scott Kitterman 7986de6629 - Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 11:52:17 -04:00
Scott Kitterman 5322c81027 0Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
2019-09-16 20:09:55 -04:00
Scott Kitterman 94538ffa6b Fix startup logging so it provides information at a useful time 2019-09-06 00:44:04 -04:00
Scott Kitterman 721da801fe - Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
2019-09-05 23:57:36 -04:00
22 changed files with 136 additions and 589 deletions
-1
View File
@@ -1,4 +1,3 @@
dist
dkimpy_milter.egg-info
*.pyc
*~
+13 -19
View File
@@ -1,28 +1,22 @@
1.1.2 2019-09-23
1.0.3 2019-11-22
- Make error logging more explicit to aid debugging
- Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
will do this correctly on its own based on umask, the milter doesn't need
to do it) (LP: #1849712)
1.0.2 2019-10-07
- Fix startup logging so it provides information at a useful time
- Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
- Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
- Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
1.1.1 2019-09-06
- Fix startup logging so it provides information at a useful time
- Fix verify processing so missing (optional) i= tag doesn't cause the milter
to fail (LP: #1842250)
- Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
1.1.0 2019-04-12
- Add SubDomains option to enable signing for sub-domains (LP: #1811535)
- Port to python3 (LP: #1815502)
- Add test suite using opendkim miltertest
- When Socket is absolute path, do not strip leading /
- Handle unix: socket prefix the same as local:
- Set up correct AuthservID defaults
- config: Reassemble strings sensibly
- Consistently prefer dnspython to Py3DNS (LP: #1815558)
- Fix sysv init so it works (LP: #1839487)
1.0.1 2019-02-11
- Reorder milter start and dropping privileges so permissions on Unix socket
* Reorder milter start and dropping privileges so permissions on Unix socket
are correct (LP: 1797720)
- Make domain checks case insensitive for determining if signing should be
done (LP: #1815311)
+10 -11
View File
@@ -17,13 +17,13 @@ to be installed when installing using setup.py, the following incantation is
required because setuptools developers decided not being able to do this by
default is a feature:
python3 setup.py install --single-version-externally-managed --record=/dev/null
python setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
available in either the main or backports repositories:
[sudo] apt install python3-milter python3-nacl python3-dnspython
[sudo] apt install -t stretch-backports python3-authres python3-dkim
[sudo] apt install python-milter python-nacl python-ipaddress python-dnspython
[sudo] apt install -t stretch-backports python-authres python-dkim
The preferred method of installation is from PyPi using pip (if distribution
packages are not available):
@@ -33,11 +33,11 @@ packages are not available):
Using pip will cause required packages to be installed via easy_install if they
have not been previously installed. Because pymilter and PyNaCl are compiled
Python extensions, the system will need appropriate development packages and
an C compiler. Alternately, install these dependencies from distribution/OS
an C compiler. Alternately, install these dependencies from dsitribution/OS
packages and then pip install dkimpy_milter.
The milter will work with either py3dns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with
The milter will work with either pydns (DNS) or dnspython (dns), preferring
dnspython if both are available. The dkimpy DKIM module also works with
either.
@@ -84,9 +84,8 @@ MTA INTEGRATION
Both a systemd unit file and a sysv init file are provided. Both make
assumptions about defaults being used, e.g. if a non-standard pidfile name is
used, they will need to be updated. The sysv init file is Debian specific and
untested, since the developers are not using sysv init. Feedback/patches
welcome.
used, they will need to be updated. The sysv init file uses start-stop-deamon
from Debian. It is not portable to systems without that available.
The dkimpy-milter drops priviledges after setup to the user/group specified in
UserID. During initial setup, this system user needs to be manually created.
@@ -94,7 +93,7 @@ As an example, using the default dkimpy-user on Debian, the command would be:
[sudo] adduser --system --no-create-home --quiet --disabled-password \
--disabled-login --shell /bin/false --group \
--home /run/dkimpy-milter dkimpy-milter
--home /var/run/dkimpy-milter dkimpy-milter
Since /var/run or /run is sometimes on a tempfs, if the PID file directory is
missing, the milter will create it on startup.
@@ -117,7 +116,7 @@ Configuration is very similar to opendkim, but needs some adjustment for
dkimpy-milter. Here's an example configuration line to include in your
sendmail.mc:
INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/run/dkimpy-milter/dkimpy-milter.sock')dnl
INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/var/run/dkimpy-milter/dkimpy-milter.sock')dnl
Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf)
and a restart of sendmail. Note that S= needs to match the value of Socket in
+6 -8
View File
@@ -44,20 +44,15 @@ No additional features
1.0.1
Bug fix only, improved documentation
1.1.0
Port to Python 3 implemented verified
Subdomain support implemented verified
Test suite implemented verified
1.1.0 (planned)
Port to Python 3
Subdomain support
Planned dataset type support (if needed):
mdb:
Considered for near-term feature release
KeyTable
KeytableEd25519
SigningTable
SigningTableEd25519
AlwaysAddARHeader
ChangeRootDirectory
ClockDrift (requires dkimpy change)
@@ -78,6 +73,8 @@ ExternalIgnoreList
FixCRLF
KeepAuthResults
KeepTemporaryFiles
KeyTable
KeytableEd25519
LogResults
LogWhy
MaximumHeaders
@@ -101,6 +98,7 @@ RequireSafeKeys
SignatureAlgorithm
SignatureTTL
SignHeaders
SigningTable
SoftwareHeader
StrictHeaders
SubDomains
+46 -78
View File
@@ -1,4 +1,4 @@
#! /usr/bin/python3
#! /usr/bin/python2
# Original dkim-milter.py code:
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2007 Business Management Systems, Inc.
@@ -28,16 +28,14 @@ import dkim
import authres
import os
import tempfile
import io
import StringIO
import re
import codecs
from Milter.utils import parse_addr, parseaddr
import dkimpy_milter.config as config
from dkimpy_milter.util import drop_privileges
from dkimpy_milter.util import setExceptHook
from dkimpy_milter.util import write_pid
from dkimpy_milter.util import read_keyfile
from dkimpy_milter.util import own_socketfile
from dkimpy_milter.util import fold
__version__ = "1.0.1"
@@ -63,9 +61,7 @@ class dkimMilter(Milter.Base):
self.external_connection = False
self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j')
if self.receiver is not None:
self.receiver = self.receiver.strip()
self.receiver = self.getsymval('j').strip()
try:
self.AuthservID = milterconfig['AuthservID']
except:
@@ -112,7 +108,7 @@ class dkimMilter(Milter.Base):
def envfrom(self, f, *str):
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
syslog.syslog("mail from: {0} {1}".format(f, str))
self.fp = io.BytesIO()
self.fp = StringIO.StringIO()
self.mailfrom = f
t = parse_addr(f)
if len(t) == 2:
@@ -145,16 +141,16 @@ class dkimMilter(Milter.Base):
self.arheaders.append(val)
if self.fp:
try:
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
self.fp.write("%s: %s\n" % (name, val))
except:
# Don't choke on header fields with non-ascii garbage in them.
# Don't choke on header fields with garbage in them.
pass
return Milter.CONTINUE
@Milter.noreply
def eoh(self):
if self.fp:
self.fp.write(b"\n") # terminate headers
self.fp.write("\n") # terminate headers
self.bodysize = 0
return Milter.CONTINUE
@@ -189,8 +185,6 @@ class dkimMilter(Milter.Base):
domain = milterconfig.get('Domain')
else:
domain = ''
if milterconfig.get('SubDomains'):
self.fdomain = _get_parent_domain(self.fdomain, domain)
if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
and not self.external_connection):
self.sign_dkim(txt)
@@ -202,20 +196,20 @@ class dkimMilter(Milter.Base):
h = authres.AuthenticationResultsHeader(authserv_id=
self.AuthservID,
results=self.arresults)
h = fold(codecs.encode(str(h), 'ascii'))
h = fold(str(h))
if (milterconfig.get('Syslog') and
milterconfig.get('debugLevel') >= 2):
syslog.syslog(codecs.decode(h, 'ascii'))
name, val = codecs.decode(h, 'ascii').split(': ', 1)
syslog.syslog(str(h))
name, val = str(h).split(': ', 1)
self.addheader(name, val, 0)
return Milter.CONTINUE
def sign_dkim(self, txt):
canon = codecs.encode(milterconfig.get('Canonicalization'), 'ascii')
canon = milterconfig.get('Canonicalization')
canonicalize = []
if len(canon.split(b'/')) == 2:
canonicalize.append(canon.split(b'/')[0])
canonicalize.append(canon.split(b'/')[1])
if len(canon.split('/')) == 2:
canonicalize.append(canon.split('/')[0])
canonicalize.append(canon.split('/')[1])
else:
canonicalize.append(canon)
canonicalize.append(canon)
@@ -225,36 +219,35 @@ class dkimMilter(Milter.Base):
try:
if privateRSA:
d = dkim.DKIM(txt)
h = d.sign(codecs.encode(milterconfig.get('Selector'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
codecs.encode(privateRSA, 'ascii'),
canonicalize=(canonicalize[0],
h = d.sign(milterconfig.get('Selector'), self.fdomain,
privateRSA, canonicalize=(canonicalize[0],
canonicalize[1]))
name, val = h.split(b': ', 1)
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
name, val = h.split(': ', 1)
self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess')
or milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM signature added (s={2} '
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's').decode(),
d.domain.decode().lower()))
d.signature_fields.get(b'a'),
d.signature_fields.get(b's'),
d.domain.lower()))
if privateEd25519:
d = dkim.DKIM(txt)
h = d.sign(codecs.encode(milterconfig.get('SelectorEd25519'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain,
privateEd25519, canonicalize=(canonicalize[0],
canonicalize[1]),
signature_algorithm=b'ed25519-sha256')
name, val = h.split(b': ', 1)
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
signature_algorithm='ed25519-sha256')
name, val = h.split(': ', 1)
self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess')
or milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM signature added (s={2} '
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's').decode(),
d.domain.decode().lower()))
d.signature_fields.get(b'a'),
d.signature_fields.get(b's'),
d.domain.lower()))
except dkim.DKIMException as x:
if milterconfig.get('Syslog'):
syslog.syslog('DKIM: {0}'.format(x))
@@ -269,23 +262,21 @@ class dkimMilter(Milter.Base):
for y in range(self.has_dkim): # Verify _ALL_ the signatures
d = dkim.DKIM(txt)
try:
dnsoverride = milterconfig.get('DNSOverride')
if isinstance(dnsoverride, str):
syslog.syslog("DNSOverride: {0}".format(dnsoverride))
res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
else:
res = d.verify(idx=y)
algo = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
if res:
if algo == 'ed25519-sha256':
if d.signature_fields.get(b'a') == 'ed25519-sha256':
self.dkim_comment = ('Good {0} signature'
.format(algo))
.format(d.signature_fields
.get(b'a')))
else:
self.dkim_comment = ('Good {0} bit {1} signature'
.format(d.keysize, algo))
.format(d.keysize,
d.signature_fields
.get(b'a')))
else:
self.dkim_comment = ('Bad {0} bit {1} signature.'
.format(d.keysize, algo))
.format(d.keysize,
d.signature_fields.get(b'a')))
except dkim.DKIMException as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'):
@@ -293,19 +284,18 @@ class dkimMilter(Milter.Base):
except Exception as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: {0}".format(x))
syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x))
try:
# i= is optional and dkimpy is fine if it's not provided
self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
self.header_i = d.signature_fields.get(b'i')
except TypeError as x:
self.header_i = None
try:
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
self.header_d = d.signature_fields.get(b'd')
self.header_a = d.signature_fields.get(b'a')
except Exception as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: {0}".format(x))
syslog.syslog("check_dkim: Internal proram fault extracting header a or d: {0}".format(x))
self.header_d = None
if not self.header_a:
self.header_a = 'rsa-sha256'
@@ -315,9 +305,9 @@ class dkimMilter(Milter.Base):
milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM signature verified (s={2} '
'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's').decode(),
d.domain.decode().lower()))
d.signature_fields.get(b'a'),
d.signature_fields.get(b's'),
d.domain.lower()))
self.dkim_domain = d.domain.lower()
else:
if milterconfig.get('DiagnosticDirectory'):
@@ -351,16 +341,6 @@ class dkimMilter(Milter.Base):
self.header_a = None
return
# get parent domain to be signed for if fdomain is a subdomain
def _get_parent_domain(fdomain, domains):
for domain in domains:
rhs = '.'+domain
# compare right hand side of fdomain against .domain
if fdomain[-len(rhs):] == rhs:
# return parent domain on match
return domain
# or return the fdomain itself
return fdomain
def main():
# Ugh, but there's no easy way around this.
@@ -390,18 +370,6 @@ def main():
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
miltername = 'dkimpy-filter'
socketname = milterconfig.get('Socket')
if socketname is None:
if int(os.environ.get('LISTEN_PID', '0')) == os.getpid():
lfds = os.environ.get('LISTEN_FDS')
if lfds is not None:
if lfds != '1':
syslog.syslog('LISTEN_FDS is set to "{0}", but we only know how to deal with "1", ignoring it'.
format(lfds))
else:
socketname = 'fd:3'
if socketname is None:
socketname = 'local:/var/run/dkimpy-milter/dkimpy-milter.sock'
own_socketfile(milterconfig, socketname)
drop_privileges(milterconfig)
sys.stdout.flush()
if milterconfig.get('Syslog'):
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/python3
from dkimpy_milter import main
if __name__ == "__main__":
main()
+15 -30
View File
@@ -31,16 +31,16 @@ import stat
import dkim
import socket
import ipaddress
from .dnsplug import Session
from dnsplug import Session
# default values
defaultConfigData = {
'Syslog': 'yes',
'SyslogFacility': 'mail',
'UMask': 0o07,
'UMask': 007,
'Mode': 'sv',
'Socket': None,
'PidFile': None,
'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid',
'UserID': 'dkimpy-milter',
'Canonicalization': 'relaxed/simple',
'InternalHosts': '127.0.0.1',
@@ -48,8 +48,6 @@ defaultConfigData = {
'DiagnosticDirectory': '',
'MacroList': '',
'MacroListVerify': '',
'DNSOverride': None,
'SubDomains': False,
'debugLevel': 0 # Undocumented config item for developer use
}
@@ -86,14 +84,14 @@ class HostsDataset(object):
self.item = item[1:]
self.negative = True
try:
self.item = ipaddress.ip_address(str(self.item, "utf-8"))
self.item = ipaddress.ip_address(unicode(self.item, "utf-8"))
if isinstance(self.item, ipaddress.IPv4Address):
self.isipv4 = True
elif isinstance(self.item, ipaddress.IPv6Address):
self.isipv6 = True
except ValueError as e:
try:
self.item = ipaddress.ip_network(str
self.item = ipaddress.ip_network(unicode
(self.item, "utf-8"),
strict=False)
if isinstance(self.item, ipaddress.IPv4Network):
@@ -111,7 +109,7 @@ class HostsDataset(object):
def match(self, connectip):
'''Check if the connect IP is part of the dataset'''
source = ipaddress.ip_address(str(connectip, "utf-8"))
source = ipaddress.ip_address(unicode(connectip, "utf-8"))
for item in self.dataset:
if item.isdomain or item.ishostname:
result = self.matchname(source) # Match host/domains first
@@ -161,13 +159,13 @@ class HostsDataset(object):
if isinstance(source, ipaddress.IPv4Address):
ips = s.dns(name, 'A')
for ip in ips:
ip = ipaddress.IPv4Address(str(ip, 'UTF-8'))
ip = ipaddress.IPv4Address(unicode(ip, 'UTF-8'))
if ip == source:
results.append(name)
if isinstance(source, ipaddress.IPv6Address):
ips = s.dns(name, 'AAAA')
for ip in ips:
ip = ipaddress.IPv6Address(str(ip, 'UTF-8'))
ip = ipaddress.IPv6Address(unicode(ip, 'UTF-8'))
if ip == source:
results.append(name)
return results
@@ -226,13 +224,13 @@ def _processConfigFile(filename=None, configdata=None, useSyslog=1,
'''Load the specified config file, exit and log errors if it fails,
otherwise return a config dictionary.'''
from . import config
import config
if configdata is None:
configdata = config.defaultConfigData
if filename is not None:
try:
_readConfigFile(filename, configdata)
except Exception as e:
except Exception, e:
raise
if useSyslog:
syslog.syslog(e.args[0])
@@ -310,9 +308,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
dictionary of name/value pairs based on configData and the values
read from path.'''
# No config file data is available yet, so to debug _readConfigFile, set
# the value here.
debugLevel = 0
debugLevel = configGlobal.get('debugLevel', 0)
if debugLevel >= 5:
syslog.syslog('readConfigFile: Loading "%s"' % path)
if configData is None:
@@ -328,7 +324,6 @@ def _readConfigFile(path, configData=None, configGlobal={}):
'PidFile': 'str',
'UserID': 'str',
'Domain': 'dataset',
'SubDomains': 'bool',
'KeyFile': 'str',
'KeyFileEd25519': 'str',
'Selector': 'str',
@@ -339,14 +334,13 @@ def _readConfigFile(path, configData=None, configGlobal={}):
'DiagnosticDirectory': 'str',
'MacroList': 'dataset',
'MacroListVerify': 'dataset',
'DNSOverride': 'str',
'debugLevel': 'int'
}
# check to see if it's a file
try:
mode = os.stat(path)[0]
except OSError as e:
except OSError, e:
syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
% (path, e.strerror))
return(configData)
@@ -382,15 +376,9 @@ def _readConfigFile(path, configData=None, configGlobal={}):
value = data[1:]
# check validity of name
try:
conversion = nameConversion.get(name)
except TypeError:
name = name[0]
syslog.syslog('Config item "%s" does not provide a value in file "%s"'
% (name, path))
conversion = None
if conversion is None:
syslog.syslog('ERROR: Unknown name or name missing value "%s" in file "%s"'
syslog.syslog('ERROR: Unknown name "%s" in file "%s"'
% (name, path))
continue
@@ -400,9 +388,6 @@ def _readConfigFile(path, configData=None, configGlobal={}):
if conversion == 'bool':
configData[name] = _find_boolean(value)
elif conversion == 'str':
if isinstance(value, list):
configData[name] = line.split(None, 1)[1]
else:
configData[name] = str(value)
elif conversion == 'int':
configData[name] = int(value)
@@ -414,7 +399,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
configData[name] = conversion(value)
fp.close()
try:
configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME'))
configData['AuthservID'] = _make_authserv_id(configData['AuthservID'])
configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
except:
pass
+8 -8
View File
@@ -84,7 +84,7 @@ class Session(object):
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname
if cname in cnames:
raise DNSError('CNAME loop')
raise DNSError, 'CNAME loop'
result = self.dns(cname, qtype, cnames=cnames)
return result
@@ -103,16 +103,16 @@ def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
#
if resp.header['tc'] == True:
if not tcpfallback:
raise DNS.DNSError('DNS: Truncated UDP Reply, SPF records should fit in a UDP packet')
raise DNS.DNSError, 'DNS: Truncated UDP Reply, SPF records should fit in a UDP packet'
try:
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
timeout=timeout)
resp = req.req()
except DNS.DNSError as x:
raise DNS.DNSError('TCP Fallback error: ' + str(x))
except DNS.DNSError, x:
raise DNS.DNSError, 'TCP Fallback error: ' + str(x)
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError as x:
raise DNS.DNSError('DNS: ' + str(x))
except IOError, x:
raise DNS.DNSError, 'DNS: ' + str(x)
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
retVal = []
@@ -164,5 +164,5 @@ if __name__ == '__main__':
import sys
s = Session()
for n,t in zip(*[iter(sys.argv[1:])]*2):
print(n,t)
print(s.dns(n,t))
print n,t
print s.dns(n,t)
+8 -55
View File
@@ -115,61 +115,37 @@ def write_pid(milterconfig):
"""Write PID in pidfile. Will not overwrite an existing file."""
import os
import syslog
pidfile = milterconfig.get('PidFile')
if pidfile is None:
return
if not os.path.isfile(pidfile):
if not os.path.isfile(milterconfig.get('PidFile')):
pid = str(os.getpid())
try:
f = open(pidfile, 'w')
f = open(milterconfig.get('PidFile'), 'w')
except IOError as e:
if str(e)[:35] == '[Errno 2] No such file or directory':
piddir = pidfile.rsplit('/', 1)[0]
piddir = milterconfig.get('PidFile').rsplit('/', 1)[0]
os.mkdir(piddir)
user, group = user_group(milterconfig.get('UserID'))
os.chown(piddir, user, group)
f = open(pidfile, 'w')
f = open(milterconfig.get('PidFile'), 'w')
if milterconfig.get('Syslog'):
syslog.syslog('PID dir created: {0}'.format(piddir))
else:
if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'
.format(pidfile, e))
.format(milterconfig.get('PidFile'), e))
raise
f.write(pid)
f.close()
user, group = user_group(milterconfig.get('UserID'))
os.chown(pidfile, user, group)
os.chown(milterconfig.get('PidFile'), user, group)
else:
if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. File exists.'
.format(pidfile))
.format(milterconfig.get('PidFile')))
raise RuntimeError('Unable to write pidfle {0}. File exists.'
.format(pidfile))
.format(milterconfig.get('PidFile')))
return pid
def own_socketfile(milterconfig, sockname=None):
"""If socket is Unix socket, chown to UserID before dropping privileges"""
import os
user, group = user_group(milterconfig.get('UserID'))
offset = None
if sockname is None:
sockname = milterconfig.get('Socket')
if sockname is None:
return
if sockname[:1] == '/':
offset = 0
elif sockname[:6] == "local:":
offset = 6
elif sockname[:5] == "unix:":
offset = 5
if offset is not None:
if os.path.exists(sockname[offset:]):
os.chown(sockname[offset:], user, group)
def read_keyfile(milterconfig, keytype):
"""Read private key from file."""
import syslog
@@ -190,26 +166,3 @@ def read_keyfile(milterconfig, keytype):
for line in keylist:
key += line
return key
def read_keytable(milterconfig, tabletype):
"""Read keytables into in memory configuration data so all keys are read
before priviledges are dropped."""
import syslog
if tabletype == "RSA":
tablefile = milterconfig.get('KeyTable')
if tabletype == "Ed25519":
tablefile = milterconfig.get('KeyTableEd25519')
if milterconfig.get(tablefile):
keytabledata = []
try:
f = open(milterconfig.get(tablefile))
for row in f:
keytabledata.append(row)
f.close()
except IOError as e:
if milterconfig.get('Syslog'):
syslog.syslog('Unable to read keytable {0}. IOError: {1}'
.format(tablefile, e))
raise
return keytabledata
+1 -1
View File
@@ -38,7 +38,7 @@ Socket inet:8892@localhost
### Name of the file where the filter should write its pid before beginning
### normal operations.
#
PidFile /run/dkimpy-milter/dkimpy-milter.pid
PidFile /var/run/dkimpy-milter/dkimpy-milter.pid
## Userid userid
### default dkimpy-milter
+9 -61
View File
@@ -131,7 +131,7 @@
.SH "NAME"
dkimpy-milter \- Python milter for DKIM signing and validation
.SH "VERSION"
1\.1\.0
0\.9\.2
.SH "DESCRIPTION"
.I dkimpy-milter(8)
@@ -221,16 +221,12 @@ domains will be verified rather than being signed.
This parameter is not required if a
.I SigningTable
or
.I SigningTableEd25519
is in use; in that case, the list of signed domains is implied by the
lines in that file.
lines in that file. [SigningTable NOT IMPLEMENTED]
This parameter is ignored if a
.I KeyTable
or
.I KeyTableD25119
is defined.
is defined. [KeyTable NOT IMPLEMENTED]
.TP
.I InternalHosts (dataset)
@@ -248,7 +244,7 @@ address explicitly. [PeerList NOT IMPLEMENTED]
Gives the location of a PEM-formatted private key to be used for RSA signing
all messages. Ignored if a
.I KeyTable
is defined.
is defined. [KeyTable NOT IMPLEMENTED]
.TP
.I KeyFileEd25519 (string)
@@ -256,17 +252,7 @@ Gives the location of a Ed25519 private key to be used for Ed25519 signing
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
generation (as used in dkimpy). Ignored if a
.I KeyTableEd25519
is defined.
.TP
.I KeyTable (dataset)
Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not supported.
.TP
.I KeyTableEd25519 (dataset)
Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not support
ed.
is defined. [KeyTableEd25519 NOT IMPLEMENTED]
.TP
.I MacroList (dataset)
@@ -323,13 +309,7 @@ When signing mode is enabled, one of the following combinations must also
be set:
(a) Domain, KeyFile, Selector, no KeyTable, no SigningTable;
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
.TP
.I DNSOverride (string)
Provide a text string that a verifying milter should use instead of
consulting the DNS on each message. This is useful primarily for
testing purposes in environments where it is awkward to modify the
system DNS resolution. It should not be used in production.
[fooTable options NOT IMPLEMENTED]
.TP
.I PeerList (dataset)
@@ -351,7 +331,7 @@ will be checked. [PeerList NOT IMPLEMENTED - included for reference only]
.TP
.I PidFile (string)
Specifies the path to a file that should be created at process start
containing the process ID. If not specified, no such file will be created.
containing the process ID.
.TP
.I Selector (string)
@@ -365,7 +345,7 @@ parameter below for more information.
This parameter is ignored if a
.I KeyTable
is defined.
is defined. [KeyTable NOT IMPLEMENTED]
.TP
.I SelectorEd25519 (string)
@@ -379,33 +359,7 @@ parameter below for more information.
This parameter is ignored if a
.I KeyTableEd25519
is defined.
.TP
.I SigningTable (dataset)
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
In any case, only the first match is applied.
.TP
.I SigningTableEd25519 (dataset)
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
In any case, only the first match is applied.
is defined. [KeyTable NOT IMPLEMENTED]
.TP
.I Socket (string)
@@ -430,12 +384,6 @@ is not given as either a hostname or an IP address, the socket will be
listening on all interfaces. A literal IP address must be enclosed in
square brackets. This option is mandatory in the configuration file.
.TP
.I SubDomains (Boolean)
Sign subdomains of those listed by the
.I Domain
parameter as well as the actual domains.
.TP
.I Syslog (Boolean)
Log via calls to
+7 -7
View File
@@ -1,7 +1,7 @@
#! /usr/bin/python3
#! /usr/bin/python
# dkimpy-milter: A DKIM signing/verification Milter application
# Author: Scott Kitterman <scott@kitterman.com>
# Copyright 2018,2019 Scott Kitterman
# Copyright 2018 Scott Kitterman
""" This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
@@ -23,14 +23,14 @@ description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for P
kw = {} # Work-around for lack of 'or' requires in setuptools.
try:
import dns
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'dnspython']
import DNS
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS']
except ImportError: # If PyDNS is not installed, prefer dnspython
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS']
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython']
setup(
name='dkimpy-milter',
version='1.1.2',
version='1.0.3',
author='Scott Kitterman',
author_email='scott@kitterman.com',
url='https://launchpad.net/dkimpy-milter',
@@ -43,7 +43,7 @@ setup(
'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: English',
'Operating System :: POSIX',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 2 :: Only',
'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Communications :: Email :: Filters',
'Topic :: Security',
+8 -6
View File
@@ -20,7 +20,7 @@
### END INIT INFO
prefix="/usr/local"
exec_prefix=${prefix}
sysconfdir="/etc/dkimpy-milter"
sysconfdir="/usr/local/etc"
bindir="${exec_prefix}/bin/"
RUNDIR="/run/dkimpy-milter"
DAEMON=${bindir}/dkimpy-milter
@@ -67,14 +67,14 @@ case "$1" in
fi
fi
fi
start-stop-daemon --start --quiet --pidfile $RUNDIR/$NAME.pid --startas \
$DAEMON $sysconfdir/$NAME.conf --name $NAME --test > /dev/null \
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
@@ -87,6 +87,7 @@ case "$1" in
force-reload)
echo -n "Force reloading $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
@@ -95,7 +96,7 @@ case "$1" in
fi
fi
sleep 1
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
@@ -103,6 +104,7 @@ case "$1" in
echo "Restarting $DESC: "
echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
@@ -113,7 +115,7 @@ case "$1" in
echo "$NAME."
sleep 1
echo -n "Starting $DESC: "
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
+1 -1
View File
@@ -5,7 +5,7 @@ After=network.target
[Service]
Type=simple
PIDFile=/run/dkimpy-milter/dkimpy-milter.pid
PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid
ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
[Install]
-32
View File
@@ -1,32 +0,0 @@
This directory contains example systemd unit files for running a
supervised, socket-activated instance of dkimpy-milter.
There are several advantages of using socket activation:
- dkimpy-milter never runs with elevated privileges, they are dropped
before any dkimpy-milter code is executed.
- The socket is opened before dkimpy-milter runs. This means that
clients can connect() to the socket immediately. So even if there
is a delay in dkimpy-milter startup, or in libmilter itself, the
connection will not fail.
- You can set the privileges of a listening Unix-domain socket by an
override of ListenGroup= in dkimpy-milter.socket (see
systemd.unit(5) for how to override). This lets you control who has
access to the daemon with finer granularity than is available with
dkimpy-milter on its own.
- dkimpy-milter will not consume system resources if it is not used.
- A fully-supervised dkimpy-milter needs no PIDFile, UMask, UserID, or
Socket configuation. This eliminates common race conditions and
startup failures, and simplifies the resulting configuration file.
There is one downside to using socket activation:
- it will only work on systems where libmilter can support connection
strings like "fd:3". This has been supported on Debian and derived
systems since sendmail 8.14.4-6 (before Debian Jessie, in early
2014), see for example:
https://sources.debian.org/src/sendmail/8.15.2-8/debian/patches/socket_activation.patch/
@@ -1,11 +0,0 @@
[Unit]
Description=DKIMpy Milter
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
Requires=dkimpy-milter.socket
[Service]
ExecStart=/usr/bin/dkimpy-milter /etc/dkimpy-milter.conf
User=dkimpy-milter
[Install]
Also=dkimpy-milter.socket
@@ -1,12 +0,0 @@
[Unit]
Description=DKIMpy Milter socket
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
[Socket]
ListenStream=/run/dkimpy-milter/dkimpy-milter.sock
SocketMode=0660
# override SocketGroup to grant access to members of another system group:
SocketGroup=dkimpy-milter
[Install]
WantedBy=sockets.target
-12
View File
@@ -1,12 +0,0 @@
-- -*- lua -*-
for _, keytype in ipairs({"ed25519", "rsa"}) do
for _, func in ipairs({"signing", "verify"}) do
mt.echo("testing "..keytype.." "..func)
conn = mt.connect("unix:"..keytype.."."..func..".sock")
if conn == nil then
error("mt.connect() failed "..keytype.." "..func)
end
mt.disconnect(conn)
mt.echo(keytype.." "..func.." complete")
end
end
-40
View File
@@ -1,40 +0,0 @@
-- -*- lua -*-
for _, keytype in ipairs({"ed25519", "rsa"}) do
for _, func in ipairs({"signing", "verify"}) do
mt.echo("testing "..keytype.." "..func)
conn = mt.connect("unix:"..keytype.."."..func..".sock")
if conn == nil then
error("mt.connect() failed "..keytype.." "..func)
end
if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
error("mt.conninfo() failed "..keytype.." "..func)
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.conninfo() unexpected reply "..keytype.." "..func)
end
if mt.test_action(conn, SMFIF_ADDHDRS) then
print("could add headers "..keytype.." "..func)
else
error("mt.test_action() says could not add headers "..keytype.." "..func)
end
if mt.test_action(conn, SMFIF_CHGHDRS) then
print("could change headers "..keytype.." "..func)
else
error("mt.test_action() says could not change headers "..keytype.." "..func)
end
-- -- FIXME: this part of the test fails, as apparently the
-- -- dkimpy-milter claims the right to change the body of a message,
-- -- even though it shouldn't. How can we fix the negotiation?
-- if mt.test_action(conn, SMFIF_CHGBODY) then
-- error("mt.test_action() says could change body "..keytype.." "..func)
-- else
-- print("could not change body "..keytype.." "..func)
-- end
mt.disconnect(conn)
mt.echo(keytype.." "..func.." test complete")
end
end
-100
View File
@@ -1,100 +0,0 @@
-- -*- lua -*-
msg = {
['headers'] = {
['From'] = 'Alice <alice@example.net>',
['Message-Id'] = '<dkimpy-milter-test-02@example.net>',
['To'] = 'Bob <bob@example.biz>',
['Date'] = 'Mon, 18 Feb 2019 08:32:50 -0500',
['Subject'] = 'Signing test',
['Content-Type'] = 'text/plain',
},
['body'] = "This is a test!\r\n",
}
-- returns miltertest connection object
function connect_and_send (sockname, headers, body)
conn = mt.connect(sockname)
if conn == nil then
error "mt.connect() failed"
end
if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
error "mt.conninfo() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error "mt.conninfo() unexpected reply"
end
-- mt.macro(conn, SMFIC_MAIL, "i", "simple-message")
if mt.mailfrom(conn, "<alice@example.net>") ~= nil then
error "mt.mailfrom() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error "mt.mailfrom() unexpected reply"
end
-- mt.rcptto() is called implicitly
-- send headers
for key,value in pairs(headers) do
if mt.header(conn, key, value) ~= nil then
error("mt.header(" .. key .. ") failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.header(" .. key .. ") unexpected reply")
end
end
-- send EOH
if mt.eoh(conn) ~= nil then
error "mt.eoh() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error "mt.eoh() unexpected reply"
end
-- send body
if mt.bodystring(conn, body) ~= nil then
error "mt.bodystring() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error "mt.bodystring() unexpected reply"
end
-- end of message; let the filter react
if mt.eom(conn) ~= nil then
error "mt.eom() failed"
end
reply = mt.getreply(conn)
if reply ~= SMFIR_CONTINUE then
error ("mt.eom() unexpected reply: " .. reply)
end
return conn
end
for _, keytype in ipairs({"ed25519", "rsa"}) do
mt.echo("testing "..keytype)
signing = connect_and_send("unix:"..keytype..".signing.sock", msg.headers, msg.body)
-- verify that a test header field got added
if not mt.eom_check(signing, MT_HDRINSERT) then
error "no header added by signer"
end
signature = mt.getheader(signing, "DKIM-Signature", 0)
mt.disconnect(signing)
mt.echo("DKIM-Signature: " .. signature)
msg.headers['DKIM-Signature'] = signature
verify = connect_and_send("unix:"..keytype..".verify.sock", msg.headers, msg.body)
if not mt.eom_check(verify, MT_HDRINSERT) then
error "no header added in verify"
end
authres = mt.getheader(verify, "Authentication-Results", 0)
mt.echo("Authentication-Results: "..authres)
mt.disconnect(verify)
mt.echo(keytype.." complete")
end
-2
View File
@@ -1,2 +0,0 @@
#!/bin/sh
python3 -m dkimpy_milter "$@"
-84
View File
@@ -1,84 +0,0 @@
#!/bin/bash
set -e
WORKDIR=$(mktemp -d)
TESTDIR=$(realpath "$(dirname "$0")")
DKIMPY_MILTER=${DKIMPY_MILTER:-"$TESTDIR/dkimpy-milter"}
KEY_TYPES=(ed25519 rsa)
cd "$WORKDIR"
printf "Testing %s from directory %s\n" "$DKIMPY_MILTER" "$WORKDIR"
for keytype in "${KEY_TYPES[@]}"; do
dknewkey --ktype "$keytype" "testkey.$keytype"
if [ "$keytype" = ed25519 ]; then
keyfile=KeyFileEd25519
selector=SelectorEd25519
else
keyfile=KeyFile
selector=Selector
fi
cat > "$keytype.signing.conf" <<EOF
Domain example.net
$keyfile testkey.$keytype.key
$selector testkey
Socket unix:$keytype.signing.sock
PidFile $keytype.signing.pid
Mode s
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype.verify.conf" <<EOF
Socket unix:$keytype.verify.sock
PidFile $keytype.verify.pid
Mode v
DNSOverride $(cat testkey.$keytype.dns)
UserID $(id --name --user):$(id --name --group)
EOF
done
cleanup() {
echo cleaning up jobs:
jobs
for keytype in "${KEY_TYPES[@]}"; do
for func in signing verify; do
if [ -s "$keytype.$func.pid" ] && kill -0 "$(cat "$keytype.$func.pid")"; then
kill "$(cat $keytype.$func.pid)"
fi
done
done
wait
for keytype in "${KEY_TYPES[@]}"; do
for func in signing verify; do
errdata="$keytype.$func.stderr"
if [ -s "$errdata" ]; then
printf -- "-> %s:\n" "$errdata"
cat "$errdata"
printf -- "-> end %s\n" "$errdata"
fi
done
done
rm -rf "$WORKDIR"
}
for keytype in "${KEY_TYPES[@]}"; do
for func in signing verify; do
PYTHONPATH="$(dirname "$TESTDIR")" "$DKIMPY_MILTER" "$keytype.$func.conf" 2>"$keytype.$func.stderr" &
done
done
trap cleanup EXIT
# ugly ugly (how are we supposed to know that the milters are all ready?):
sleep 2
# uses miltertest from opendkim:
for x in ${TESTS:-"$TESTDIR"/*.miltertest}; do
if ! [ -e "$x" ]; then
if [ -e "$TESTDIR/$x" ]; then
x="$TESTDIR/$x"
fi
fi
printf -- "-> running %s...\n" "$x"
miltertest -s "$x"
done