diff --git a/README.md b/README.md index f56da5eafaa7e15b8c468de28bf498b76e7bb79e..e45500d395e276bd5c254a156fc524feef8b077a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ for a given address. uffd-socketmapd uses this API to integrate alias lookup with MTAs that support the socketmap protocol, like sendmail and postfix. uffd-socketmapd can be run manually. For production deployments, use the -provided debian packages. Add our package mirror to `/etc/sources.list`: +provided Debian packages. Add our package mirror to `/etc/sources.list`: ``` deb https://packages.cccv.de/uffd bullseye main @@ -26,6 +26,14 @@ by adding the following lines to `/etc/postfix/main.cf`: virtual_alias_maps = socketmap:unix:/uffd-socketmapd.sock:virtual # Defaults to $virtual_alias_maps, which does not work here, so unset it virtual_alias_domains = + +# Optional: To use uffd's remailer feature, setup address rewriting. Remailer +# recipient addresses will be rewritten in both the envelope (like virtual +# aliases) and the message headers. Make sure that rewriting takes place +# before DKIM signing and that it is only applied to messages from your +# services (see local_header_rewrite_clients). +recipient_canonical_maps = socketmap:unix:/uffd-socketmapd.sock:remailer_canonical +local_header_rewrite_clients = permit_inet_interfaces permit_sasl_authenticated ``` Note that uffd-socketmapd requires at least uffd v1.2.0! diff --git a/test_server.py b/test_server.py index 5da2692c61ddf79f56207914be7ae136ffea435d..c43e59df4217d90d5ca2594e128128ce8c07fcf4 100644 --- a/test_server.py +++ b/test_server.py @@ -60,14 +60,18 @@ class MockConnection: pass class UffdAPIMock: - def __init__(self, aliases=None): + def __init__(self, aliases=None, remailer_map=None): self.aliases = aliases or {} + self.remailer_map = remailer_map or {} def get_aliases(self, mail_address): return self.aliases.get(mail_address, []) + def resolve_remailer(self, orig_address): + return self.remailer_map.get(orig_address, None) + class TestSocketmapsRequestHandler(unittest.TestCase): - def test_handle(self): + def test_handle_virtual(self): api = UffdAPIMock(aliases={ 'test@example.com': ['test-dest@example.com'], 'space test@example.com': ['space test dest@example.com'], @@ -136,3 +140,28 @@ class TestSocketmapsRequestHandler(unittest.TestCase): conn = MockConnection(request, 4096) RequestHandler(conn, '', None).handle() self.assertIn(b'PERM ', conn.sent) + + def test_handle_remailer(self): + api = UffdAPIMock(remailer_map={ + 'v1-23-testuser@remailer.example.com': 'testuser@example.com', + 'v1-23-testadmin@remailer.example.com': 'testadmin@example.com', + }) + RequestHandler = make_requesthandler(api) + + request = b'54:remailer_canonical v1-23-testuser@remailer.example.com,' + response = b'23:OK testuser@example.com,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) + + request = b'56:remailer_canonical v1-42-not-a-user@remailer.example.com,' + response = b'9:NOTFOUND ,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) + + request = b'35:remailer_canonical test@example.com,' + response = b'9:NOTFOUND ,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) diff --git a/uffd-socketmapd b/uffd-socketmapd index 76d151a5545842cea97698278b0a58bcd10555c4..63672a83ed34852647a85b45890ca3e9e46cfb5f 100755 --- a/uffd-socketmapd +++ b/uffd-socketmapd @@ -30,6 +30,11 @@ class UffdAPI: logger.debug('API getmails response for %s: %s', repr(mail_address), destinations) return destinations + def resolve_remailer(self, orig_address): + '''Return real mail address for a given remailer mail address or None''' + result = self.get('/api/v1/resolve-remailer', orig_address=orig_address) + return result['address'] + # From https://cr.yp.to/proto/netstrings.txt # # Any string of 8-bit bytes may be encoded as [len]":"[string]",". @@ -188,6 +193,12 @@ class SocketmapsRequestHandler(socketserver.BaseRequestHandler): # matter, since Postfix internally processes lookup results as a # comma-separated list. return 'OK ' + ','.join(results) + if name == 'remailer_canonical': + domain = key.rsplit('@', 1)[-1] + result = self.api.resolve_remailer(key) + if result is None: + return 'NOTFOUND ' + return 'OK ' + result return 'PERM Unknown request name', logging.WARNING def make_requesthandler(api):