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)