Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add dkim client capability #158

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
'pysasl >= 0.4.0',
'pycares < 3.0.0; python_version < "3.0"',
'pycares >= 1; python_version >= "3.0"'],
extras_require={
'dkim': ['dkimpy >= 0.9.0'],
},
classifiers=['Development Status :: 3 - Alpha',
'Topic :: Communications :: Email :: Mail Transport Agents',
'Intended Audience :: Developers',
Expand Down
72 changes: 71 additions & 1 deletion slimta/policy/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@
from time import strftime, gmtime, localtime
from math import floor
from socket import getfqdn
import logging

import slimta.logging
from slimta.core import __version__ as VERSION
from . import QueuePolicy
try:
from dkim import dkim_sign, DKIMException
except ImportError:
dkim_sign = None

__all__ = ['AddDateHeader', 'AddMessageIdHeader', 'AddReceivedHeader']

__all__ = ['AddDateHeader', 'AddMessageIdHeader', 'AddReceivedHeader',
'AddDKIMHeader']


class AddDateHeader(QueuePolicy):
Expand Down Expand Up @@ -132,4 +140,66 @@ def apply(self, envelope):
envelope.prepend_header('Received', data)


class AddDKIMHeader(QueuePolicy):
"""Adds a Domain Key Identified Mail header
will by default sign all the headers (except the ones marked
as SHOULD NOT SIGN as stated in dkimpy doc)
if this is not the last header added, the following ones
will not be signed.
:param dkim: Dict of dicts indexed by domain (example.com)
- privkey: private key: PEM file loaded in a string
- selector: selector setup in DNS for the domain
- signature_algorithm: (default rsa-sha256)
- include-headers: headers to sign (by default, all
except the ones marked as SHOULD NOT SIGN see
dkimpy doc)
Refs:
https://www.dkim.org
https://gathman.org/pydkim/
https://launchpad.net/dkimpy
"""

def __init__(self, dkim):
if not dkim_sign:
raise ImportError('dkimpy is not installed')
self.dkim = dkim
self.logger = logging.getLogger(__name__)

def apply(self, envelope):
dom = envelope.sender
if not dom:
return # no warn if sending deliv. fail notif.
if '@' not in dom:
slimta.logging.logline(self.logger.error, __name__, id(self),
'DKIM: invalid sender', **dict(sender=dom))
return
dom = dom.split('@')[1]
if dom not in self.dkim:
slimta.logging.logline(self.logger.debug, __name__, id(self),
"DKIM: domain :'" + dom +
"' is not setup, ignore")
return
domk = self.dkim[dom]
pk = domk['privkey'].encode('utf-8')
sel = domk['selector'].encode('utf-8')
algo = domk['signature_algorithm'] or 'rsa-sha256'
algo = algo.encode('utf-8')
flds = domk['include_headers']
if flds:
flds = [x.encode('utf-8') for x in flds]
dom = dom.encode('utf-8')
try:
h, m = envelope.flatten()
wm = h + m
hd = dkim_sign(message=wm, selector=sel, domain=dom, privkey=pk,
signature_algorithm=algo, include_headers=flds)
except DKIMException as e:
slimta.logging.logline(self.logger.error, __name__, id(self),
'DKIM: exception:' + str(e),
**dict(sender=envelope.sender, domain=dom))
return
data = hd.replace(b'\r\n', b'').decode('utf-8').split(':', 1)[1]
envelope.prepend_header('DKIM-Signature', data) # RFC 6376 par. 5.6


# vim:et:fdm=marker:sts=4:sw=4:ts=4
1 change: 1 addition & 0 deletions test/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ mox3
testfixtures
flake8
twine
dkimpy >= 0.9.0
59 changes: 57 additions & 2 deletions test/test_slimta_policy_headers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from mox3.mox import MoxTestBase, IsA, IgnoreArg
import unittest
import logging

import slimta.logging
from slimta.policy.headers import AddDateHeader, AddMessageIdHeader, \
AddReceivedHeader
AddReceivedHeader, AddDKIMHeader
from slimta.envelope import Envelope


class TestPolicyHeaders(unittest.TestCase):
class TestPolicyHeaders(MoxTestBase, unittest.TestCase):

def test_add_date_header(self):
env = Envelope()
Expand Down Expand Up @@ -63,5 +66,57 @@ def test_add_received_header_prepended(self):
AddReceivedHeader().apply(env)
self.assertEqual(['Received', 'From'], env.headers.keys())

def test_add_dkim_header(self):
log = []
def set_log(a,b,c,d, **kwargs):
log.append(d)
self.mox.StubOutWithMock(slimta.logging, 'logline')
slimta.logging.logline(IgnoreArg(), IsA(str), IsA(int), IsA(str),
sender='sender').WithSideEffects(set_log)
slimta.logging.logline(IgnoreArg(), IsA(str), IsA(int),
IsA(str)).WithSideEffects(set_log)
slimta.logging.logline(IgnoreArg(), IsA(str), IsA(int),
IsA(str), domain=b'example.com',
sender='[email protected]').WithSideEffects(set_log)
self.mox.ReplayAll()
env = Envelope('sender', ['[email protected]'])
env.parse(b'From: [email protected]\r\n')
dkim = {}
AddDKIMHeader(dkim).apply(env)
self.assertFalse(env.headers['DKIM-Signature'])
self.assertTrue(log[0] == "DKIM: invalid sender")
env = Envelope('[email protected]', ['[email protected]'])
env.parse(b'From: [email protected]\r\n')
AddDKIMHeader(dkim).apply(env)
self.assertFalse(env.headers['DKIM-Signature'])
self.assertTrue(log[1] ==
"DKIM: domain :'example.com' is not setup, ignore")
dkim['example.com'] = {'privkey': 'bad', 'selector': 'sel',
'signature_algorithm': 'rsa-sha1',
'include_headers': ['from','subject'] }
AddDKIMHeader(dkim).apply(env)
self.assertFalse(env.headers['DKIM-Signature'])
self.assertTrue(log[2] == "DKIM: exception:Private key not found")
pk = """
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAMhmwtECnKod9ywj8KcK308anyuS2iglAAoaAsibaduk0TTZX/sG
wOAwwh71jsrdMIMDKGAnOn7ikYSVfxvFUQsCAwEAAQJAVWVsyRIa3mcsh9O83gHF
DPlkMHZAnnC95pAU9ZU8c8qzGolDz2h3g+3py09L2dNN1KrmHgjs706OuKznTK3C
KQIhAPocPlKGOQvM5t1Iv7kU2dkMsDso5iMLWJ7si5zTAM7/AiEAzR7aoUhJiFaD
gm35ak2QzAk99H6uZXL5pPCvQJ+HyfUCIQCxTXJU2Df6iJAk0JyxXPmuJ5OK7Mxw
jWuOhgvW6bIKCwIhAJ7RT+hmpwCYM7TuX5puOjmwwjanS3KjRiXucVMw8httAiBV
LB94QlyDoRo6NOTbRIU1quGV/G3jufSl5hgqwuibQw==
-----END RSA PRIVATE KEY-----
"""
dkim['example.com'] = { 'privkey': pk, 'selector': 'sel',
'signature_algorithm': 'rsa-sha1',
'include_headers': ['from','subject'] }
AddDKIMHeader(dkim).apply(env)
self.assertTrue(env.headers['DKIM-Signature'])
sighd = env.headers['DKIM-Signature']
self.assertIn('a=rsa-sha1', sighd)
self.assertIn('d=example.com', sighd)
self.assertIn('s=sel', sighd)


# vim:et:fdm=marker:sts=4:sw=4:ts=4