diff --git a/server.py b/server.py index dd59646006ee1c4bd0c46dc1a4495253bb05bf3f..c13bf10cc587ddc7742fd9ef99cf046a7d933afc 100755 --- a/server.py +++ b/server.py @@ -19,8 +19,7 @@ class UffdAPI: def get(self, endpoint, **kwargs): resp = self.session.get(self.baseurl + endpoint, params=kwargs) - if not resp.ok: - raise requests.exceptions.RequestException(f'API responded with status {resp.status_code}') + resp.raise_for_status() return resp.json() def get_aliases(self, mail_address): @@ -148,34 +147,47 @@ class SocketmapsRequestHandler(socketserver.BaseRequestHandler): return 'PERM Malformed request', logging.WARNING name, key = request.split(' ', 1) if name == 'virtual': - # With other lookup methods (like LDAP), virtual(5) attempts to lookup - # "user@domain" first, then "user", then "@domain", until it finds - # a match. With socketmap virtual(5) only attempts to lookup "user@domain". - # To be a drop-in replacement for LDAP, we have to emulate this behaviour. - if '@' in key: - user, domain = key.split('@', 1) + # Postfix option virtual_alias_domains defaults to virtual_alias_maps, + # so we might get some domain requests. Since Postfix does not retry + # failing virtual_alias_domains lookups, PERM is the best option here. + if '@' not in key: + return 'PERM Key is not a valid mail address. Maybe fix the virtual_alias_domains setting.', logging.WARNING + key = key.strip().lower() + # Emulate Postfixes virtual(8) behaviour when used with ldap_table(5). + local, domain = key.split('@', 1) + if '+' in local: + local, extention = local.split('+', 1) + lookup_order = [ + (f'{local}+{extention}@{domain}', ''), + (f'{local}@{domain}', extention), + (f'{local}+{extention}', ''), + (f'{local}', extention), + (f'@{domain}', extention), + ] else: - user, domain = key, '' + lookup_order = [ + (f'{local}@{domain}', ''), + (f'{local}', ''), + (f'@{domain}', ''), + ] results = [] - if user and domain: - results = self.api.get_aliases(f'{user}@{domain}') - if not results and user: - results = self.api.get_aliases(f'{user}') - if not results and domain: - results = self.api.get_aliases(f'@{domain}') + for key, extention in lookup_order: + for address in self.api.get_aliases(key): + # All destinations must be full addresses, but we don't want to fail + # here, if any is malformed. + if '@' in address and extention: + local, domain = address.split('@', 1) + address = f'{local}+{extention}@{domain}' + results.append(address) + if results: + break + # Returning "OK " (without any data) is interperted as a protocol error + # and Postfix will retry the request. if not results: return 'NOTFOUND ' - # Values returned by UffdAPI.get_aliases() could theoretically contain - # commas (currently, we should sanitize this in uffd though). Since we - # use a comma as the value delimiter, we should escape commas in the - # values in some way. However, regardless of the lookup table used by - # Postfix, it internally processes multiple lookup results as a - # comma-separated list. So even with LDAP, which is able to respond with - # multiple values and allows safe transfer of values that contain commas, - # a value that contains one or more commas is processed as if it was - # multiple values. - # There is no way for escaping the separator here and it would not - # matter anyway. + # There is no way to escape commas in the result items and it would not + # matter, since Postfix internally processes lookup results as a + # comma-separated list. return 'OK ' + ','.join(results) return 'PERM Unknown request name', logging.WARNING diff --git a/test_server.py b/test_server.py index f0d4bb814f7c0a8fa14d4038715750ea47da7d49..5da2692c61ddf79f56207914be7ae136ffea435d 100644 --- a/test_server.py +++ b/test_server.py @@ -74,6 +74,8 @@ class TestSocketmapsRequestHandler(unittest.TestCase): 'multitest@example.com': ['test1@example.com', 'test2@example.com'], '@example.com': ['catchall@example.com'], 'user-only': ['user-only@example.com'], + 'test+ext@example.com': ['test-ext@example.com'], + 'ext@example.com': ['ext+ext@example.com'], }) RequestHandler = make_requesthandler(api) request = b'24:virtual test@example.com,' @@ -112,6 +114,18 @@ class TestSocketmapsRequestHandler(unittest.TestCase): RequestHandler(conn, '', None).handle() self.assertEqual(conn.sent, response) + request = b'28:virtual test+ext@example.com,' + response = b'23:OK test-ext@example.com,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) + + request = b'27:virtual ext+ext@example.com,' + response = b'26:OK ext+ext+ext@example.com,' + conn = MockConnection(request, 4096) + RequestHandler(conn, '', None).handle() + self.assertEqual(conn.sent, response) + request = b'27:virtual foo@bar.example.com,' response = b'9:NOTFOUND ,' conn = MockConnection(request, 4096)