diff --git a/test_server.py b/test_server.py index 5da2692c61ddf79f56207914be7ae136ffea435d..2444e72ccb86a745ca0fb182c609df18793b9ec3 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,70 @@ 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', + # Artifical entry to test if map is queried (see _with_remailer_domain case) + 'foobar@test.example.com': 'DOESNOTEXIT', + }) + 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) + + # See _with_remailer_domain case + request = b'42:remailer_canonical foobar@test.example.com,' + response = b'14:OK DOESNOTEXIT,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) + + def test_handle_remailer_with_remailer_domain(self): + api = UffdAPIMock(remailer_map={ + 'v1-23-testuser@remailer.example.com': 'testuser@example.com', + 'v1-23-testadmin@remailer.example.com': 'testadmin@example.com', + # Artifical entry to test if map is queried + 'foobar@test.example.com': 'DOESNOTEXIT', + }) + RequestHandler = make_requesthandler(api, remailer_domain='remailer.example.com') + + 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) + + request = b'42:remailer_canonical foobar@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..58397a162d5951426f03ee4c36c654c19634cbf7 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]",". @@ -118,6 +123,7 @@ def decode_netstring(netstring): class SocketmapsRequestHandler(socketserver.BaseRequestHandler): api = None # Overwritten + remailer_domain = None # Overwritten def handle(self): buf = b'' @@ -188,12 +194,22 @@ class SocketmapsRequestHandler(socketserver.BaseRequestHandler): # matter, since Postfix internally processes lookup results as a # comma-separated list. return 'OK ' + ','.join(results) + elif name == 'remailer_canonical': + domain = key.rsplit('@', 1)[-1] + # If self.remailer_domain is unset, uffd will do the filtering + if self.remailer_domain is not None and domain != self.remailer_domain: + return 'NOTFOUND ' + 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): +def make_requesthandler(api, remailer_domain=None): class RequestHandler(SocketmapsRequestHandler): pass RequestHandler.api = api + RequestHandler.remailer_domain = remailer_domain return RequestHandler class FilenoUnixStreamServer(socketserver.UnixStreamServer): @@ -229,7 +245,8 @@ class StdoutFilter(logging.Filter): @click.option('--api-url', required=True, help='Uffd base URL without API prefix or trailing slash (e.g. https://example.com)') @click.option('--api-user', required=True, help='API user/client id') @click.option('--api-secret', required=True, help='API secret, do not set this on the command-line, use environment variable SERVER_API_SECRET instead') -def main(socket_path, socket_fd, api_url, api_user, api_secret): +@click.option('--remailer-domain', help='Domain to filter remailer lookups locally') +def main(socket_path, socket_fd, api_url, api_user, api_secret, remailer_domain): if (socket_path is None and socket_fd is None) or \ (socket_path is not None and socket_fd is not None): raise click.ClickException('Either --socket-path or --socket-fd must be specified') @@ -244,7 +261,7 @@ def main(socket_path, socket_fd, api_url, api_user, api_secret): logger.addHandler(stderr_handler) api = UffdAPI(api_url, api_user, api_secret) - RequestHandler = make_requesthandler(api) + RequestHandler = make_requesthandler(api, remailer_domain) if socket_path is not None: cleanup_unix_socket(socket_path) socketserver.ThreadingUnixStreamServer(socket_path, RequestHandler).serve_forever()