Compare commits

...

58 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
Scott Kitterman 51c8fdcb6c Bump version to 1.0.1, update TODO, set release date 2019-02-11 15:14:11 -05:00
Scott Kitterman aa4dadc22f * Reorder milter start and dropping privileges so permissions on Unix socket
are correct (LP: 1797720)
2019-02-11 15:09:34 -05:00
Scott Kitterman b1abbf9d61 - Make domain checks case insensitive for determining if signing should be
done (LP: #1815311)
2019-02-11 14:55:35 -05:00
Scott Kitterman ea2b612e8d - Add information on Ed25519 key creation to README (LP: #1815313) 2019-02-11 14:23:55 -05:00
Scott Kitterman 5945e818ca - Add additional Sendmail configuration information to README from OpenDKIM
update based on input from Дилян Палаузов (LP: #1801619)
2019-02-11 13:32:37 -05:00
Scott Kitterman f38fed3bee Rip out unused whichbd module in preparation for python3 port 2019-02-11 03:16:53 -05:00
Scott Kitterman 06948b3dbc Update references in man/dkimpy-milter.8 2019-02-09 22:20:01 -05:00
Scott Kitterman e951ab6c5e Remove obsolete .IX macro from man pages
Conflicts:
	man/dkimpy-milter.conf.5
2019-02-09 22:19:41 -05:00
Scott Kitterman 03c86a2b08 Fix grammar error in README 2019-02-09 22:18:11 -05:00
Scott Kitterman 2cda1758c1 Fix spelling error in README 2019-02-09 22:17:53 -05:00
Scott Kitterman a188bd3960 Deleted reference to obsolete syslog target in unit file 2018-05-11 14:31:15 -04:00
Scott Kitterman c91a12f0d1 Documentation updates for 1.0.0 release 2018-05-11 14:29:15 -04:00
Scott Kitterman 286ffbb6c9 Add release date for 0.9.7 to CHANGES 2018-03-19 01:07:52 -04:00
Scott Kitterman ec3252c367 - Minor sysv init improvments 2018-03-15 23:59:03 -04:00
Scott Kitterman a2ff03727d - Put version directly in setup.py and do not import dkimpy_milter to ease
install via pip
2018-03-15 23:44:31 -04:00
Scott Kitterman af4b05e242 - Add missing documentation key to system/dkimpy-milter.service 2018-03-15 20:49:35 -04:00
Scott Kitterman 6509eaad35 - Made sysv init executable 2018-03-15 20:44:16 -04:00
Scott Kitterman f9483fea8c - Added protection for malformed From addresses. If the From does not at
least have an '@' in the address, then the signing domain is not extracted
   and the message will not be signed
2018-03-15 20:42:49 -04:00
Scott Kitterman 7a3a7bfb43 Bump version to 0.9.6 2018-03-12 22:08:07 -04:00
Scott Kitterman 8a0e1bdd97 - Fixed typo in path for fallback location of the config file if one is not
provided
2018-03-12 22:03:45 -04:00
Scott Kitterman e3005aa723 Move OversignHeaders up earlier on TODO. 2018-03-12 22:03:36 -04:00
Scott Kitterman 45d3ba13ca Added more to README about first run with systemd 2018-03-11 00:42:22 -05:00
Scott Kitterman f05309437f Fix merge conflict 2018-03-11 00:28:08 -05:00
Scott Kitterman d4499f6990 Fixed typo in package installation section of README 2018-03-11 00:27:19 -05:00
Scott Kitterman 7d87309f4b Fixed typo in package installation section of README 2018-03-11 00:24:39 -05:00
Scott Kitterman 1d8c309da9 Fix setup.py install locations so they are installed correctly and drop unneeded README changes. 2018-03-10 20:06:21 -05:00
Scott Kitterman 4d5961e4d5 Bump version 2018-03-10 19:52:29 -05:00
Scott Kitterman 59448e8e57 - Add information to README about manually putting init scripts in the right
locations
2018-03-10 19:51:29 -05:00
Scott Kitterman 695de0db14 - Add conf file location to systemd unit file 2018-03-10 19:43:37 -05:00
Scott Kitterman dfd6fa68c3 Changelog: release 0.9.5 (Beta 1) 2018-03-10 19:06:55 -05:00
Scott Kitterman 86eb152f93 Enhanced signature verification logging to provide more useful information, added signing success logging, and more PEP 8 2018-03-10 19:02:37 -05:00
Scott Kitterman 126966e110 - Update Authentication Results result comment not to mention key size for
ed25519 signatures, since it's irrelevant
2018-03-10 18:18:01 -05:00
Scott Kitterman 5d8d47cd52 - Fixed install_requires so either dnspython (preferred if neither is
installed) or PyDNS satisfies the install requirements
2018-03-10 17:49:22 -05:00
Scott Kitterman 1843ca6244 - Added support for SyslogSuccess option
- Rationalized logging to be much less verbose unless SyslogSuccess or
   debugLevel are set - default is generally start/stop/errors only
2018-03-10 16:06:22 -05:00
Scott Kitterman f9358d594c Delete unused import 2018-03-10 15:36:40 -05:00
Scott Kitterman a8aa422b03 Post pep-8 cleanup 2018-03-10 15:34:56 -05:00
Scott Kitterman 9836f2c9c2 Update TODO 2018-03-10 03:00:59 -05:00
Scott Kitterman 70606ac58c pep8 and a few other cleanups 2018-03-10 02:45:35 -05:00
Scott Kitterman 6348bdcdc7 Cleanup, indentation, pyflakes 2018-03-10 00:52:45 -05:00
Scott Kitterman fd39384e78 Fix for DiagnosticDirectory 2018-03-09 23:49:57 -05:00
Scott Kitterman 924c96d555 - Added example in README to show use of MacroList* to separate inbound and
outbound mail streams
2018-03-09 22:50:07 -05:00
Scott Kitterman efeabd19d3 Added support for MacroListVerify option 2018-03-09 22:39:55 -05:00
Scott Kitterman a9b8a44bfc Add support for MacroList option 2018-03-09 21:53:58 -05:00
Scott Kitterman daaa6aada7 Fix option name typo in man/dkimpy-milter.conf.5 2018-03-09 20:45:57 -05:00
Scott Kitterman e795db7c69 Start 0.9.5: Beta 1 (updated Alpha -> Beta warning in README and trove classifiers) 2018-03-09 18:08:42 -05:00
11 changed files with 734 additions and 404 deletions
+65
View File
@@ -1,3 +1,68 @@
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)
- 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
are correct (LP: 1797720)
- Make domain checks case insensitive for determining if signing should be
done (LP: #1815311)
- Add additional Sendmail configuration information to README from OpenDKIM
update based on input from Дилян Палаузов (LP: #1801619)
- Add information on Ed25519 key creation to README (LP: #1815313)
1.0.0 2018-05-11
- Minor documentation updates
- Deleted reference to obsolete syslog target in unit file
0.9.7 2018-03-19
- Made sysv init executable
- Add missing documentation key to system/dkimpy-milter.service
- Put version directly in setup.py and do not import dkimpy_milter to ease
install via pip
- Minor sysv init improvments
0.9.6 2018-03-13
- Fixed typo in package installation section of README
- Added more to README about first run with systemd
- Fixed typo in path for fallback location of the config file if one is not
provided
- Added protection for malformed From addresses. If the From does not at
least have an '@' in the address, then the signing domain is not extracted
and the message will not be signed
0.9.5.1 2018-03-10
- Add conf file location to systemd unit file
- Fix setup.py install locations so they are installed correctly
0.9.5 2018-03-10
- Beta 1 (updated Alpha -> Beta warning in README and trove classifiers)
- Added support for MacroList option
- Added support for MacroListVerify option
- Added example in README to show use of MacroList* to separate inbound and
outbound mail streams
- Added support for SyslogSuccess option (both signing and verifying)
- Rationalized logging to be much less verbose unless SyslogSuccess or
debugLevel are set - default is generally start/stop/errors only
- Fixed install_requires so either dnspython (preferred if neither is
installed) or PyDNS satisfies the install requirements
- Updated Authentication Results result comment not to mention key size for
ed25519 signatures, since it's irrelevant
- Enhanced signature verification logging to provide more useful information
0.9.4 2018-03-09 0.9.4 2018-03-09
- Create PID directory if it is missing - Create PID directory if it is missing
- Fix crash when verifying if domain for signing was not set - Fix crash when verifying if domain for signing was not set
+169 -22
View File
@@ -1,11 +1,17 @@
This is a DKIM signing and verification milter. In theory it works with both OVERVIEW
Postfix and Sendmail, but the author has zero experience with Sendmail, so ========
reports of success/failure with Sendmail and patches are welcom.
This is a DKIM signing and verification milter. It has been tested with both
Postfix and Sendmail.
The configuration file is designed to be compatible with OpenDKIM, but only The configuration file is designed to be compatible with OpenDKIM, but only
a subset of OpenDKIM options are supported. If an unsupported option is a subset of OpenDKIM options are supported. If an unsupported option is
specified, an error will be raised. specified, an error will be raised.
INSTALLATION
===========
This package includes a default configuration file and man pages. For those This package includes a default configuration file and man pages. For those
to be installed when installing using setup.py, the following incantation is to be installed when installing using setup.py, the following incantation is
required because setuptools developers decided not being able to do this by required because setuptools developers decided not being able to do this by
@@ -13,11 +19,11 @@ default is a feature:
python 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 Squueze), all dependencies are For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
available in either the main or backports repositories: available in either the main or backports repositories:
[sudo] apt install python-milter python-nacl pthon-ipaddress python-dnspython [sudo] apt install python-milter python-nacl python-ipaddress python-dnspython
[sudo] apt install -t squeeze-backports python-authres python-dkim [sudo] apt install -t stretch-backports python-authres python-dkim
The preferred method of installation is from PyPi using pip (if distribution The preferred method of installation is from PyPi using pip (if distribution
packages are not available): packages are not available):
@@ -25,17 +31,61 @@ packages are not available):
[sudo] pip install dkimpy_milter [sudo] pip install dkimpy_milter
Using pip will cause required packages to be installed via easy_install if they Using pip will cause required packages to be installed via easy_install if they
have not been previously installed. 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 dsitribution/OS
packages and then pip install dkimpy_milter.
The milter will work with either pydns (DNS) or dnspython (dns), preferring The milter will work with either pydns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with dnspython if both are available. The dkimpy DKIM module also works with
either. either.
SETUP
====
SIGNING KEYS
============
In order to create DKIM signatures, a private key must be available. Signing
keys should be protected (owned by root:root with permissions 600 in a
directory that is not world readable). Different keys are required for RSA
and (if used) Ed25519.
RSA
===
Both public and private keys for RSA have standard formats and there are many
tools available to create them. Keys must (RFC 8302) have a minimum size of
1024 bits and should have a size of at least 2048 bits. The dknewkey script
that is provided with dkimpy is one such tool:
dknewkey exampleprivkey
will produce both the private key file (.key suffix) and a file with the DKIM
public key record to be published DNS (.dns suffix). RSA is the default key
type. 2048 bits is the default key size.
ED25519
=======
There is no standardized non-binary representation for Ed25519 private keys,
so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools
must be used to be compatible. The same dknewkey script support Ed25519:
dknewkey --ktype ed25519 anothernewkey
will provide both the private key file (.key suffix) and a file with the DKIM
public key record to be published DNS (.dns suffix). Ed25519 keys do not have
variable bit lengths.
MTA INTEGRATION
==============
Both a systemd unit file and a sysv init file are provided. Both make 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 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 used, they will need to be updated. The sysv init file uses start-stop-deamon
untested, since the developers are not using sysv init. Feedback/patches from Debian. It is not portable to systems without that available.
welcome.
The dkimpy-milter drops priviledges after setup to the user/group specified in 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. UserID. During initial setup, this system user needs to be manually created.
@@ -48,10 +98,19 @@ As an example, using the default dkimpy-user on Debian, the command would be:
Since /var/run or /run is sometimes on a tempfs, if the PID file directory is Since /var/run or /run is sometimes on a tempfs, if the PID file directory is
missing, the milter will create it on startup. missing, the milter will create it on startup.
To start dkimpy-milter with systemd for the first time, you will need to take
the following steps:
[sudo] systemctl daemon-reload
[sudo] systemctl enable dkimpy-milter
[sudo] systemctl start dkimpy-milter
[sudo] systemctl status dkimpy-milter (to verify it started correctly)
As with all milters, dkimpy-milter needs to be integrated with your MTA of As with all milters, dkimpy-milter needs to be integrated with your MTA of
choice (Sendmail or Postfix). choice (Sendmail or Postfix).
For Sendmail: SENDMAIL
========
Configuration is very similar to opendkim, but needs some adjustment for Configuration is very similar to opendkim, but needs some adjustment for
dkimpy-milter. Here's an example configuration line to include in your dkimpy-milter. Here's an example configuration line to include in your
@@ -67,12 +126,64 @@ Milter support should be present by default in most versions of sendmail
these days, but if not included in your Sendmail build, see: these days, but if not included in your Sendmail build, see:
http://www.elandsys.com/resources/sendmail/milter.html http://www.elandsys.com/resources/sendmail/milter.html
For Postfix: ISSUES USING SENDMAIL TO SIGN AND VERIFY
========================================
When using the sendmail MTA in both signing and verifying mode, there are
a few issues of which to be aware that might cause operational problems
and deserve consideration.
(a) When the MTA will be used for relaying emails, e.g. delivering to other
hosts using the aliases mechanism, it is important not to break
signatures inserted by the original sender. This is particularly sensitive
particular when the sending domain has published a "reject" DMARC policy.
By default, sendmail quotes to address header fields when there are no
quotes and the display part of the address contains a period or an
apostrophe. However, opendkim only sees the raw, unmodified form of
the header field, and so the content that gets verified and what gets
signed will not be the same, guaranteeing the attached signature is not
valid.
To direct sendmail not to modify the headers, add this to your sendmail.mc:
conf(`confMUST_QUOTE_CHARS', `')
(b) As stated in sendmail's KNOWNBUGS file, sendmail truncates header field
values longer than 256 characters, which could mean truncating the domain
of a long From: header field value and invalidating the signature.
You may wish to consider increasing MAXNAME in sendmail/conf.h to mitigate
changing the messages and invalidating their signatures. This change
requires recompiling sendmail.
(c) Similar to (a) above, sendmail may wrap very long single-line recipient
fields for presentation purposes; for example:
To: very long name <a@example.org>,anotherloo...ong name b <b@example.org>
...might be rewritten as:
To: very long name <a@example.org>,
anotherloo...ong name b <b@example.org>
This rewrite is also done after opendkim has seen the message, meaning
the signature opendkim attaches to the message does not match the
content it signed. There is not a known configuration change to
mitigate this mutation.
The only known mechanism for dealing with this is to have distinct
instances of opendkim do the verifying (inbound) and signing (outbound)
so that the version that arrives at the signing instance is already
in the rewritten form, guaranteeing the input and output are the same
and thus the signature matches the payload.
POSTFIX
=======
Integration of dkimpy-milter into Postfix is like any milter (See Postfix's 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 README_FILES/MILTER_README). Here's an example master.cf excerpt that talks
two dkimpy-milter instances, one configured for signing and one configured for to two dkimpy-milter instances, one configured for signing and one configured
verification: for verification:
smtp inet n - - - - smtpd smtp inet n - - - - smtpd
... ...
@@ -86,10 +197,46 @@ submission inet n - - - - smtpd
These need to match the Socket value for each dkimpy-milter instance. These need to match the Socket value for each dkimpy-milter instance.
The python DKIM library, dkimpy, requires the entire message being signed or Care is required to segregate outbound mail to be signed and inbound mail to
verified to be in memory, so dkimpy-milter does not write messages out to a temp be verified. The above example uses two instances of dkimpy-milter to do
file. This may impact performance on low-memory systems. this. There are many possible ways. Here is another example using milter
macros to keep the mail streams segregated:
WARNING: This is an alpha grade release to support interoperability testing with Postfix master.cf:
Ed25519 signatures and basic functionality. It is known to be incomplete and
not suitable for general use. smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
-o milter_macro_daemon_name=VERIFYING
...
submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
...
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_milters=inet:localhost:8891
...
Dkimpy-milter.conf:
...
Mode sv
MacroList dameon_name|ORIGINATING
MacroListVerify daemon_name|VERIFYING
...
NOTES
=====
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.
DKIM with Ed25519 signatures are described in RFC 8463. Version 1.0.0 and
later support Ed25519 signing and verification. RFC 8301 removed rsa-sha1
from DKIM. dkimpy-milter does not sign with rsa-sha1, but still considers
rsa-sha1 signatures as valid for verification because they are still in
common use and are not known to be cryptographically broken.
+14 -10
View File
@@ -34,16 +34,21 @@ DiagnosticDirectory implemented verified
InternalHosts implemented verified InternalHosts implemented verified
0.9.5 (Beta) 0.9.5 (Beta)
MacroList implemented verified
MacroListVerify implemented verified
SyslogSuccess SyslogSuccess implemented verified
1.0.0 1.0.0
Convert dkim-milter-python config No additional features
No additional features planned
Plannedataset type support: 1.0.1
db:/.db Bug fix only, improved documentation
1.1.0 (planned)
Port to Python 3
Subdomain support
Planned dataset type support (if needed):
mdb: mdb:
Considered for near-term feature release Considered for near-term feature release
@@ -51,10 +56,10 @@ Considered for near-term feature release
AlwaysAddARHeader AlwaysAddARHeader
ChangeRootDirectory ChangeRootDirectory
ClockDrift (requires dkimpy change) ClockDrift (requires dkimpy change)
DNSTimeout (requires dkmpy change) DNSTimeout (requires dkimpy change)
MacroList
MilterDebug MilterDebug
MinimumKeyBits MinimumKeyBits
OversignHeaders (may require dkimpy changes)
PeerList PeerList
SignatureAlgorithm SignatureAlgorithm
@@ -85,7 +90,6 @@ On-InternalError
On-KeyNotFound On-KeyNotFound
On-NoSignature On-NoSignature
On-SignatureError On-SignatureError
OversignHeaders
RemoveARAll RemoveARAll
RemoveARFrom RemoveARFrom
RemoveOldSignatures RemoveOldSignatures
+300 -224
View File
@@ -25,247 +25,322 @@ import sys
import syslog import syslog
import Milter import Milter
import dkim import dkim
from dkim.dnsplug import get_txt
from dkim.util import parse_tag_value
import authres import authres
import os import os
import tempfile import tempfile
import StringIO import StringIO
import re import re
from Milter.config import MilterConfigParser from Milter.utils import parse_addr, parseaddr
from Milter.utils import iniplist,parse_addr,parseaddr
import dkimpy_milter.config as config import dkimpy_milter.config as config
from dkimpy_milter.util import drop_privileges from dkimpy_milter.util import drop_privileges
from dkimpy_milter.util import setExceptHook from dkimpy_milter.util import setExceptHook
from dkimpy_milter.util import write_pid from dkimpy_milter.util import write_pid
from dkimpy_milter.util import read_keyfile from dkimpy_milter.util import read_keyfile
from dkimpy_milter.util import own_socketfile
from dkimpy_milter.util import fold from dkimpy_milter.util import fold
__version__ = "0.9.4" __version__ = "1.0.1"
FWS = re.compile(r'\r?\n[ \t]+') FWS = re.compile(r'\r?\n[ \t]+')
class dkimMilter(Milter.Base): class dkimMilter(Milter.Base):
"Milter to check and sign DKIM. Each connection gets its own instance." "Milter to check and sign DKIM. Each connection gets its own instance."
def __init__(self): def __init__(self):
self.mailfrom = None self.mailfrom = None
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
# we don't want config used to change during a connection # we don't want config used to change during a connection
self.conf = milterconfig self.conf = milterconfig
self.privatersa = privateRSA self.privatersa = privateRSA
self.privateed25519 = privateEd25519 self.privateed25519 = privateEd25519
self.fp = None self.fp = None
self.fdomain = ''
@Milter.noreply @Milter.noreply
def connect(self,hostname,unused,hostaddr): def connect(self, hostname, unused, hostaddr):
self.internal_connection = False self.internal_connection = False
self.hello_name = None self.external_connection = False
# sometimes people put extra space in sendmail config, so we strip self.hello_name = None
self.receiver = self.getsymval('j').strip() # sometimes people put extra space in sendmail config, so we strip
try: self.receiver = self.getsymval('j').strip()
self.AuthservID = milterconfig['AuthservID'] try:
except: self.AuthservID = milterconfig['AuthservID']
self.AuthservID = self.receiver except:
if hostaddr and len(hostaddr) > 0: self.AuthservID = self.receiver
ipaddr = hostaddr[0] if hostaddr and len(hostaddr) > 0:
if milterconfig['InternalHostsObj']: ipaddr = hostaddr[0]
if milterconfig['InternalHostsObj'].match(ipaddr): if milterconfig['IntHosts']:
self.internal_connection = True if milterconfig['IntHosts'].match(ipaddr):
else: ipaddr = '' self.internal_connection = True
self.connectip = ipaddr else:
if self.internal_connection: ipaddr = ''
connecttype = 'INTERNAL' self.connectip = ipaddr
else: if milterconfig.get('MacroList') and not self.internal_connection:
connecttype = 'EXTERNAL' macrolist = milterconfig.get('MacroList')
if milterconfig.get('Syslog'): for macro in macrolist:
syslog.syslog("connect from {0} at {1} {2}".format(hostname,hostaddr,connecttype)) macroname = macro.split('|')[0]
return Milter.CONTINUE macroname = '{' + macroname + '}'
macroresult = self.getsymval(macroname)
if ((len(macro.split('|')) == 1 and macroresult) or macroresult
in macro.split('|')[1:]):
self.internal_connection = True
if milterconfig.get('MacroListVerify'):
macrolist = milterconfig.get('MacroListVerify')
for macro in macrolist:
macroname = macro.split('|')[0]
macroname = '{' + macroname + '}'
macroresult = self.getsymval(macroname)
if ((len(macro.split('|')) == 1 and macroresult) or macroresult
in macro.split('|')[1:]):
self.external_connection = True
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 1:
syslog.syslog("connect from {0} at {1} {2}"
.format(hostname, hostaddr, connecttype))
return Milter.CONTINUE
# multiple messages can be received on a single connection # multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message. # of each message.
@Milter.noreply @Milter.noreply
def envfrom(self,f,*str): def envfrom(self, f, *str):
if milterconfig.get('Syslog'): if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
syslog.syslog("mail from: {0} {1}".format(f,str)) syslog.syslog("mail from: {0} {1}".format(f, str))
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.mailfrom = f self.mailfrom = f
t = parse_addr(f) t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower() if len(t) == 2:
self.canon_from = '@'.join(t) t[1] = t[1].lower()
self.user = self.getsymval('{auth_authen}') self.canon_from = '@'.join(t)
self.has_dkim = 0 self.has_dkim = 0
self.author = None self.author = None
self.arheaders = [] self.arheaders = []
self.arresults = [] self.arresults = []
'''if self.user: return Milter.CONTINUE
# Very simple SMTP AUTH policy by default:
# any successful authentication is considered INTERNAL
self.internal_connection = True
auth_type = self.getsymval('{auth_type}')
ssl_bits = self.getsymval('{cipher_bits}')
if milterconfig.get('Syslog'):
syslog.syslog(
"SMTP AUTH:",self.user,"sslbits =",ssl_bits, auth_type,
"ssf =",self.getsymval('{auth_ssf}'), "INTERNAL"
)
# Detailed authorization policy is configured in the access file below.
self.arresults.append(
authres.SMTPAUTHAuthenticationResult(result = 'pass',
result_comment = auth_type+' sslbits='+ssl_bits, smtp_auth = self.user)
)'''
return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def header(self,name,val): def header(self, name, val):
lname = name.lower() lname = name.lower()
if lname == 'dkim-signature': if lname == 'dkim-signature':
if milterconfig.get('Syslog'): if (milterconfig.get('Syslog') and
syslog.syslog("{0}: {1}".format(name,val)) milterconfig.get('debugLevel') >= 1):
self.has_dkim += 1 syslog.syslog("{0}: {1}".format(name, val))
if lname == 'from': self.has_dkim += 1
fname,self.author = parseaddr(val) if lname == 'from':
self.fdomain = self.author.split('@')[1] fname, self.author = parseaddr(val)
if milterconfig.get('Syslog'): try:
syslog.syslog("{0}: {1}".format(name,val)) self.fdomain = self.author.split('@')[1].lower()
elif lname == 'authentication-results': except IndexError as er:
self.arheaders.append(val) pass # self.author was not a proper email address
if self.fp: if (milterconfig.get('Syslog') and
self.fp.write("%s: %s\n" % (name,val)) milterconfig.get('debugLevel') >= 1):
return Milter.CONTINUE syslog.syslog("{0}: {1}".format(name, val))
elif lname == 'authentication-results':
self.arheaders.append(val)
if self.fp:
try:
self.fp.write("%s: %s\n" % (name, val))
except:
# Don't choke on header fields with garbage in them.
pass
return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def eoh(self): def eoh(self):
if self.fp: if self.fp:
self.fp.write("\n") # terminate headers self.fp.write("\n") # terminate headers
self.bodysize = 0 self.bodysize = 0
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def body(self,chunk): # copy body to temp file def body(self, chunk): # copy body to temp file
if self.fp: if self.fp:
self.fp.write(chunk) # IOError causes TEMPFAIL in milter self.fp.write(chunk) # IOError causes TEMPFAIL in milter
self.bodysize += len(chunk) self.bodysize += len(chunk)
return Milter.CONTINUE return Milter.CONTINUE
def eom(self): def eom(self):
if not self.fp: if not self.fp:
return Milter.ACCEPT # no message collected - so no eom processing return Milter.ACCEPT # no message collected - so no eom processing
# Remove existing Authentication-Results headers for our authserv_id # Remove existing Authentication-Results headers for our authserv_id
for i,val in enumerate(self.arheaders,1): for i, val in enumerate(self.arheaders, 1):
# FIXME: don't delete A-R headers from trusted MTAs # FIXME: don't delete A-R headers from trusted MTAs
try: try:
ar = authres.AuthenticationResultsHeader.parse_value(FWS.sub('',val)) ar = (authres.AuthenticationResultsHeader
if ar.authserv_id == self.AuthservID: .parse_value(FWS.sub('', val)))
self.chgheader('authentication-results',i,'') if ar.authserv_id == self.AuthservID:
if milterconfig.get('Syslog'): self.chgheader('authentication-results', i, '')
syslog.syslog('REMOVE: {0}'.format(val)) if (milterconfig.get('Syslog') and
except: milterconfig.get('debugLevel') >= 1):
# Don't error out on unparseable AR header fiels syslog.syslog('REMOVE: {0}'.format(val))
pass except:
# Check or sign DKIM # Don't error out on unparseable AR header fiels
self.fp.seek(0) pass
if milterconfig.get('Domain'): # Check and/or sign DKIM
domain = milterconfig.get('Domain') self.fp.seek(0)
else: txt = self.fp.read()
domain = '' if milterconfig.get('Domain'):
if (self.fdomain in domain) and (not milterconfig.get('Mode') == 'v'): domain = milterconfig.get('Domain')
txt = self.fp.read() else:
self.sign_dkim(txt) domain = ''
result = None if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
if (self.has_dkim) and (not self.internal_connection) and (milterconfig.get('Mode') == 'v' or milterconfig.get('Mode') == 'sv'): and not self.external_connection):
txt = self.fp.read() self.sign_dkim(txt)
self.check_dkim(txt) if ((self.has_dkim) and (not self.internal_connection) and
else: (milterconfig.get('Mode') == 'v' or
result = 'none' milterconfig.get('Mode') == 'sv')):
if self.arresults: self.check_dkim(txt)
h = authres.AuthenticationResultsHeader(authserv_id = self.AuthservID, if self.arresults:
results=self.arresults) h = authres.AuthenticationResultsHeader(authserv_id=
h = fold(str(h)) self.AuthservID,
if milterconfig.get('Syslog'): results=self.arresults)
syslog.syslog(str(h)) h = fold(str(h))
name,val = str(h).split(': ',1) if (milterconfig.get('Syslog') and
self.addheader(name,val,0) milterconfig.get('debugLevel') >= 2):
return Milter.CONTINUE syslog.syslog(str(h))
name, val = str(h).split(': ', 1)
self.addheader(name, val, 0)
return Milter.CONTINUE
def sign_dkim(self,txt): def sign_dkim(self, txt):
canon = milterconfig.get('Canonicalization') canon = milterconfig.get('Canonicalization')
canonicalize = [] canonicalize = []
if len(canon.split('/')) == 2: if len(canon.split('/')) == 2:
canonicalize.append(canon.split('/')[0]) canonicalize.append(canon.split('/')[0])
canonicalize.append(canon.split('/')[1]) canonicalize.append(canon.split('/')[1])
else: else:
canonicalize.append(canon) canonicalize.append(canon)
canonicalize.append(canon) canonicalize.append(canon)
syslog.syslog('canonicalize: {0}'.format(canonicalize)) if (milterconfig.get('Syslog') and
try: milterconfig.get('debugLevel') >= 1):
if privateRSA: syslog.syslog('canonicalize: {0}'.format(canonicalize))
d = dkim.DKIM(txt) try:
h = d.sign(milterconfig.get('Selector'), self.fdomain, privateRSA, if privateRSA:
canonicalize=(canonicalize[0], canonicalize[1])) d = dkim.DKIM(txt)
name,val = h.split(': ',1) h = d.sign(milterconfig.get('Selector'), self.fdomain,
self.addheader(name,val.strip().replace('\r\n','\n'),0) privateRSA, canonicalize=(canonicalize[0],
if privateEd25519: canonicalize[1]))
d = dkim.DKIM(txt) name, val = h.split(': ', 1)
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, privateEd25519, self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
canonicalize=(canonicalize[0], canonicalize[1]), signature_algorithm='ed25519-sha256') if (milterconfig.get('Syslog') and
name,val = h.split(': ',1) (milterconfig.get('SyslogSuccess')
self.addheader(name,val.strip().replace('\r\n','\n'),0) or milterconfig.get('debugLevel') >= 1)):
except dkim.DKIMException as x: syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
if milterconfig.get('Syslog'): 'd={3})'.format(self.getsymval('i'),
syslog.syslog('DKIM: {0}'.format(x)) d.signature_fields.get(b'a'),
except Exception as x: d.signature_fields.get(b's'),
if milterconfig.get('Syslog'): d.domain.lower()))
syslog.syslog("sign_dkim: {0}".format(x)) if privateEd25519:
raise d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain,
def check_dkim(self,txt): privateEd25519, canonicalize=(canonicalize[0],
res = False canonicalize[1]),
conf = self.conf signature_algorithm='ed25519-sha256')
for y in range(self.has_dkim): # Verify _ALL_ the signatures name, val = h.split(': ', 1)
d = dkim.DKIM(txt) self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
try: if (milterconfig.get('Syslog') and
res = d.verify(idx=y) (milterconfig.get('SyslogSuccess')
if res: or milterconfig.get('debugLevel') >= 1)):
self.dkim_comment = 'Good {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a')) syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
else: 'd={3})'.format(self.getsymval('i'),
self.dkim_comment = 'Bad {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a')) d.signature_fields.get(b'a'),
except dkim.DKIMException as x: d.signature_fields.get(b's'),
self.dkim_comment = str(x) d.domain.lower()))
except dkim.DKIMException as x:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('DKIM: {0}'.format(x)) syslog.syslog('DKIM: {0}'.format(x))
except Exception as x: except Exception as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: {0}".format(x)) syslog.syslog("sign_dkim: {0}".format(x))
self.header_i = d.signature_fields.get(b'i') raise
self.header_d = d.signature_fields.get(b'd')
self.header_a = d.signature_fields.get(b'a') def check_dkim(self, txt):
if res: res = False
if milterconfig.get('Syslog'): self.header_a = None
syslog.syslog('DKIM: Pass ({0})'.format(d.domain)) for y in range(self.has_dkim): # Verify _ALL_ the signatures
self.dkim_domain = d.domain d = dkim.DKIM(txt)
else: try:
if milterconfig.get['DiagnosticDirectory']: res = d.verify(idx=y)
fd,fname = tempfile.mkstemp(".dkim") if res:
with os.fdopen(fd,"w+b") as fp: if d.signature_fields.get(b'a') == 'ed25519-sha256':
fp.write(txt) self.dkim_comment = ('Good {0} signature'
if milterconfig.get('Syslog'): .format(d.signature_fields
syslog.syslog('DKIM: Fail (saved as {0})'.format(fname)) .get(b'a')))
else: else:
syslog.syslog('DKIM: Fail ({0})'.format(d.domain)) self.dkim_comment = ('Good {0} bit {1} signature'
if res: .format(d.keysize,
result = 'pass' d.signature_fields
else: .get(b'a')))
result = 'fail' else:
res = False self.dkim_comment = ('Bad {0} bit {1} signature.'
self.arresults.append( .format(d.keysize,
authres.DKIMAuthenticationResult(result=result, d.signature_fields.get(b'a')))
header_i = self.header_i, header_d = self.header_d, header_a = self.header_a, except dkim.DKIMException as x:
result_comment = self.dkim_comment) self.dkim_comment = str(x)
) if milterconfig.get('Syslog'):
return syslog.syslog('DKIM: {0}'.format(x))
except Exception as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x))
try:
self.header_i = d.signature_fields.get(b'i')
except TypeError as x:
self.header_i = None
try:
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: 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'
if res:
if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess') or
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'),
d.signature_fields.get(b's'),
d.domain.lower()))
self.dkim_domain = d.domain.lower()
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:
if milterconfig.get('Syslog'):
if d.domain:
syslog.syslog('DKIM: Fail ({0})'
.format(d.domain.lower()))
else:
syslog.syslog('DKIM: Fail, unextractable domain')
if res:
result = 'pass'
else:
result = 'fail'
res = False
if self.header_d:
self.arresults.append(
authres.DKIMAuthenticationResult(result=result,
header_i=self.header_i,
header_d=self.header_d,
header_a=self.header_a,
result_comment=
self.dkim_comment)
)
self.header_a = None
return
def main(): def main():
# Ugh, but there's no easy way around this. # Ugh, but there's no easy way around this.
@@ -274,15 +349,16 @@ def main():
global privateEd25519 global privateEd25519
privateRSA = False privateRSA = False
privateEd25519 = False privateEd25519 = False
configFile = '/etc/dkimpy-milter.conf' configFile = '/usr/local/etc/dkimpy-milter.conf'
if len(sys.argv) > 1: if len(sys.argv) > 1:
if sys.argv[1] in ( '-?', '--help', '-h' ): if sys.argv[1] in ('-?', '--help', '-h'):
print('usage: dkimpy-milter [<configfilename>]') print('usage: dkimpy-milter [<configfilename>]')
sys.exit(1) sys.exit(1)
configFile = sys.argv[1] configFile = sys.argv[1]
milterconfig = config._processConfigFile(filename = configFile) milterconfig = config._processConfigFile(filename=configFile)
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
facility = eval("syslog.LOG_{0}".format(milterconfig.get('SyslogFacility').upper())) facility = eval("syslog.LOG_{0}"
.format(milterconfig.get('SyslogFacility').upper()))
syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility) syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility)
setExceptHook() setExceptHook()
pid = write_pid(milterconfig) pid = write_pid(milterconfig)
@@ -294,12 +370,12 @@ def main():
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
miltername = 'dkimpy-filter' miltername = 'dkimpy-filter'
socketname = milterconfig.get('Socket') socketname = milterconfig.get('Socket')
if milterconfig.get('Syslog'):
syslog.syslog('dkimpy-milter started:{0} user:{1}'.format(pid,milterconfig.get('UserID')))
sys.stdout.flush()
Milter.runmilter(miltername,socketname,240)
own_socketfile(milterconfig)
drop_privileges(milterconfig) drop_privileges(milterconfig)
sys.stdout.flush()
if milterconfig.get('Syslog'):
syslog.syslog('dkimpy-milter starting:{0} user:{1}'
.format(pid, milterconfig.get('UserID')))
Milter.runmilter(miltername, socketname, 240)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+99 -93
View File
@@ -27,8 +27,6 @@
import syslog import syslog
import os import os
import sys import sys
import re
import urllib
import stat import stat
import dkim import dkim
import socket import socket
@@ -37,26 +35,28 @@ from dnsplug import Session
# default values # default values
defaultConfigData = { defaultConfigData = {
'Syslog' : 'yes', 'Syslog': 'yes',
'SyslogFacility' : 'mail', 'SyslogFacility': 'mail',
'UMask' : 007, 'UMask': 007,
'Mode' : 'sv', 'Mode': 'sv',
'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', 'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid', 'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid',
'UserID' : 'dkimpy-milter', 'UserID': 'dkimpy-milter',
'Canonicalization' : 'relaxed/simple', 'Canonicalization': 'relaxed/simple',
'InternalHosts' : '127.0.0.1', 'InternalHosts': '127.0.0.1',
'InternalHostsObj' : False, 'IntHosts': False,
'DiagnosticDirectory' : '' 'DiagnosticDirectory': '',
} 'MacroList': '',
'MacroListVerify': '',
'debugLevel': 0 # Undocumented config item for developer use
}
#################################
class ConfigException(Exception): class ConfigException(Exception):
'''Exception raised when there's a configuration file error.''' '''Exception raised when there's a configuration file error.'''
pass pass
#################################
class HostsDataset(object): class HostsDataset(object):
'''Hold a group of host related dataset objects''' '''Hold a group of host related dataset objects'''
@@ -85,34 +85,41 @@ class HostsDataset(object):
self.negative = True self.negative = True
try: try:
self.item = ipaddress.ip_address(unicode(self.item, "utf-8")) self.item = ipaddress.ip_address(unicode(self.item, "utf-8"))
if isinstance(self.item, ipaddress.IPv4Address): self.isipv4 = True if isinstance(self.item, ipaddress.IPv4Address):
elif isinstance(self.item, ipaddress.IPv6Address): self.isipv6 = True self.isipv4 = True
elif isinstance(self.item, ipaddress.IPv6Address):
self.isipv6 = True
except ValueError as e: except ValueError as e:
try: try:
self.item = ipaddress.ip_network(unicode(self.item, "utf-8"), strict=False) self.item = ipaddress.ip_network(unicode
if isinstance(self.item, ipaddress.IPv4Network): self.isipv4cidr = True (self.item, "utf-8"),
elif isinstance(self.item, ipaddress.IPv6Network): self.isipv6cidr = True strict=False)
if isinstance(self.item, ipaddress.IPv4Network):
self.isipv4cidr = True
elif isinstance(self.item, ipaddress.IPv6Network):
self.isipv6cidr = True
except ValueError as e2: except ValueError as e2:
if self.item[0] == '.' and len(self.item.split('.')) > 2: if self.item[0] == '.' and len(self.item.split('.')) > 2:
self.isdomain = True self.isdomain = True
elif len(self.item.split('.')) > 1: # It has a '.' in it elif len(self.item.split('.')) > 1: # It has a '.' in it
self.ishostname = True self.ishostname = True
else: else:
raise ConfigException('Unknown dataset item: {0}'.format(item)) raise ConfigException('Unknown dataset item: {0}'
.format(item))
def match(self, connectip): def match(self, connectip):
'''Check if the connect IP is part of the dataset''' '''Check if the connect IP is part of the dataset'''
source = ipaddress.ip_address(unicode(connectip, "utf-8")) source = ipaddress.ip_address(unicode(connectip, "utf-8"))
for item in self.dataset: for item in self.dataset:
if item.isdomain or item.ishostname: if item.isdomain or item.ishostname:
result = self.matchname(source) # Match host/domain names first result = self.matchname(source) # Match host/domains first
if result: if result:
return(result) return(result)
elif item.isipv4 or item.isipv4cidr: elif item.isipv4 or item.isipv4cidr: # Then IPv4/6 addresses or
if isinstance(source, ipaddress.IPv4Address): # Then IPv4/6 addresses if isinstance(source, ipaddress.IPv4Address): # networks
return(self.match4(source)) # or networks depending return(self.match4(source)) # depending on the item type
elif item.isipv6 or item.isipv6cidr: # on the item type and elif item.isipv6 or item.isipv6cidr: # and connect type
if isinstance(source, ipaddress.IPv6Address): # connection type if isinstance(source, ipaddress.IPv6Address):
return(self.match6(source)) return(self.match6(source))
def matchname(self, source): def matchname(self, source):
@@ -126,7 +133,7 @@ class HostsDataset(object):
for item in self.dataset: for item in self.dataset:
if item.isdomain: if item.isdomain:
for ptr in ptrlist: for ptr in ptrlist:
# Strip the leading '.' off the domain name so exact match works. # Strip the leading '.' off the domain name for exact match
if item.item[1:] == ptr[-len(item.item)+1:]: if item.item[1:] == ptr[-len(item.item)+1:]:
matchdomain = True matchdomain = True
negativedomain = item.negative negativedomain = item.negative
@@ -211,21 +218,16 @@ class HostsDataset(object):
match = False match = False
return(match) 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,
def _processConfigFile(filename = None, configdata = None, useSyslog = 1, useStderr=0):
useStderr = 0):
'''Load the specified config file, exit and log errors if it fails, '''Load the specified config file, exit and log errors if it fails,
otherwise return a config dictionary.''' otherwise return a config dictionary.'''
import config import config
if configdata == None: configdata = config.defaultConfigData if configdata is None:
if filename != None: configdata = config.defaultConfigData
if filename is not None:
try: try:
_readConfigFile(filename, configdata) _readConfigFile(filename, configdata)
except Exception, e: except Exception, e:
@@ -237,7 +239,7 @@ def _processConfigFile(filename = None, configdata = None, useSyslog = 1,
sys.exit(1) sys.exit(1)
return(configdata) return(configdata)
####################
def _find_boolean(item): def _find_boolean(item):
if type(item) == int: if type(item) == int:
item = str(item) item = str(item)
@@ -248,14 +250,15 @@ def _find_boolean(item):
else: else:
raise dkim.ParameterError() raise dkim.ParameterError()
return item return item
####################
def _calculate_authserv_id(as_id):
def _make_authserv_id(as_id):
"""Determine AuthservID if needed""" """Determine AuthservID if needed"""
if as_id == 'HOSTNAME': if as_id == 'HOSTNAME':
as_id = socket.gethostname() as_id = socket.gethostname()
return as_id return as_id
####################
def _dataset_to_list(dataset): def _dataset_to_list(dataset):
"""Convert a dataset (as defined in dkimpymilter.8) and return a python """Convert a dataset (as defined in dkimpymilter.8) and return a python
list of values.""" list of values."""
@@ -291,78 +294,78 @@ def _dataset_to_list(dataset):
else: else:
return [dataset.strip().strip(',')] return [dataset.strip().strip(',')]
if dataset[-3:] == '.db' or dataset[:3] == 'db:': if dataset[-3:] == '.db' or dataset[:3] == 'db:':
# This is a Sleepycat (Oracle) DB dataset # This is a Sleepycat (Oracle) DB dataset, which we dont support
import whichdb # Will need rewriting someday for python3 raise dkim.ParameterError('Unsupported dataset db datase: {0}'
if dataset[-3:] == '.db': .format(type(dataset)))
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))) raise dkim.ParameterError('Unimplmented dataset type: {0}'
.format(type(dataset)))
###############################################################
commentRx = re.compile(r'^(.*)#.*$') def _readConfigFile(path, configData=None, configGlobal={}):
def _readConfigFile(path, configData = None, configGlobal = {}):
'''Reads a configuration file from the specified path, merging it '''Reads a configuration file from the specified path, merging it
with the configuration data specified in configData. Returns a with the configuration data specified in configData. Returns a
dictionary of name/value pairs based on configData and the values dictionary of name/value pairs based on configData and the values
read from path.''' read from path.'''
debugLevel = configGlobal.get('debugLevel', 0) debugLevel = configGlobal.get('debugLevel', 0)
if debugLevel >= 5: syslog.syslog('readConfigFile: Loading "%s"' % path) if debugLevel >= 5:
if configData == None: configData = {} syslog.syslog('readConfigFile: Loading "%s"' % path)
if configData is None:
configData = {}
nameConversion = { nameConversion = {
'AuthservID' : 'str', 'AuthservID': 'str',
'Syslog' : 'bool', 'Syslog': 'bool',
'SyslogFacility' : 'str', 'SyslogFacility': 'str',
'SyslogSuccess' : 'bool', 'SyslogSuccess': 'bool',
'UMask' : 'int', 'UMask': 'int',
'Mode' : 'str', 'Mode': 'str',
'Socket' : 'str', 'Socket': 'str',
'PidFile' : 'str', 'PidFile': 'str',
'UserID' : 'str', 'UserID': 'str',
'Domain' : 'dataset', 'Domain': 'dataset',
'KeyFile' : 'str', 'KeyFile': 'str',
'KeyFileEd25519' : 'str', 'KeyFileEd25519': 'str',
'Selector' : 'str', 'Selector': 'str',
'SelectorEd25519': 'str', 'SelectorEd25519': 'str',
'Canonicalization' : 'str', 'Canonicalization': 'str',
'InternalHosts' : 'dataset', 'InternalHosts': 'dataset',
'InternalHostsObj': 'bool', 'IntHosts': 'bool',
'DiagnosticDirectory' : 'str' 'DiagnosticDirectory': 'str',
} 'MacroList': 'dataset',
'MacroListVerify': 'dataset',
'debugLevel': 'int'
}
# check to see if it's a file # check to see if it's a file
try: try:
mode = os.stat(path)[0] mode = os.stat(path)[0]
except OSError, e: except OSError, e:
syslog.syslog(syslog.LOG_ERR,'ERROR stating "%s": %s' % ( path, e.strerror )) syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
% (path, e.strerror))
return(configData) return(configData)
if not stat.S_ISREG(mode): if not stat.S_ISREG(mode):
syslog.syslog(syslog.LOG_ERR,'ERROR: is not a file: "%s", mode=%s' % ( path, oct(mode) )) syslog.syslog(syslog.LOG_ERR, 'ERROR: is not a file: "%s", mode=%s'
% (path, oct(mode)))
return(configData) return(configData)
# load file # load file
fp = open(path, 'r') fp = open(path, 'r')
while 1: while 1:
line = fp.readline() line = fp.readline()
if not line: break if not line:
break
# parse line # parse line
line = line.split('#', 1)[0].strip() line = line.split('#', 1)[0].strip()
if not line: continue if not line:
continue
data = line.split() data = line.split()
if len(data) != 2: if len(data) != 2:
if len(data) == 1: if len(data) == 1:
if debugLevel >= 1: if debugLevel >= 1:
syslog.syslog('Configuration item "%s" not defined in file "%s"' syslog.syslog('Config item "%s" not defined in file "%s"'
% ( line, path )) % (line, path))
if len(data) == 1: if len(data) == 1:
name = data name = data
value = '' value = ''
@@ -374,12 +377,14 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
# check validity of name # check validity of name
conversion = nameConversion.get(name) conversion = nameConversion.get(name)
if conversion == None: if conversion is None:
syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path )) syslog.syslog('ERROR: Unknown name "%s" in file "%s"'
% (name, path))
continue continue
if debugLevel >= 5: syslog.syslog('readConfigFile: Found entry "%s=%s"' if debugLevel >= 5:
% ( name, value )) syslog.syslog('readConfigFile: Found entry "%s=%s"'
% (name, value))
if conversion == 'bool': if conversion == 'bool':
configData[name] = _find_boolean(value) configData[name] = _find_boolean(value)
elif conversion == 'str': elif conversion == 'str':
@@ -389,12 +394,13 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
elif conversion == 'dataset': elif conversion == 'dataset':
configData[name] = _dataset_to_list(value) configData[name] = _dataset_to_list(value)
else: else:
syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion)) syslog.syslog(str('name: ' + name + ' value: ' + value +
' conversion: ' + conversion))
configData[name] = conversion(value) configData[name] = conversion(value)
fp.close() fp.close()
try: try:
configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID']) configData['AuthservID'] = _make_authserv_id(configData['AuthservID'])
configData['InternalHostsObj'] = HostsDataset(configData['InternalHosts']) configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
except: except:
pass pass
+17 -21
View File
@@ -16,6 +16,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
def fold(header): def fold(header):
"""Fold a header line into multiple crlf-separated lines at column 72. """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 Borrowed from dkimpy and updated to only add \n instead of \r\n because
@@ -46,9 +47,9 @@ def fold(header):
j = i + 1 j = i + 1
pre += header[:j] + b"\n " pre += header[:j] + b"\n "
header = header[j:] header = header[j:]
namelen = 0
return pre + header return pre + header
def user_group(userid): def user_group(userid):
"""Return user and group from UserID""" """Return user and group from UserID"""
import grp import grp
@@ -64,13 +65,14 @@ def user_group(userid):
running_gid = grp.getgrnam(gidname).gr_gid running_gid = grp.getgrnam(gidname).gr_gid
return running_uid, running_gid return running_uid, running_gid
def drop_privileges(milterconfig): def drop_privileges(milterconfig):
import os import os
import syslog import syslog
if os.getuid() != 0: if os.getuid() != 0:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('drop_privileges: Not running as root. Cannot drop permissions.') syslog.syslog('drop_privileges: Not root. No action taken.')
return return
# Get user and group # Get user and group
@@ -86,9 +88,9 @@ def drop_privileges(milterconfig):
# Set umask # Set umask
old_umask = os.umask(milterconfig.get('UMask')) old_umask = os.umask(milterconfig.get('UMask'))
#################
class ExceptHook: class ExceptHook:
def __init__(self, useSyslog = 1, useStderr = 0): def __init__(self, useSyslog=1, useStderr=0):
self.useSyslog = useSyslog self.useSyslog = useSyslog
self.useStderr = useStderr self.useStderr = useStderr
@@ -104,12 +106,11 @@ class ExceptHook:
sys.stderr.write(line) sys.stderr.write(line)
####################
def setExceptHook(): def setExceptHook():
import sys import sys
sys.excepthook = ExceptHook(useSyslog = 1, useStderr = 1) sys.excepthook = ExceptHook(useSyslog=1, useStderr=1)
####################
def write_pid(milterconfig): def write_pid(milterconfig):
"""Write PID in pidfile. Will not overwrite an existing file.""" """Write PID in pidfile. Will not overwrite an existing file."""
import os import os
@@ -126,10 +127,11 @@ def write_pid(milterconfig):
os.chown(piddir, user, group) os.chown(piddir, user, group)
f = open(milterconfig.get('PidFile'), 'w') f = open(milterconfig.get('PidFile'), 'w')
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Missing pid dir created: {0}'.format(piddir)) syslog.syslog('PID dir created: {0}'.format(piddir))
else: else:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'.format(milterconfig.get('PidFile'), e)) syslog.syslog('Unable to write pidfle {0}. IOError: {1}'
.format(milterconfig.get('PidFile'), e))
raise raise
f.write(pid) f.write(pid)
f.close() f.close()
@@ -137,20 +139,13 @@ def write_pid(milterconfig):
os.chown(milterconfig.get('PidFile'), user, group) os.chown(milterconfig.get('PidFile'), user, group)
else: else:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) syslog.syslog('Unable to write pidfle {0}. File exists.'
raise RuntimeError('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) .format(milterconfig.get('PidFile')))
raise RuntimeError('Unable to write pidfle {0}. File exists.'
.format(milterconfig.get('PidFile')))
return pid return pid
def own_socketfile(milterconfig):
"""If socket is Unix socket, chown to UserID before dropping privileges"""
import os
user, group = user_group(milterconfig.get('UserID'))
if milterconfig.get('Socket')[:1] == '/':
os.chown(milterconfig.get('Socket')[1:], user, group)
if milterconfig.get('Socket')[:6] == "local:":
os.chown(milterconfig.get('Socket')[6:], user, group)
####################
def read_keyfile(milterconfig, keytype): def read_keyfile(milterconfig, keytype):
"""Read private key from file.""" """Read private key from file."""
import syslog import syslog
@@ -163,7 +158,8 @@ def read_keyfile(milterconfig, keytype):
keylist = f.readlines() keylist = f.readlines()
except IOError as e: except IOError as e:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to read keyfile {0}. IOError: {1}'.format(keyfile, e)) syslog.syslog('Unable to read keyfile {0}. IOError: {1}'
.format(keyfile, e))
raise raise
f.close() f.close()
key = '' key = ''
+5 -4
View File
@@ -127,7 +127,6 @@
.rm #[ #] #H #V #F C .rm #[ #] #H #V #F C
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "dkimpy-milter 8"
.TH dkimpyy-milter 8 .TH dkimpyy-milter 8
.SH NAME .SH NAME
.B dkimpy .B dkimpy
@@ -269,7 +268,7 @@ proposal, and Cisco's
.B Internet Identified Mail .B Internet Identified Mail
(IIM) proposal. (IIM) proposal.
.SH VERSION .SH VERSION
This man page covers version 0.9.4 of This man page covers version 1.1.0 of
.I dkimpy-milter. .I dkimpy-milter.
.SH COPYRIGHT .SH COPYRIGHT
Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights
@@ -278,7 +277,7 @@ reserved.
Copyright (c) 2009-2013, 2015, The Trusted Domain Project. Copyright (c) 2009-2013, 2015, The Trusted Domain Project.
All rights reserved. All rights reserved.
Copyright (c) 2018 Scott Kitterman <scott@kitterman.com> Copyright (c) 2018, 2019 Scott Kitterman <scott@kitterman.com>
.SH SEE ALSO .SH SEE ALSO
.I dkimpy-milter.conf(5), sendmail(8) .I dkimpy-milter.conf(5), sendmail(8)
.P .P
@@ -292,4 +291,6 @@ RFC6376 - DomainKeys Identified Mail
.P .P
RFC7601 - Message Header Field for Indicating Message Authentication Status RFC7601 - Message Header Field for Indicating Message Authentication Status
.P .P
draft-ietf-dcrup-dkim-crypto - A new cryptographic signature method for DKIM RFC8301 - Cryptographic Algorithm and Key Usage Update to DomainKeys Identified Mail (DKIM)
.P
RFC8463 - A New Cryptographic Signature Method for DomainKeys Identified Mail (DKIM)
+38 -10
View File
@@ -127,16 +127,13 @@
.rm #[ #] #H #V #F C .rm #[ #] #H #V #F C
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "dkimpy-milter.conf 5"
.TH dkimpy-milter.conf 5 "2018-02-12" .TH dkimpy-milter.conf 5 "2018-02-12"
.SH "NAME" .SH "NAME"
dkimpy-milter \- Python milter for DKIM signing and validation dkimpy-milter \- Python milter for DKIM signing and validation
.SH "VERSION" .SH "VERSION"
.IX Header "VERSION"
0\.9\.2 0\.9\.2
.SH "DESCRIPTION" .SH "DESCRIPTION"
.IX Header "DESCRIPTION"
.I dkimpy-milter(8) .I dkimpy-milter(8)
implements the implements the
.B DKIM .B DKIM
@@ -160,18 +157,15 @@ The provided setup.py installs this configuration file in /etc or
Command line invocation of parameters as is done by OpenDKIM is not supported. Command line invocation of parameters as is done by OpenDKIM is not supported.
.SH "USAGE" .SH "USAGE"
.IX Header "USAGE"
Usage: Usage:
dkimpy-milter [/etc/dkimpy-milter.conf] dkimpy-milter [/etc/dkimpy-milter.conf]
.SH "OTHER DOCUMENTATION" .SH "OTHER DOCUMENTATION"
.IX Header "OTHER DOCUMENTATION"
This documentation assumes you have read Postfix's README_FILES/MILTER_README This documentation assumes you have read Postfix's README_FILES/MILTER_README
(or Sendmail equivalent) and are generally familiar with Domain Keys Identified (or Sendmail equivalent) and are generally familiar with Domain Keys Identified
Mail (DKIM). See RFC 6376 for details. Mail (DKIM). See RFC 6376 for details.
.SH "SYNOPSIS" .SH "SYNOPSIS"
.IX Header "SYNOPSIS"
dkimpy-milter operates with a default installed configuration file and dkimpy-milter operates with a default installed configuration file and
set of default configuration options that are used if the configuration file set of default configuration options that are used if the configuration file
@@ -181,14 +175,12 @@ files can be used directly. Not all OpenDKIM options are supported. If an
unsupported option from OpenDKIM is specified, an error will be raised. unsupported option from OpenDKIM is specified, an error will be raised.
.SH "DESCRIPTION" .SH "DESCRIPTION"
.IX Header "DESCRIPTION"
Configuration options are described here and in the configuration file Configuration options are described here and in the configuration file
provided with the package. The provided setup.py installs this configuration provided with the package. The provided setup.py installs this configuration
file in /etc or /usr/local/etc. file in /etc or /usr/local/etc.
.SH "OPTIONS" .SH "OPTIONS"
.IX Header "OPTIONS"
.TP .TP
.I AuthservID (string) .I AuthservID (string)
@@ -255,13 +247,50 @@ all messages. Ignored if a
is defined. [KeyTable NOT IMPLEMENTED] is defined. [KeyTable NOT IMPLEMENTED]
.TP .TP
.I KeyFileEd25119 (string) .I KeyFileEd25519 (string)
Gives the location of a Ed25519 private key to be used for Ed25519 signing 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 all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
generation (as used in dkimpy). Ignored if a generation (as used in dkimpy). Ignored if a
.I KeyTableEd25519 .I KeyTableEd25519
is defined. [KeyTableEd25519 NOT IMPLEMENTED] is defined. [KeyTableEd25519 NOT IMPLEMENTED]
.TP
.I MacroList (dataset)
Defines a set of MTA-provided
.I macros
that should be checked to see if the sender has been determined to be a
local user and therefore whether or not the message should be signed. If
a
.I value
is specified matching a macro name in the data set, the value of the macro
must match a value specified (matching is case-sensitive), otherwise the
macro must be defined but may contain any value. The set is empty by
default, meaning macros are not considered when making the sign-verify
decision. The general format of the value is
.I value1[|value2[|...]];
if one or more value is defined then the macro must be set to one of the
listed values, otherwise the macro must be set but can contain any
value.
In order for the macro and its value to be available to the filter for
checking, the MTA must send it during the protocol exchange. This is either
accomplished via manual configuration of the MTA to send the desired macros
or, for MTA/filter combinations that support the feature, the filter can
request those macros that are of interest. The latter is a feature negotiated
at the time the filter receives a connection from the MTA and its availability
depends upon the version of milter used to compile the filter and the version
of the MTA making the connection.
.TP
.I MacroListVerify (dataset)
Defines a set of MTA-provided
.I macros
that should be checked to see if the sender has been determined to be an
external source and therefore whether or not the message should be signed.
Entries in this data set follow the same form as those of the
.I MacroList
option above. [this option is not inhereted from OpenDKIM]
.TP .TP
.I Mode (string) .I Mode (string)
Selects operating modes. The string is a concatenation of characters that Selects operating modes. The string is a concatenation of characters that
@@ -404,7 +433,6 @@ unless an alternate
is specified. is specified.
.SH "AUTHORS" .SH "AUTHORS"
.IX Header "AUTHORS"
\ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>. \ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>.
It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc. It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
Copyright (c) 2013-2015 Stuart D. Gathman Copyright (c) 2013-2015 Stuart D. Gathman
+12 -6
View File
@@ -18,20 +18,26 @@
from setuptools import setup from setuptools import setup
import os import os
import dkimpy_milter
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail." description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
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', 'ipaddress', 'PyDNS']
except ImportError: # If PyDNS is not installed, prefer dnspython
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython']
setup( setup(
name='dkimpy-milter', name='dkimpy-milter',
version=dkimpy_milter.__version__, version='1.0.3',
author='Scott Kitterman', author='Scott Kitterman',
author_email='scott@kitterman.com', author_email='scott@kitterman.com',
url='https://launchpad.net/dkimpy-milter', url='https://launchpad.net/dkimpy-milter',
description=description, description=description,
download_url = "https://pypi.python.org/pypi/dkimpy-milter", download_url = "https://pypi.python.org/pypi/dkimpy-milter",
classifiers= [ classifiers= [
'Development Status :: 3 - Alpha', 'Development Status :: 5 - Production/Stable',
'Environment :: No Input/Output (Daemon)', 'Environment :: No Input/Output (Daemon)',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License (GPL)',
@@ -52,9 +58,9 @@ setup(
data_files=[(os.path.join('share', 'man', 'man5'), data_files=[(os.path.join('share', 'man', 'man5'),
['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'), ['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'),
['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']), ['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']),
(os.path.join('/lib', 'systemd', 'system'), (os.path.join('lib', 'systemd', 'system'),
['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'), ['system/dkimpy-milter.service']),(os.path.join('etc', 'init.d'),
['system/dkimpy-milter'])], ['system/dkimpy-milter'])],
install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dns'],
zip_safe = False, zip_safe = False,
**kw
) )
Regular → Executable
+10 -10
View File
@@ -13,8 +13,6 @@
# Provides: dkim-milter dkim-milter-python dkimpy-milter # Provides: dkim-milter dkim-milter-python dkimpy-milter
# Required-Start: $remote_fs $syslog $network $time # Required-Start: $remote_fs $syslog $network $time
# Required-Stop: $remote_fs $syslog $network # Required-Stop: $remote_fs $syslog $network
# Should-Start:
# Should-Stop:
# Default-Start: 2 3 4 5 # Default-Start: 2 3 4 5
# Default-Stop: 0 1 6 # Default-Stop: 0 1 6
# Short-Description: dkimpy-milter # Short-Description: dkimpy-milter
@@ -22,16 +20,16 @@
### END INIT INFO ### END INIT INFO
prefix="/usr/local" prefix="/usr/local"
exec_prefix=${prefix} exec_prefix=${prefix}
sysconfdir="/etc/dkimpy-milter" sysconfdir="/usr/local/etc"
bindir="${exec_prefix}/bin/" bindir="${exec_prefix}/bin/"
RUNDIR="/var/run/dkimpy-milter" RUNDIR="/run/dkimpy-milter"
DAEMON=${bindir}/dkimpy-milter DAEMON=${bindir}/dkimpy-milter
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin: PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:
NAME=dkimpy-milter NAME=dkimpy-milter
DESC="Python DKIM Milter" DESC="Python DKIM Milter"
USER=dkimpy-milter USER=dkimpy-milter
GROUP=dkimpy-milter GROUP=dkimpy-milter
SOCKET=$RUNDIR/dkimpy-milter.pid SOCKET=$RUNDIR/dkimpy-milter.sock
test -x $DAEMON || exit 0 test -x $DAEMON || exit 0
@@ -69,14 +67,14 @@ case "$1" in
fi fi
fi fi
fi fi
start-stop-daemon --start --background --quiet --pidfile \
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
stop) stop)
echo -n "Stopping $DESC: " echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -89,6 +87,7 @@ case "$1" in
force-reload) force-reload)
echo -n "Force reloading $DESC: " echo -n "Force reloading $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -97,7 +96,7 @@ case "$1" in
fi fi
fi fi
sleep 1 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 $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
@@ -105,6 +104,7 @@ case "$1" in
echo "Restarting $DESC: " echo "Restarting $DESC: "
echo -n "Stopping $DESC: " echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -115,7 +115,7 @@ case "$1" in
echo "$NAME." echo "$NAME."
sleep 1 sleep 1
echo -n "Starting $DESC: " 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 $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
+3 -2
View File
@@ -1,11 +1,12 @@
[Unit] [Unit]
Description=DKIMpy Milter Description=DKIMpy Milter
After=syslog.target network.target Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
After=network.target
[Service] [Service]
Type=simple Type=simple
PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid
ExecStart=/usr/local/bin/dkimpy-milter ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target