Compare commits

...

16 Commits

Author SHA1 Message Date
Scott Kitterman 977fac5fae Add 0.9.4 release date 2018-03-09 17:13:17 -05:00
Scott Kitterman 36ff60d8d3 - Added support for DiagnosticDirectory and updated dkimpy-milter specifics in
dkimpy-milter.conf.5
2018-03-09 17:10:48 -05:00
Scott Kitterman 4769bde19c - Added support for InternalHosts option (ipaddress and either dns (dnspython)
or pydns (DNS) modules are now required)
2018-03-09 16:29:49 -05:00
Scott Kitterman e6021dd960 Add dnsplug.py so either DNS or dns can be used for host/domain based dataset processing 2018-03-09 13:51:36 -05:00
Scott Kitterman 9d28ab3567 Add MTA integration information and update depenency installation in README. 2018-03-08 00:37:34 -05:00
Scott Kitterman df19aa081e - Added support for AuthservID option 2018-03-04 15:15:37 -05:00
Scott Kitterman 2e9d0f607f Update man pages for newly implemented features 2018-03-04 14:00:18 -05:00
Scott Kitterman fb32a8fe0b Implement detection of type db datasets, but not used yet 2018-03-04 13:56:06 -05:00
A. Schulze 3e57876361 - Make RSA signatures in dkimpy-milter optional, so dkimpy-milter can be
added after an existing DKIM signing application to add an Ed25519
   signature (Thanks to A. Schulze for the patch)
2018-03-04 13:33:32 -05:00
Scott Kitterman 7683fa7187 - Fixed a bug where dkim fail might be reported as pass when verifying
multiple signatures and a previous signature had passed
2018-03-04 13:18:06 -05:00
Scott Kitterman fc893a62c3 - Added information about creating a dedicated user and PID file directory
creation to README
2018-03-03 14:54:43 -05:00
Scott Kitterman c01c04b83f - Fix header folding to use \n only to align with milter protocol
requirements
2018-03-03 14:39:20 -05:00
Scott Kitterman fc583a6e3c Updated TODO for work done in 0.9.2/3 2018-03-03 14:37:37 -05:00
Scott Kitterman ebfb0b5fc3 Fix crash when verifying if domain for signing was not set 2018-03-03 10:42:59 -05:00
Scott Kitterman 48a44916e7 Create PID directory if it is missing 2018-03-03 01:45:15 -05:00
Scott Kitterman 5a81886a5e Start 0.9.4 2018-03-03 00:59:43 -05:00
10 changed files with 537 additions and 60 deletions
+18
View File
@@ -1,3 +1,21 @@
0.9.4 2018-03-09
- Create PID directory if it is missing
- Fix crash when verifying if domain for signing was not set
- Fix header folding to use \n only to align with milter protocol
requirements
- Added information about creating a dedicated user and PID file directory
creation to README
- Fixed a bug where dkim fail might be reported as pass when verifying
multiple signatures and a previous signature had passed
- Make RSA signatures in dkimpy-milter optional, so dkimpy-milter can be
added after an existing DKIM signing application to add an Ed25519
signature (Thanks to A. Schulze for the patch)
- Added support for AuthservID option
- Added support for InternalHosts option (ipaddress and either dns (dnspython)
or pydns (DNS) modules are now required)
- Added support for DiagnosticDirectory and updated dkimpy-milter specifics in
dkimpy-milter.conf.5
0.9.3 2018-03-02
- Fixup csl dataset processing for single item lists
- file: dataset support
+56 -2
View File
@@ -16,22 +16,76 @@ python setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squueze), all dependencies are
available in either the main or backports repositories:
[sudo] apt install python-milter python-nacl
[sudo] apt install python-milter python-nacl pthon-ipaddress python-dnspython
[sudo] apt install -t squeeze-backports python-authres python-dkim
The preferred method of installation is from PyPi using pip:
The preferred method of installation is from PyPi using pip (if distribution
packages are not available):
[sudo] pip install dkimpy_milter
Using pip will cause required packages to be installed via easy_install if they
have not been previously installed.
The milter will work with either pydns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with
either.
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.
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.
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 /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.
As with all milters, dkimpy-milter needs to be integrated with your MTA of
choice (Sendmail or Postfix).
For Sendmail:
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:/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
the dkimpy-milter configuration file.
Milter support should be present by default in most versions of sendmail
these days, but if not included in your Sendmail build, see:
http://www.elandsys.com/resources/sendmail/milter.html
For Postfix:
Integration of dkimpy-milter into Postfix is like any milter (See Postfix's
README_FILES/MILTER_README). Here's an example master.cf excerpt the talks to
two dkimpy-milter instances, one configured for signing and one configured for
verification:
smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8892
...
submission inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
...
These need to match the Socket value for each dkimpy-milter instance.
The python DKIM library, dkimpy, requires the entire message being signed or
verified to be in memory, so dkimpy-milter does not write messages out to a temp
file. This may impact performance on low-memory systems.
+10 -7
View File
@@ -25,11 +25,16 @@ Sign based on Domain implemented verified
Canonicalization implemented verified
SyslogFacility implemented verified
0.9.3 (Alpha)
File dataset implemented verified
0.9.4 (Alpha)
AuthservID implemented verified
DiagnosticDirectory implemented verified
InternalHosts implemented verified
0.9.5 (Beta)
AuthservID
Diagnostics
DiagnosticDirectory
InternalHosts
SyslogSuccess
@@ -38,10 +43,7 @@ Convert dkim-milter-python config
No additional features planned
Plannedataset type support:
file://
refile:
db:/.db
csl:
mdb:
Considered for near-term feature release
@@ -59,6 +61,7 @@ SignatureAlgorithm
Later
BaseDirectory
Diagnostics (requires dkimpy changes)
DontSignMailTo
ExemptDomains
ExternalIgnoreList
+22 -7
View File
@@ -40,8 +40,9 @@ 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__ = "0.9.3"
__version__ = "0.9.4"
FWS = re.compile(r'\r?\n[ \t]+')
class dkimMilter(Milter.Base):
@@ -62,10 +63,15 @@ class dkimMilter(Milter.Base):
self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
try:
self.AuthservID = milterconfig['AuthservID']
except:
self.AuthservID = self.receiver
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
"""if iniplist(ipaddr,self.conf.internal_connect): FIXME
self.internal_connection = True"""
if milterconfig['InternalHostsObj']:
if milterconfig['InternalHostsObj'].match(ipaddr):
self.internal_connection = True
else: ipaddr = ''
self.connectip = ipaddr
if self.internal_connection:
@@ -151,7 +157,7 @@ class dkimMilter(Milter.Base):
# FIXME: don't delete A-R headers from trusted MTAs
try:
ar = authres.AuthenticationResultsHeader.parse_value(FWS.sub('',val))
if ar.authserv_id == self.receiver:
if ar.authserv_id == self.AuthservID:
self.chgheader('authentication-results',i,'')
if milterconfig.get('Syslog'):
syslog.syslog('REMOVE: {0}'.format(val))
@@ -160,7 +166,11 @@ class dkimMilter(Milter.Base):
pass
# Check or sign DKIM
self.fp.seek(0)
if (self.fdomain in milterconfig.get('Domain')) and (not milterconfig.get('Mode') == 'v'):
if milterconfig.get('Domain'):
domain = milterconfig.get('Domain')
else:
domain = ''
if (self.fdomain in domain) and (not milterconfig.get('Mode') == 'v'):
txt = self.fp.read()
self.sign_dkim(txt)
result = None
@@ -170,9 +180,9 @@ class dkimMilter(Milter.Base):
else:
result = 'none'
if self.arresults:
h = authres.AuthenticationResultsHeader(authserv_id = self.receiver,
h = authres.AuthenticationResultsHeader(authserv_id = self.AuthservID,
results=self.arresults)
h = dkim.fold(str(h))
h = fold(str(h))
if milterconfig.get('Syslog'):
syslog.syslog(str(h))
name,val = str(h).split(': ',1)
@@ -190,6 +200,7 @@ class dkimMilter(Milter.Base):
canonicalize.append(canon)
syslog.syslog('canonicalize: {0}'.format(canonicalize))
try:
if privateRSA:
d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('Selector'), self.fdomain, privateRSA,
canonicalize=(canonicalize[0], canonicalize[1]))
@@ -236,15 +247,19 @@ class dkimMilter(Milter.Base):
syslog.syslog('DKIM: Pass ({0})'.format(d.domain))
self.dkim_domain = d.domain
else:
if milterconfig.get['DiagnosticDirectory']:
fd,fname = tempfile.mkstemp(".dkim")
with os.fdopen(fd,"w+b") as fp:
fp.write(txt)
if milterconfig.get('Syslog'):
syslog.syslog('DKIM: Fail (saved as {0})'.format(fname))
else:
syslog.syslog('DKIM: Fail ({0})'.format(d.domain))
if res:
result = 'pass'
else:
result = 'fail'
res = False
self.arresults.append(
authres.DKIMAuthenticationResult(result=result,
header_i = self.header_i, header_d = self.header_d, header_a = self.header_a,
+196 -3
View File
@@ -31,7 +31,9 @@ import re
import urllib
import stat
import dkim
import socket
import ipaddress
from dnsplug import Session
# default values
defaultConfigData = {
@@ -42,7 +44,10 @@ defaultConfigData = {
'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid',
'UserID' : 'dkimpy-milter',
'Canonicalization' : 'relaxed/simple'
'Canonicalization' : 'relaxed/simple',
'InternalHosts' : '127.0.0.1',
'InternalHostsObj' : False,
'DiagnosticDirectory' : ''
}
@@ -51,6 +56,167 @@ class ConfigException(Exception):
'''Exception raised when there's a configuration file error.'''
pass
#################################
class HostsDataset(object):
'''Hold a group of host related dataset objects'''
def __init__(self, dataset):
self.dataset = []
# Self.dataset will end up being a list of DataSetItem(s).
for item in dataset:
item = item.rstrip(']')
item = item.lstrip('[')
self.dataset.append(self.DatasetItem(item))
class DatasetItem(object):
'''Individual dataset item'''
def __init__(self, item):
self.item = item
self.isipv4 = False
self.isipv4cidr = False
self.isipv6 = False
self.isipv6cidr = False
self.ishostname = False
self.isdomain = False
self.negative = False
if self.item[0] == '!':
self.item = item[1:]
self.negative = True
try:
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(unicode(self.item, "utf-8"), strict=False)
if isinstance(self.item, ipaddress.IPv4Network): self.isipv4cidr = True
elif isinstance(self.item, ipaddress.IPv6Network): self.isipv6cidr = True
except ValueError as e2:
if self.item[0] == '.' and len(self.item.split('.')) > 2:
self.isdomain = True
elif len(self.item.split('.')) > 1: # It has a '.' in it
self.ishostname = True
else:
raise ConfigException('Unknown dataset item: {0}'.format(item))
def match(self, connectip):
'''Check if the connect IP is part of the dataset'''
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/domain names first
if result:
return(result)
elif item.isipv4 or item.isipv4cidr:
if isinstance(source, ipaddress.IPv4Address): # Then IPv4/6 addresses
return(self.match4(source)) # or networks depending
elif item.isipv6 or item.isipv6cidr: # on the item type and
if isinstance(source, ipaddress.IPv6Address): # connection type
return(self.match6(source))
def matchname(self, source):
'''Does source IP address relate to a domain/hostname in the dataset'''
match = False
matchone = False
negativeone = False
matchdomain = False
negativedomain = False
ptrlist = self.getptr(source)
for item in self.dataset:
if item.isdomain:
for ptr in ptrlist:
# Strip the leading '.' off the domain name so exact match works.
if item.item[1:] == ptr[-len(item.item)+1:]:
matchdomain = True
negativedomain = item.negative
elif item.ishostname:
for ptr in ptrlist:
if item.item == ptr:
matchone = True
negativeone = item.negative
if matchdomain and not negativedomain:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def getptr(self, source):
'''Get validated PTR name of IP address'''
results = []
s = Session()
ptrnames = s.dns(source.reverse_pointer, 'PTR')
for name in ptrnames:
if isinstance(source, ipaddress.IPv4Address):
ips = s.dns(name, 'A')
for ip in ips:
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(unicode(ip, 'UTF-8'))
if ip == source:
results.append(name)
return results
def match4(self, source):
'''Is the source IP related to a IPv4 address/network in the dataset'''
match = False
matchone = False
negativeone = False
matchcidr = False
negativecidr = False
for item in self.dataset:
if item.isipv4:
if source == item.item:
matchone = True
negativeone = item.negative
elif item.isipv4cidr:
if source in item.item:
matchcidr = True
negativecidr = item.negative
if matchcidr and not negativecidr:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def match6(self, source):
'''Is the source IP realted to a IPv6 address/network in the dataset'''
match = False
matchone = False
negativeone = False
matchcidr = False
negativecidr = False
for item in self.dataset:
if item.isipv6:
if source == item.item:
matchone = True
negativeone = item.negative
elif item.isipv6cidr:
if source in item.item:
matchcidr = True
negativecidr = item.negative
if matchcidr and not negativecidr:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def dump(self):
for item in self.dataset:
print 'name: {0} ip4: {1} cidr4: {2} ip6: {3} cidr6: {4} host: {5} domain: {6} negat: {7} type: {8}'.format(item.item,
item.isipv4, item.isipv4cidr, item.isipv6, item.isipv6cidr, item.ishostname, item.isdomain,
item.negative, type(item.item))
####################################################################
def _processConfigFile(filename = None, configdata = None, useSyslog = 1,
useStderr = 0):
@@ -82,8 +248,14 @@ def _find_boolean(item):
else:
raise dkim.ParameterError()
return item
####################
def _calculate_authserv_id(as_id):
"""Determine AuthservID if needed"""
if as_id == 'HOSTNAME':
as_id = socket.gethostname()
return as_id
####################
def _dataset_to_list(dataset):
"""Convert a dataset (as defined in dkimpymilter.8) and return a python
list of values."""
@@ -118,6 +290,19 @@ def _dataset_to_list(dataset):
return [dataset[4:].strip().strip(',')]
else:
return [dataset.strip().strip(',')]
if dataset[-3:] == '.db' or dataset[:3] == 'db:':
# This is a Sleepycat (Oracle) DB dataset
import whichdb # Will need rewriting someday for python3
if dataset[-3:] == '.db':
dbname = dataset
elif dataset[:3] == 'db:':
dbname = dataset[3:]
else:
raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset)))
if whichdb.whichdb(dbname) != 'dbhash':
raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset)))
#TODO replace this with code to use db maps
raise dkim.ParameterError('Unsupported dataset db dataset not yet used: {0}'.format(type(dataset)))
raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset)))
@@ -148,6 +333,9 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
'Selector' : 'str',
'SelectorEd25519': 'str',
'Canonicalization' : 'str',
'InternalHosts' : 'dataset',
'InternalHostsObj': 'bool',
'DiagnosticDirectory' : 'str'
}
# check to see if it's a file
@@ -204,5 +392,10 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion))
configData[name] = conversion(value)
fp.close()
try:
configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID'])
configData['InternalHostsObj'] = HostsDataset(configData['InternalHosts'])
except:
pass
return(configData)
+168
View File
@@ -0,0 +1,168 @@
## @package dnsplug
# Provide a higher level interface to pydns or dnspython (or other provider).
# NOT RELEASED: this is a proposed API and implementation.
# Goals - work with both pydns and dnspython (and possibly other libraries)
# at a simplied level.
# TODO:
# 1. map exceptions to common dnsplug.DNSError exception (with
# original exception saved as a member).
# 2. include dict based implementation (handy for test suites)
# 3. move implementations to subpackages to enable autoselect on first call.
## Maximum number of CNAME records to follow
MAX_CNAME = 10
## Lookup DNS records by label and RR type.
# The response can include records of other types that the DNS
# server thinks we might need. FIXME: empty result
# could mean NXDOMAIN or NOANSWER.
# @param name the DNS label to lookup
# @param qtype the name of the DNS RR type to lookup
# @param tcpfallback if False, raise exception instead of TCP fallback
# @return a list of ((name,type),data) tuples
def DNSLookup(name, qtype, tcpfallback=True, timeout=30):
raise NotImplementedError('No supported dns library found')
class Session(object):
"""A Session object has a simple cache with no TTL that is valid
for a single "session", for example an SMTP conversation."""
def __init__(self):
self.cache = {}
## Additional DNS RRs we can safely cache.
# We have to be careful which additional DNS RRs we cache. For
# instance, PTR records are controlled by the connecting IP, and they
# could poison our local cache with bogus A and MX records.
# Each entry is a tuple of (query_type,rr_type). So for instance,
# the entry ('MX','A') says it is safe (for milter purposes) to cache
# any 'A' RRs found in an 'MX' query.
SAFE2CACHE = frozenset((
('MX','MX'), ('MX','A'),
('CNAME','CNAME'), ('CNAME','A'),
('A','A'),
('AAAA','AAAA'),
('PTR','PTR'),
('NS','NS'), ('NS','A'),
('TXT','TXT'),
('SPF','SPF')
))
## Cached DNS lookup.
# @param name the DNS label to query
# @param qtype the query type, e.g. 'A'
# @param cnames tracks CNAMES already followed in recursive calls
def dns(self, name, qtype, cnames=None):
"""DNS query.
If the result is in cache, return that. Otherwise pull the
result from DNS, and cache ALL answers, so additional info
is available for further queries later.
CNAMEs are followed.
If there is no data, [] is returned.
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType)
"""
result = self.cache.get( (name, qtype) )
cname = None
if not result:
safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype):
if k == (name, 'CNAME'):
cname = v
if (qtype,k[1]) in safe2cache:
self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), [])
if not result and cname:
if not cnames:
cnames = {}
elif len(cnames) >= MAX_CNAME:
#return result # if too many == NX_DOMAIN
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname
if cname in cnames:
raise DNSError, 'CNAME loop'
result = self.dns(cname, qtype, cnames=cnames)
return result
def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
try:
# FIXME: To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse
# a req object stored in a Session.
req = DNS.DnsRequest(name, qtype=qtype, timeout=timeout)
resp = req.req()
#resp.show()
# key k: ('wayforward.net', 'A'), value v
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
# A RR as dotted quad. For consistency, this driver should
# return both as binary string.
#
if resp.header['tc'] == True:
if not tcpfallback:
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, x:
raise DNS.DNSError, 'TCP Fallback error: ' + str(x)
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError, x:
raise DNS.DNSError, 'DNS: ' + str(x)
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
retVal = []
try:
# FIXME: how to disable TCP fallback in dnspython if not tcpfallback?
answers = dns.resolver.query(name, qtype)
for rdata in answers:
if qtype == 'A' or qtype == 'AAAA':
retVal.append(((name, qtype), rdata.address))
elif qtype == 'MX':
retVal.append(((name, qtype), (rdata.preference, rdata.exchange)))
elif qtype == 'PTR':
retVal.append(((name, qtype), rdata.target.to_text(True)))
elif qtype == 'TXT' or qtype == 'SPF':
retVal.append(((name, qtype), rdata.strings))
except dns.resolver.NoAnswer:
pass
except dns.resolver.NXDOMAIN:
pass
return retVal
try:
# prefer dnspython (the more complete library)
import dns
import dns.resolver # http://www.dnspython.org
import dns.exception
if not hasattr(dns.rdatatype,'SPF'):
# patch in type99 support
dns.rdatatype.SPF = 99
dns.rdatatype._by_text['SPF'] = dns.rdatatype.SPF
DNSLookup = DNSLookup_dnspython
except:
import DNS # http://pydns.sourceforge.net
if not hasattr(DNS.Type, 'SPF'):
# patch in type99 support
DNS.Type.SPF = 99
DNS.Type.typemap[99] = 'SPF'
DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
# Fails on Mac OS X? Add domain to /etc/resolv.conf
DNS.DiscoverNameServers()
DNSLookup = DNSLookup_pydns
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)
+42
View File
@@ -16,6 +16,39 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
def fold(header):
"""Fold a header line into multiple crlf-separated lines at column 72.
Borrowed from dkimpy and updated to only add \n instead of \r\n because
that's what the milter protocol wants.
>>> text(fold(b'foo'))
'foo'
>>> text(fold(b'foo '+b'foo'*24).splitlines()[0])
'foo '
>>> text(fold(b'foo'*25).splitlines()[-1])
' foo'
>>> len(fold(b'foo'*25).splitlines()[0])
72
"""
i = header.rfind(b"\r\n ")
if i == -1:
pre = b""
else:
i += 3
pre = header[:i]
header = header[i:]
maxleng = 72
while len(header) > maxleng:
i = header[:maxleng].rfind(b" ")
if i == -1:
j = maxleng
else:
j = i + 1
pre += header[:j] + b"\n "
header = header[j:]
namelen = 0
return pre + header
def user_group(userid):
"""Return user and group from UserID"""
import grp
@@ -86,6 +119,15 @@ def write_pid(milterconfig):
try:
f = open(milterconfig.get('PidFile'), 'w')
except IOError as e:
if str(e)[:35] == '[Errno 2] No such file or directory':
piddir = milterconfig.get('PidFile').rsplit('/', 1)[0]
os.mkdir(piddir)
user, group = user_group(milterconfig.get('UserID'))
os.chown(piddir, user, group)
f = open(milterconfig.get('PidFile'), 'w')
if milterconfig.get('Syslog'):
syslog.syslog('Missing pid dir created: {0}'.format(piddir))
else:
if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'.format(milterconfig.get('PidFile'), e))
raise
+5 -15
View File
@@ -156,7 +156,7 @@ values, or to a file that contains them, or a database containing the data.
Some data sets require that the value contain more than one entry. How this
is done depends on which data set type is used. Not all these datasets are
currently used by dkimp-milter. See
currently used by dkimpy-milter. See
.B dkimpy-milter.conf(5)
for details about specific options and which dataset types they use.
@@ -169,17 +169,7 @@ one per line. If a line contains whitespace-separated values, then the
line is presumed to define a key and its corresponding value. Blank lines
are ignored, and the hash ("#") character denotes the start of a comment.
If a value contains multiple entries, the entries should be separated by
colons. [Not implemented yet]
.TP
.I b)
If the string begins with "refile:", then the remainder of the string is
presumed to specify a file that contains a set of patterns, one per line,
and their associated values. The pattern is taken as the start of the line
to the first whitespace, and the portion after that whitespace is taken as
the value to be used when that pattern is matched. Patterns are simple
wildcard patterns, matching all text except that the asterisk ("*") character
is considered a wildcard. If a value contains multiple entries, the entries
should be separated by colons. [Not implemented yet]
colons.
.TP
.I c)
If the string begins with "db:" and the program was compiled with
@@ -196,11 +186,11 @@ is compiled in). [Not implemented yet]
.TP
.I i)
If the string contains none of these prefixes but starts with a slash ("/")
character, it is presumed to be a flat file as described above. [Not implemented yet]
character, it is presumed to be a flat file as described above.
.TP
.I j)
If the string begins with "csl:", the string is treated as a comma-separated
list as described in m) below. [Not implemented yet]
list as described in m) below.
.TP
.I l)
If the string begins with "mdb:", it refers to a directory that contains
@@ -279,7 +269,7 @@ proposal, and Cisco's
.B Internet Identified Mail
(IIM) proposal.
.SH VERSION
This man page covers version 0.9.2 of
This man page covers version 0.9.4 of
.I dkimpy-milter.
.SH COPYRIGHT
Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights
+4 -10
View File
@@ -215,18 +215,12 @@ The value may include two different canonicalizations separated by a
slash ("/") character, in which case the first will be applied to the
header and the second to the body.
.TP
.I Diagnostics (Boolean)
Requests the inclusion of "z=" tags in signatures, which encode the
original header field set for use by verifiers when diagnosing verification
failures. Not recommended for normal operation. [dkimpy-milter specific: also
increases the verbosity of Syslog logging if enabled.]
.TP
.I DiagnosticDirectory (string)
Directory into which to write diagnostic reports when message verification
fails on a message bearing a "z=" tag. If not set (the default), these files
are not generated.
fails. If not set (the default), these files are not generated. [Unlike
OpenDKIM, this applies to all messages, not just on messages bearing a "z=" tag
because dkimpy does not yet support "z=".]
.TP
.I Domain (dataset)
@@ -374,7 +368,7 @@ Log via calls to
using the named facility. The facility names are the same as the ones
allowed in
.I syslog.conf(5).
The default is "mail". [Hardcoded to default for now]
The default is "mail".
.TP
.I SyslogSuccess (Boolean)
+1 -1
View File
@@ -55,6 +55,6 @@ setup(
(os.path.join('/lib', 'systemd', 'system'),
['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'),
['system/dkimpy-milter'])],
install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl'],
install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dns'],
zip_safe = False,
)