Skip to content
Snippets Groups Projects
Commit 8f791a34 authored by Julian's avatar Julian
Browse files

Remailer support

Adds "remailer_canonical" request that allows canonical_map address rewriting
with Postfix for uffd's remailer feature.
parent a0373b5b
No related branches found
No related tags found
No related merge requests found
Pipeline #15342 passed
...@@ -60,14 +60,18 @@ class MockConnection: ...@@ -60,14 +60,18 @@ class MockConnection:
pass pass
class UffdAPIMock: class UffdAPIMock:
def __init__(self, aliases=None): def __init__(self, aliases=None, remailer_map=None):
self.aliases = aliases or {} self.aliases = aliases or {}
self.remailer_map = remailer_map or {}
def get_aliases(self, mail_address): def get_aliases(self, mail_address):
return self.aliases.get(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): class TestSocketmapsRequestHandler(unittest.TestCase):
def test_handle(self): def test_handle_virtual(self):
api = UffdAPIMock(aliases={ api = UffdAPIMock(aliases={
'test@example.com': ['test-dest@example.com'], 'test@example.com': ['test-dest@example.com'],
'space test@example.com': ['space test dest@example.com'], 'space test@example.com': ['space test dest@example.com'],
...@@ -136,3 +140,70 @@ class TestSocketmapsRequestHandler(unittest.TestCase): ...@@ -136,3 +140,70 @@ class TestSocketmapsRequestHandler(unittest.TestCase):
conn = MockConnection(request, 4096) conn = MockConnection(request, 4096)
RequestHandler(conn, '', None).handle() RequestHandler(conn, '', None).handle()
self.assertIn(b'PERM ', conn.sent) 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)
...@@ -30,6 +30,11 @@ class UffdAPI: ...@@ -30,6 +30,11 @@ class UffdAPI:
logger.debug('API getmails response for %s: %s', repr(mail_address), destinations) logger.debug('API getmails response for %s: %s', repr(mail_address), destinations)
return 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 # From https://cr.yp.to/proto/netstrings.txt
# #
# Any string of 8-bit bytes may be encoded as [len]":"[string]",". # Any string of 8-bit bytes may be encoded as [len]":"[string]",".
...@@ -118,6 +123,7 @@ def decode_netstring(netstring): ...@@ -118,6 +123,7 @@ def decode_netstring(netstring):
class SocketmapsRequestHandler(socketserver.BaseRequestHandler): class SocketmapsRequestHandler(socketserver.BaseRequestHandler):
api = None # Overwritten api = None # Overwritten
remailer_domain = None # Overwritten
def handle(self): def handle(self):
buf = b'' buf = b''
...@@ -188,12 +194,22 @@ class SocketmapsRequestHandler(socketserver.BaseRequestHandler): ...@@ -188,12 +194,22 @@ class SocketmapsRequestHandler(socketserver.BaseRequestHandler):
# matter, since Postfix internally processes lookup results as a # matter, since Postfix internally processes lookup results as a
# comma-separated list. # comma-separated list.
return 'OK ' + ','.join(results) return 'OK ' + ','.join(results)
if 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 return 'PERM Unknown request name', logging.WARNING
def make_requesthandler(api): def make_requesthandler(api, remailer_domain=None):
class RequestHandler(SocketmapsRequestHandler): class RequestHandler(SocketmapsRequestHandler):
pass pass
RequestHandler.api = api RequestHandler.api = api
RequestHandler.remailer_domain = remailer_domain
return RequestHandler return RequestHandler
class FilenoUnixStreamServer(socketserver.UnixStreamServer): class FilenoUnixStreamServer(socketserver.UnixStreamServer):
...@@ -229,7 +245,8 @@ class StdoutFilter(logging.Filter): ...@@ -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-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-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') @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 \ if (socket_path is None and socket_fd is None) or \
(socket_path is not None and socket_fd is not None): (socket_path is not None and socket_fd is not None):
raise click.ClickException('Either --socket-path or --socket-fd must be specified') 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): ...@@ -244,7 +261,7 @@ def main(socket_path, socket_fd, api_url, api_user, api_secret):
logger.addHandler(stderr_handler) logger.addHandler(stderr_handler)
api = UffdAPI(api_url, api_user, api_secret) api = UffdAPI(api_url, api_user, api_secret)
RequestHandler = make_requesthandler(api) RequestHandler = make_requesthandler(api, remailer_domain)
if socket_path is not None: if socket_path is not None:
cleanup_unix_socket(socket_path) cleanup_unix_socket(socket_path)
socketserver.ThreadingUnixStreamServer(socket_path, RequestHandler).serve_forever() socketserver.ThreadingUnixStreamServer(socket_path, RequestHandler).serve_forever()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment