diff --git a/README.md b/README.md
index a1a59daaf816d44add0a91aad31f1c9e48e0d5bb..7cb76413b045e0810e59b8f57ff2d54346b9a643 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Please note that we refer to Debian packages here and **not** pip packages.
 - python3-oauthlib
 - python3-flask-babel
 - python3-argon2
+- python3-itsdangerous (also a dependency of python3-flask)
 - python3-mysqldb or python3-pymysql for MySQL/MariaDB support
 
 Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Buster or Bullseye.
diff --git a/debian/control b/debian/control
index f9fef23b94d2f0720ab7773a2c0aae6ab26c3cdd..ae3744ceb9a28e343fe72422911c36855c2112da 100644
--- a/debian/control
+++ b/debian/control
@@ -26,6 +26,7 @@ Depends:
  python3-oauthlib,
  python3-flask-babel,
  python3-argon2,
+ python3-itsdangerous,
  uwsgi,
  uwsgi-plugin-python3,
 Recommends:
diff --git a/setup.py b/setup.py
index ad1822a8314c01583dede68d10682cc3da49c05f..45f8ca6346f9ea87abcfcedc33ffac5bec9cce28 100644
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,7 @@ setup(
 		'Flask-Babel==0.11.2',
 		'alembic==1.0.0',
 		'argon2-cffi==18.3.0',
+		'itsdangerous==0.24',
 
 		# The main dependencies on their own lead to version collisions and pip is
 		# not very good at resolving them, so we pin the versions from Debian Buster
@@ -52,7 +53,6 @@ setup(
 		'click==7.0',
 		'cryptography==2.6.1',
 		'idna==2.6',
-		'itsdangerous==0.24',
 		'Jinja2==2.10',
 		'MarkupSafe==1.1.0',
 		'oauthlib==2.1.0',
diff --git a/tests/test_api.py b/tests/test_api.py
index 4d1a0bf7bc56df8cd0031e77b6c4f09c64355566..7e86379a7fa9261aa792140843cee1ae1c64da98 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -5,7 +5,7 @@ from flask import url_for
 from uffd.api.views import apikey_required
 from uffd.api.models import APIClient
 from uffd.service.models import Service
-from uffd.user.models import User
+from uffd.user.models import User, remailer
 from uffd.password_hash import PlaintextPasswordHash
 from uffd.database import db
 from utils import UffdTestCase, db_flush
@@ -140,6 +140,19 @@ class TestAPIGetusers(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, [])
 
+	def test_with_remailer(self):
+		service = Service.query.filter_by(name='test').one()
+		service.use_remailer = True
+		db.session.commit()
+		user = self.get_user()
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		r = self.client.get(path=url_for('api.getusers', id=10000), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		service = Service.query.filter_by(name='test').one()
+		self.assertEqual(self.fix_result(r.json), [
+			{'displayname': 'Test User', 'email': remailer.build_address(user, service), 'id': 10000, 'loginname': 'testuser', 'groups': ['uffd_access', 'users']},
+		])
+
 	def test_loginname(self):
 		r = self.client.get(path=url_for('api.getusers', loginname='testuser'), headers=[basic_auth('test', 'test')], follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
@@ -159,6 +172,19 @@ class TestAPIGetusers(UffdTestCase):
 			{'displayname': 'Test Admin', 'email': 'admin@example.com', 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
 		])
 
+	def test_email_with_remailer(self):
+		service = Service.query.filter_by(name='test').one()
+		service.use_remailer = True
+		db.session.commit()
+		user = self.get_admin()
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		r = self.client.get(path=url_for('api.getusers', email=remailer.build_address(user, service)), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		service = Service.query.filter_by(name='test').one()
+		self.assertEqual(self.fix_result(r.json), [
+			{'displayname': 'Test Admin', 'email': remailer.build_address(user, service), 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
+		])
+
 	def test_email_empty(self):
 		r = self.client.get(path=url_for('api.getusers', email='foo@bar'), headers=[basic_auth('test', 'test')], follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
@@ -231,3 +257,28 @@ class TestAPIGetgroups(UffdTestCase):
 		r = self.client.get(path=url_for('api.getgroups', member='notauser'), headers=[basic_auth('test', 'test')], follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, [])
+
+class TestAPIRemailerResolve(UffdTestCase):
+	def setUpDB(self):
+		db.session.add(APIClient(service=Service(name='test'), auth_username='test', auth_password='test', perm_remailer=True))
+		db.session.add(Service(name='service1'))
+		db.session.add(Service(name='service2', use_remailer=True))
+
+	def test(self):
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		service = Service.query.filter_by(name='service2').one()
+		user = self.get_user()
+		r = self.client.get(path=url_for('api.resolve_remailer', orig_address=remailer.build_address(user, service)), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(r.json, {'address': user.mail})
+		r = self.client.get(path=url_for('api.resolve_remailer', orig_address='foo@bar'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(r.json, {'address': None})
+
+	def test_invalid(self):
+		r = self.client.get(path=url_for('api.resolve_remailer', orig_address=['foo', 'bar']), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 400)
+		r = self.client.get(path=url_for('api.resolve_remailer'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 400)
+		r = self.client.get(path=url_for('api.resolve_remailer', foo='bar'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 400)
diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py
index d3e68dfec7c330d2b44fee1e4d461ccc5616d37b..9021240c3789e46c6483671e6c6df6425ff9b36d 100644
--- a/tests/test_oauth2.py
+++ b/tests/test_oauth2.py
@@ -6,7 +6,7 @@ from flask import url_for, session
 # These imports are required, because otherwise we get circular imports?!
 from uffd import user
 
-from uffd.user.models import User
+from uffd.user.models import User, remailer
 from uffd.password_hash import PlaintextPasswordHash
 from uffd.session.models import DeviceLoginConfirmation
 from uffd.service.models import Service
@@ -20,7 +20,7 @@ class TestViews(UffdTestCase):
 		db.session.add(OAuth2Client(service=Service(name='test', limit_access=False), client_id='test', client_secret='testsecret', redirect_uris=['http://localhost:5009/callback', 'http://localhost:5009/callback2']))
 		db.session.add(OAuth2Client(service=Service(name='test1', access_group=self.get_admin_group()), client_id='test1', client_secret='testsecret1', redirect_uris=['http://localhost:5008/callback']))
 
-	def assert_authorization(self, r):
+	def assert_authorization(self, r, mail=None):
 		while True:
 			if r.status_code != 302 or r.location.startswith('http://localhost:5009/callback'):
 				break
@@ -44,7 +44,7 @@ class TestViews(UffdTestCase):
 		self.assertEqual(r.json['id'], user.unix_uid)
 		self.assertEqual(r.json['name'], user.displayname)
 		self.assertEqual(r.json['nickname'], user.loginname)
-		self.assertEqual(r.json['email'], user.mail)
+		self.assertEqual(r.json['email'], mail or user.mail)
 		self.assertTrue(r.json.get('groups'))
 
 	def test_authorization(self):
@@ -52,6 +52,16 @@ class TestViews(UffdTestCase):
 		r = self.client.get(path=url_for('oauth2.authorize', response_type='code', client_id='test', state='teststate', redirect_uri='http://localhost:5009/callback', scope='profile'), follow_redirects=False)
 		self.assert_authorization(r)
 
+	def test_authorization_with_remailer(self):
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		service = Service.query.filter_by(name='test').one()
+		service.use_remailer = True
+		db.session.commit()
+		self.login_as('user')
+		r = self.client.get(path=url_for('oauth2.authorize', response_type='code', client_id='test', state='teststate', redirect_uri='http://localhost:5009/callback', scope='profile'), follow_redirects=False)
+		service = Service.query.filter_by(name='test').one()
+		self.assert_authorization(r, mail=remailer.build_address(self.get_user(), service))
+
 	def test_authorization_client_secret_rehash(self):
 		OAuth2Client.query.delete()
 		db.session.add(OAuth2Client(service=Service(name='rehash_test', limit_access=False), client_id='test', client_secret=PlaintextPasswordHash.from_password('testsecret'), redirect_uris=['http://localhost:5009/callback', 'http://localhost:5009/callback2']))
diff --git a/tests/test_user.py b/tests/test_user.py
index 6a92ab6b72c99fb92f9b64cb75a31dc2534237ad..0990a74f0bdb1f54e49eef14bafb19892bafe51b 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -7,8 +7,9 @@ import sqlalchemy
 # These imports are required, because otherwise we get circular imports?!
 from uffd import user
 
-from uffd.user.models import User, Group
+from uffd.user.models import User, remailer, RemailerAddress, Group
 from uffd.role.models import Role, RoleGroup
+from uffd.service.models import Service
 from uffd import create_app, db
 
 from utils import dump, UffdTestCase
@@ -98,6 +99,136 @@ class TestUserModel(UffdTestCase):
 			db.session.add(user2)
 			db.session.commit()
 
+	def test_set_mail(self):
+		user = User()
+		self.assertTrue(user.set_mail('foobar@example.com'))
+		self.assertEqual(user.mail, 'foobar@example.com')
+		self.assertFalse(user.set_mail(''))
+		self.assertEqual(user.mail, 'foobar@example.com')
+		self.assertFalse(user.set_mail('foobar'))
+		self.assertFalse(user.set_mail('@'))
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertFalse(user.set_mail('foobar@remailer.example.com'))
+		self.assertFalse(user.set_mail('v1-1-testuser@remailer.example.com'))
+		self.assertFalse(user.set_mail('v1-1-testuser @ remailer.example.com'))
+		self.assertFalse(user.set_mail('v1-1-testuser@REMAILER.example.com'))
+		self.assertFalse(user.set_mail('v1-1-testuser@foobar@remailer.example.com'))
+
+	def test_get_service_mail(self):
+		service1 = Service(name='service1')
+		service2 = Service(name='service2', use_remailer=True)
+		db.session.add_all([service1, service2])
+		db.session.commit()
+		user = self.get_user()
+		self.assertEqual(user.get_service_mail(service1), user.mail)
+		self.assertEqual(user.get_service_mail(service2), user.mail)
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertEqual(user.get_service_mail(service1), user.mail)
+		self.assertEqual(user.get_service_mail(service2), remailer.build_address(user, service2))
+		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testadmin']
+		self.assertEqual(user.get_service_mail(service1), user.mail)
+		self.assertEqual(user.get_service_mail(service2), user.mail)
+		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testadmin', 'testuser']
+		self.assertEqual(user.get_service_mail(service1), user.mail)
+		self.assertEqual(user.get_service_mail(service2), remailer.build_address(user, service2))
+
+	def test_filter_by_service_mail(self):
+		service1 = Service(name='service1')
+		service2 = Service(name='service2', use_remailer=True)
+		db.session.add_all([service1, service2])
+		db.session.commit()
+		user = self.get_user()
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service2))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service2))).all(), [])
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service2))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, user.mail)).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service2))).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service1))).all(), [])
+		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testadmin']
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service2))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service2))).all(), [])
+		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testadmin', 'testuser']
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, user.mail)).all(), [user])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service1, remailer.build_address(user, service2))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, user.mail)).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service1))).all(), [])
+		self.assertEqual(User.query.filter(User.filter_by_service_mail(service2, remailer.build_address(user, service2))).all(), [user])
+
+class TestRemailer(UffdTestCase):
+	def setUpDB(self):
+		self.service1 = Service(name='service1')
+		self.service2 = Service(name='service2', use_remailer=True)
+		db.session.add_all([self.service1, self.service2])
+
+	def test_is_remailer_domain(self):
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertTrue(remailer.is_remailer_domain('remailer.example.com'))
+		self.assertTrue(remailer.is_remailer_domain('REMAILER.EXAMPLE.COM'))
+		self.assertTrue(remailer.is_remailer_domain(' remailer.example.com '))
+		self.assertFalse(remailer.is_remailer_domain('other.remailer.example.com'))
+		self.assertFalse(remailer.is_remailer_domain('example.com'))
+		self.app.config['REMAILER_OLD_DOMAINS'] = [' OTHER.remailer.example.com ']
+		self.assertTrue(remailer.is_remailer_domain(' OTHER.remailer.example.com '))
+		self.assertTrue(remailer.is_remailer_domain('remailer.example.com'))
+		self.assertTrue(remailer.is_remailer_domain('other.remailer.example.com'))
+		self.assertFalse(remailer.is_remailer_domain('example.com'))
+
+	def test_build_address(self):
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		user = self.get_user()
+		self.assertTrue(remailer.build_address(user, self.service1).endswith('@remailer.example.com'))
+		self.assertTrue(remailer.build_address(user, self.service2).endswith('@remailer.example.com'))
+		self.assertLessEqual(len(remailer.build_local_part(user, self.service1)), 64)
+		self.assertLessEqual(len(remailer.build_address(user, self.service1)), 256)
+		self.assertEqual(remailer.build_address(user, self.service1), remailer.build_address(user, self.service1))
+		self.assertNotEqual(remailer.build_address(user, self.service1), remailer.build_address(user, self.service2))
+		addr = remailer.build_address(user, self.service1)
+		self.app.config['REMAILER_OLD_DOMAINS'] = ['old.remailer.example.com']
+		self.assertEqual(remailer.build_address(user, self.service1), addr)
+		self.assertTrue(remailer.build_address(user, self.service1).endswith('@remailer.example.com'))
+		self.app.config['REMAILER_SECRET_KEY'] = self.app.config['SECRET_KEY']
+		self.assertEqual(remailer.build_address(user, self.service1), addr)
+		self.app.config['REMAILER_SECRET_KEY'] = 'REMAILER-DEBUGKEY'
+		self.assertNotEqual(remailer.build_address(user, self.service1), addr)
+
+	def test_parse_address(self):
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		user = self.get_user()
+		addr = remailer.build_address(user, self.service2)
+		# REMAILER_DOMAIN behaviour
+		self.app.config['REMAILER_DOMAIN'] = None
+		self.assertIsNone(remailer.parse_address(addr))
+		self.assertIsNone(remailer.parse_address('foo@example.com'))
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertEqual(remailer.parse_address(addr), RemailerAddress(user, self.service2))
+		self.assertIsNone(remailer.parse_address('foo@example.com'))
+		self.assertIsNone(remailer.parse_address('foo@remailer.example.com'))
+		self.assertIsNone(remailer.parse_address('v1-foo@remailer.example.com'))
+		self.app.config['REMAILER_DOMAIN'] = 'new-remailer.example.com'
+		self.assertIsNone(remailer.parse_address(addr))
+		self.app.config['REMAILER_OLD_DOMAINS'] = ['remailer.example.com']
+		self.assertEqual(remailer.parse_address(addr), RemailerAddress(user, self.service2))
+		# REMAILER_SECRET_KEY behaviour
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.app.config['REMAILER_OLD_DOMAINS'] = []
+		self.assertEqual(remailer.parse_address(addr), RemailerAddress(user, self.service2))
+		self.app.config['REMAILER_SECRET_KEY'] = self.app.config['SECRET_KEY']
+		self.assertEqual(remailer.parse_address(addr), RemailerAddress(user, self.service2))
+		self.app.config['REMAILER_SECRET_KEY'] = 'REMAILER-DEBUGKEY'
+		self.assertIsNone(remailer.parse_address(addr))
+
 class TestUserViews(UffdTestCase):
 	def setUp(self):
 		super().setUp()
diff --git a/uffd/api/models.py b/uffd/api/models.py
index 391a0aed2391c741e5418593e42b44e466b1a483..f770ee1a89ad8743beeef5c27857c0e6f26ea855 100644
--- a/uffd/api/models.py
+++ b/uffd/api/models.py
@@ -17,6 +17,7 @@ class APIClient(db.Model):
 	perm_users = Column(Boolean(), default=False, nullable=False)
 	perm_checkpassword = Column(Boolean(), default=False, nullable=False)
 	perm_mail_aliases = Column(Boolean(), default=False, nullable=False)
+	perm_remailer = Column(Boolean(), default=False, nullable=False)
 
 	@classmethod
 	def permission_exists(cls, name):
diff --git a/uffd/api/views.py b/uffd/api/views.py
index 1f406d2470f500f84e1764efcf7e682fe83c0d56..33e7122ab2726e219955b03873a7834744fb4d0d 100644
--- a/uffd/api/views.py
+++ b/uffd/api/views.py
@@ -2,7 +2,7 @@ import functools
 
 from flask import Blueprint, jsonify, request, abort
 
-from uffd.user.models import User, Group
+from uffd.user.models import User, remailer, Group
 from uffd.mail.models import Mail, MailReceiveAddress, MailDestinationAddress
 from uffd.api.models import APIClient
 from uffd.session.views import login_get_user, login_ratelimit
@@ -29,6 +29,7 @@ def apikey_required(permission=None):
 				db.session.commit()
 			if permission is not None and not client.has_permission(permission):
 				return 'Forbidden', 403
+			request.api_client = client
 			return func(*args, **kwargs)
 		return decorator
 	return wrapper
@@ -67,7 +68,7 @@ def generate_user_dict(user):
 	return {
 		'id': user.unix_uid,
 		'loginname': user.loginname,
-		'email': user.mail,
+		'email': user.get_service_mail(request.api_client.service),
 		'displayname': user.displayname,
 		'groups': [group.name for group in user.groups]
 	}
@@ -87,7 +88,7 @@ def getusers():
 	elif key == 'loginname' and len(values) == 1:
 		query = query.filter(User.loginname == values[0])
 	elif key == 'email' and len(values) == 1:
-		query = query.filter(User.mail == values[0])
+		query = query.filter(User.filter_by_service_mail(request.api_client.service, values[0]))
 	elif key == 'group' and len(values) == 1:
 		query = query.join(User.groups).filter(Group.name == values[0])
 	else:
@@ -141,3 +142,16 @@ def getmails():
 	else:
 		abort(400)
 	return jsonify([generate_mail_dict(mail) for mail in mails])
+
+@bp.route('/resolve-remailer', methods=['GET', 'POST'])
+@apikey_required('remailer')
+def resolve_remailer():
+	if list(request.values.keys()) != ['orig_address']:
+		abort(400)
+	values = request.values.getlist('orig_address')
+	if len(values) != 1:
+		abort(400)
+	remailer_address = remailer.parse_address(values[0])
+	if not remailer_address:
+		return jsonify(address=None)
+	return jsonify(address=remailer_address.user.mail)
diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 9368ce275acc39db86ac25b12d92a982749d136e..3e25e57675d7438b65cdc1b21352f3d2e90dc859 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -36,6 +36,21 @@ MAIL_PASSWORD='*****'
 MAIL_USE_STARTTLS=True
 MAIL_FROM_ADDRESS='foo@bar.com'
 
+# Set to a domain name (e.g. "remailer.example.com") to enable remailer.
+# Requires special mail server configuration (see uffd-socketmapd). Can be
+# enabled/disabled per-service in the service settings. If enabled, services
+# no longer get real user mail addresses but instead special autogenerated
+# addresses that are replaced with the real mail addresses by the mail server.
+REMAILER_DOMAIN = ''
+REMAILER_OLD_DOMAINS = []
+# Secret used for construction and verification of remailer addresses.
+# If None, the value of SECRET_KEY is used.
+REMAILER_SECRET_KEY = None
+# Set to list of user loginnames to limit remailer to a small list of users.
+# Useful for debugging. If None remailer is active for all users (if
+# configured and enabled for a service).
+REMAILER_LIMIT_TO_USERS = None
+
 # Do not enable this on a public service! There is no spam protection implemented at the moment.
 SELF_SIGNUP=False
 
diff --git a/uffd/migrations/versions/704d1245331c_remailer_setting_and_api_permission.py b/uffd/migrations/versions/704d1245331c_remailer_setting_and_api_permission.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a4011280a707cb9afdf9c35404d26e5f58e8172
--- /dev/null
+++ b/uffd/migrations/versions/704d1245331c_remailer_setting_and_api_permission.py
@@ -0,0 +1,72 @@
+"""remailer setting and api permission
+
+Revision ID: 704d1245331c
+Revises: b9d3f7dac9db
+Create Date: 2022-04-19 17:32:52.304313
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = '704d1245331c'
+down_revision = 'b9d3f7dac9db'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+	meta = sa.MetaData(bind=op.get_bind())
+	api_client = sa.Table('api_client', meta,
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('service_id', sa.Integer(), nullable=False),
+    sa.Column('auth_username', sa.String(length=40), nullable=False),
+    sa.Column('auth_password', sa.Text(), nullable=False),
+    sa.Column('perm_users', sa.Boolean(), nullable=False),
+    sa.Column('perm_checkpassword', sa.Boolean(), nullable=False),
+    sa.Column('perm_mail_aliases', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')),
+    sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username'))
+	)
+	service = sa.Table('service', meta,
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('name', sa.String(length=255), nullable=False),
+    sa.Column('limit_access', sa.Boolean(), nullable=False),
+    sa.Column('access_group_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
+    sa.UniqueConstraint('name', name=op.f('uq_service_name'))
+	)
+	with op.batch_alter_table('api_client', copy_from=api_client) as batch_op:
+		batch_op.add_column(sa.Column('perm_remailer', sa.Boolean(), nullable=False, default=False))
+	with op.batch_alter_table('service', copy_from=service) as batch_op:
+		batch_op.add_column(sa.Column('use_remailer', sa.Boolean(), nullable=False, default=False))
+
+def downgrade():
+	meta = sa.MetaData(bind=op.get_bind())
+	api_client = sa.Table('api_client', meta,
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('service_id', sa.Integer(), nullable=False),
+    sa.Column('auth_username', sa.String(length=40), nullable=False),
+    sa.Column('auth_password', sa.Text(), nullable=False),
+    sa.Column('perm_users', sa.Boolean(), nullable=False),
+    sa.Column('perm_checkpassword', sa.Boolean(), nullable=False),
+    sa.Column('perm_mail_aliases', sa.Boolean(), nullable=False),
+    sa.Column('perm_remailer', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')),
+    sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username'))
+	)
+	service = sa.Table('service', meta,
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('name', sa.String(length=255), nullable=False),
+    sa.Column('limit_access', sa.Boolean(), nullable=False),
+    sa.Column('access_group_id', sa.Integer(), nullable=True),
+    sa.Column('use_remailer', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
+    sa.UniqueConstraint('name', name=op.f('uq_service_name'))
+	)
+	with op.batch_alter_table('service', copy_from=service) as batch_op:
+		batch_op.drop_column('use_remailer')
+	with op.batch_alter_table('api_client', copy_from=api_client) as batch_op:
+		batch_op.drop_column('perm_remailer')
diff --git a/uffd/oauth2/views.py b/uffd/oauth2/views.py
index d13fd42da1883319bc7d52c38592d6f3c6e5b1d5..e53bf7ccd28872d16216ea70a9d80f69be06e539 100644
--- a/uffd/oauth2/views.py
+++ b/uffd/oauth2/views.py
@@ -234,7 +234,7 @@ def userinfo():
 		id=user.unix_uid,
 		name=user.displayname,
 		nickname=user.loginname,
-		email=user.mail,
+		email=user.get_service_mail(request.oauth.client.service),
 		groups=[group.name for group in user.groups]
 	)
 
diff --git a/uffd/service/models.py b/uffd/service/models.py
index 7d3501e19a36091e80597fb4009661e5a07086dc..8584b1b0b709faf12e079439d6c80b5f5885ed81 100644
--- a/uffd/service/models.py
+++ b/uffd/service/models.py
@@ -23,6 +23,8 @@ class Service(db.Model):
 	oauth2_clients = relationship('OAuth2Client', back_populates='service', cascade='all, delete-orphan')
 	api_clients = relationship('APIClient', back_populates='service', cascade='all, delete-orphan')
 
+	use_remailer = Column(Boolean(), default=False, nullable=False)
+
 	def has_access(self, user):
 		return not self.limit_access or self.access_group in user.groups
 
diff --git a/uffd/service/templates/service/api.html b/uffd/service/templates/service/api.html
index 9e0f7a8b3c70bf1997a36dac8d5555ecab4d4f7b..50f9d7f971364f2afa57f4a457fe0e451b65668c 100644
--- a/uffd/service/templates/service/api.html
+++ b/uffd/service/templates/service/api.html
@@ -44,8 +44,21 @@
 				<input class="form-check-input" type="checkbox" id="client-perm-mail-aliases" name="perm_mail_aliases" value="1" aria-label="enabled" {{ 'checked' if client.perm_mail_aliases }}>
 				<label class="form-check-label" for="client-perm-mail-aliases"><b>mail_aliases</b>: {{_('Access mail aliases')}}</label>
 			</div>
+			<div class="form-check">
+				<input class="form-check-input" type="checkbox" id="client-perm-remailer" name="perm_remailer" value="1" aria-label="enabled" {{ 'checked' if client.perm_remailer }}>
+				<label class="form-check-label" for="client-perm-remailer"><b>remailer</b>: {{_('Resolve remailer addresses')}}</label>
+				{% if not remailer.configured %}
+				<i class="fas fa-exclamation-triangle text-warning" data-toggle="tooltip" data-placement="top" title="{{ _('This option has no effect: Remailer config options are unset') }}"></i>
+				{% endif %}
+			</div>
 		</div>
 
 	</form>
 </div>
+
+<script>
+$(function () {
+  $('[data-toggle="tooltip"]').tooltip()
+})
+</script>
 {% endblock %}
diff --git a/uffd/service/templates/service/show.html b/uffd/service/templates/service/show.html
index 0bb892c258ea6c6ca36fa066bef3e34208dca675..c1f075deff8f111d9c647b772a2da66070bcf16f 100644
--- a/uffd/service/templates/service/show.html
+++ b/uffd/service/templates/service/show.html
@@ -30,6 +30,15 @@
 				{% endfor %}
 			</select>
 		</div>
+		<div class="form-group col">
+			<div class="form-check">
+				<input class="form-check-input" type="checkbox" id="service-use-remailer" name="use_remailer" value="1" aria-label="enabled" {{ 'checked' if service.use_remailer }}>
+				<label class="form-check-label" for="service-use-remailer">{{_('Hide mail addresses with remailer')}}</label>
+				{% if not remailer.configured %}
+				<i class="fas fa-exclamation-triangle text-warning" data-toggle="tooltip" data-placement="top" title="{{ _('This option has no effect: Remailer config options are unset') }}"></i>
+				{% endif %}
+			</div>
+		</div>
 	</form>
 
 	{% if service.id %}
@@ -85,7 +94,7 @@
 						</a>
 					</td>
 					<td>
-						{% for perm in ['users', 'checkpassword', 'mail_aliases'] if client.has_permission(perm) %}
+						{% for perm in ['users', 'checkpassword', 'mail_aliases', 'remailer'] if client.has_permission(perm) %}
 						{{ perm }}{{ ',' if not loop.last }}
 						{% endfor %}
 					</td>
@@ -98,4 +107,9 @@
 
 </div>
 
+<script>
+$(function () {
+  $('[data-toggle="tooltip"]').tooltip()
+})
+</script>
 {% endblock %}
diff --git a/uffd/service/views.py b/uffd/service/views.py
index cd01583316a9daa3afbbf334e0e57c77d715db35..7411058d2a3245cba4cb3b4df1626a4a5bef0566 100644
--- a/uffd/service/views.py
+++ b/uffd/service/views.py
@@ -73,6 +73,7 @@ def edit_submit(id=None):
 	else:
 		service.limit_access = True
 		service.access_group = Group.query.get(request.form['access-group'])
+	service.use_remailer = request.form.get('use_remailer') == '1'
 	db.session.commit()
 	return redirect(url_for('service.show', id=service.id))
 
@@ -157,6 +158,7 @@ def api_submit(service_id, id=None):
 	client.perm_users = request.form.get('perm_users') == '1'
 	client.perm_checkpassword = request.form.get('perm_checkpassword') == '1'
 	client.perm_mail_aliases = request.form.get('perm_mail_aliases') == '1'
+	client.perm_remailer = request.form.get('perm_remailer') == '1'
 	db.session.commit()
 	return redirect(url_for('service.show', id=service.id))
 
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 837caf0baecd3654d28ed65210d9f6a8a1ea2098..b6723ca0244d78a396dc215299589e1d5d40bb87 100644
Binary files a/uffd/translations/de/LC_MESSAGES/messages.mo and b/uffd/translations/de/LC_MESSAGES/messages.mo differ
diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po
index cedf033d87d0713739a4c065b51968b99e62e6d2..9f4f419d66695d464083e44538e1054dd13c4b20 100644
--- a/uffd/translations/de/LC_MESSAGES/messages.po
+++ b/uffd/translations/de/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2022-03-20 13:07+0100\n"
+"POT-Creation-Date: 2022-04-21 14:17+0200\n"
 "PO-Revision-Date: 2021-05-25 21:18+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -114,8 +114,8 @@ msgstr "Mailversand fehlgeschlagen"
 #: uffd/invite/templates/invite/list.html:6
 #: uffd/mail/templates/mail/list.html:8 uffd/role/templates/role/list.html:8
 #: uffd/service/templates/service/index.html:8
-#: uffd/service/templates/service/show.html:41
-#: uffd/service/templates/service/show.html:69
+#: uffd/service/templates/service/show.html:50
+#: uffd/service/templates/service/show.html:78
 #: uffd/user/templates/group/list.html:8 uffd/user/templates/user/list.html:8
 msgid "New"
 msgstr "Neu"
@@ -130,7 +130,7 @@ msgstr "Erstellt durch"
 
 #: uffd/invite/templates/invite/list.html:14
 #: uffd/service/templates/service/api.html:34
-#: uffd/service/templates/service/show.html:76
+#: uffd/service/templates/service/show.html:85
 msgid "Permissions"
 msgstr "Berechtigungen"
 
@@ -292,7 +292,7 @@ msgstr "Enthaltene Rollen"
 #: uffd/selfservice/templates/selfservice/self.html:97
 #: uffd/service/templates/service/index.html:14
 #: uffd/service/templates/service/show.html:20
-#: uffd/service/templates/service/show.html:75
+#: uffd/service/templates/service/show.html:84
 #: uffd/user/templates/group/list.html:15
 #: uffd/user/templates/group/show.html:26
 #: uffd/user/templates/user/show.html:106
@@ -1054,7 +1054,7 @@ msgstr "Passwort vergessen"
 
 #: uffd/selfservice/templates/selfservice/forgot_password.html:9
 #: uffd/selfservice/templates/selfservice/self.html:21
-#: uffd/session/templates/session/login.html:9
+#: uffd/session/templates/session/login.html:12
 #: uffd/signup/templates/signup/start.html:14
 #: uffd/user/templates/user/list.html:18 uffd/user/templates/user/show.html:47
 msgid "Login Name"
@@ -1118,7 +1118,7 @@ msgid "Update Profile"
 msgstr "Änderungen speichern"
 
 #: uffd/selfservice/templates/selfservice/self.html:44
-#: uffd/session/templates/session/login.html:13
+#: uffd/session/templates/session/login.html:16
 #: uffd/signup/templates/signup/start.html:41
 #: uffd/user/templates/user/show.html:76
 msgid "Password"
@@ -1235,8 +1235,17 @@ msgstr "Passwörter von Nutzeraccounts verifizieren"
 msgid "Access mail aliases"
 msgstr "Zugriff auf Mail-Weiterleitungen"
 
+#: uffd/service/templates/service/api.html:49
+msgid "Resolve remailer addresses"
+msgstr "Auflösen von Remailer-Adressen"
+
+#: uffd/service/templates/service/api.html:51
+#: uffd/service/templates/service/show.html:38
+msgid "This option has no effect: Remailer config options are unset"
+msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
+
 #: uffd/service/templates/service/oauth2.html:20
-#: uffd/service/templates/service/show.html:47
+#: uffd/service/templates/service/show.html:56
 msgid "Client ID"
 msgstr "Client-ID"
 
@@ -1262,7 +1271,7 @@ msgstr ""
 "Eine URI pro Zeile, vorangestellt die mit Leerzeichen getrennte HTTP-"
 "Methode (GET/POST)"
 
-#: uffd/service/templates/service/overview.html:8
+#: uffd/service/templates/service/overview.html:11
 msgid ""
 "Some services may not be publicly listed! Log in to see all services you "
 "have access to."
@@ -1270,15 +1279,21 @@ msgstr ""
 "Einige Dienste sind eventuell nicht öffentlich aufgelistet! Melde dich an"
 " um alle Dienste zu sehen, auf die du Zugriff hast."
 
-#: uffd/service/templates/service/overview.html:44
+#: uffd/service/templates/service/overview.html:15
+#: uffd/session/templates/session/login.html:6
+#: uffd/session/templates/session/login.html:20 uffd/templates/base.html:106
+msgid "Login"
+msgstr "Anmelden"
+
+#: uffd/service/templates/service/overview.html:55
 msgid "No access"
 msgstr "Kein Zugriff"
 
-#: uffd/service/templates/service/overview.html:64
+#: uffd/service/templates/service/overview.html:75
 msgid "Manage OAuth2 and API clients"
 msgstr "OAuth2- und API-Clients verwalten"
 
-#: uffd/service/templates/service/overview.html:84
+#: uffd/service/templates/service/overview.html:95
 #: uffd/user/templates/user/list.html:58 uffd/user/templates/user/list.html:79
 msgid "Close"
 msgstr "Schließen"
@@ -1300,6 +1315,10 @@ msgstr "Alle Account haben Zugriff (veraltet)"
 msgid "Members of group \"%(group_name)s\" have access"
 msgstr "Mitglieder der Gruppe \"%(group_name)s\" haben Zugriff"
 
+#: uffd/service/templates/service/show.html:36
+msgid "Hide mail addresses with remailer"
+msgstr "Verstecke Mailadressen mit dem Remailer"
+
 #: uffd/session/views.py:71
 #, python-format
 msgid ""
@@ -1414,24 +1433,19 @@ msgstr ""
 "auf dem anderen Gerät und gib dort den obenstehenden Startcode ein. Geben"
 " anschließend den Bestätigungscode hier ein."
 
-#: uffd/session/templates/session/login.html:6
-#: uffd/session/templates/session/login.html:17
-msgid "Login"
-msgstr "Anmelden"
-
-#: uffd/session/templates/session/login.html:20
+#: uffd/session/templates/session/login.html:23
 msgid "- or -"
 msgstr "- oder -"
 
-#: uffd/session/templates/session/login.html:22
+#: uffd/session/templates/session/login.html:25
 msgid "Login with another device"
 msgstr "Über anderes Gerät anmelden"
 
-#: uffd/session/templates/session/login.html:27
+#: uffd/session/templates/session/login.html:30
 msgid "Register"
 msgstr "Registrieren"
 
-#: uffd/session/templates/session/login.html:29
+#: uffd/session/templates/session/login.html:32
 msgid "Forgot Password?"
 msgstr "Passwort vergessen?"
 
@@ -1569,7 +1583,7 @@ msgstr "Du bist nicht berechtigt, auf diese Seite zuzugreifen."
 msgid "Change"
 msgstr "Ändern"
 
-#: uffd/templates/base.html:135
+#: uffd/templates/base.html:142
 msgid "About uffd"
 msgstr "Über uffd"
 
diff --git a/uffd/user/models.py b/uffd/user/models.py
index 900ce743865941dd29ded81a3d0d0c93882e51c7..8ff69508929b1714087c940b25a01871c85a7bf5 100644
--- a/uffd/user/models.py
+++ b/uffd/user/models.py
@@ -2,6 +2,7 @@ import string
 import re
 
 from flask import current_app, escape
+import itsdangerous
 from flask_babel import lazy_gettext
 from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Text
 from sqlalchemy.orm import relationship
@@ -97,13 +98,99 @@ class User(db.Model):
 	def set_mail(self, value):
 		if len(value) < 3 or '@' not in value:
 			return False
+		domain = value.rsplit('@', 1)[-1]
+		if remailer.is_remailer_domain(domain):
+			return False
 		self.mail = value
 		return True
 
+	def get_service_mail(self, service):
+		if not remailer.configured or not service.use_remailer:
+			return self.mail
+		if current_app.config['REMAILER_LIMIT_TO_USERS'] is not None and \
+				self.loginname not in current_app.config['REMAILER_LIMIT_TO_USERS']:
+			return self.mail
+		return remailer.build_address(self, service)
+
+	@classmethod
+	def filter_by_service_mail(cls, service, address):
+		if not remailer.configured or not service.use_remailer:
+			return cls.mail == address
+		remailer_address = remailer.parse_address(address)
+		if remailer_address and remailer_address.service == service and \
+				remailer_address.user.get_service_mail(service) == address:
+			return cls.id == remailer_address.user.id
+		if current_app.config['REMAILER_LIMIT_TO_USERS'] is not None:
+			return db.and_(
+				db.not_(cls.loginname.in_(current_app.config['REMAILER_LIMIT_TO_USERS'])),
+				cls.mail == address
+			)
+		return False
+
 	# Somehow pylint non-deterministically fails to detect that .update_groups is set in invite.modes
 	def update_groups(self):
 		pass
 
+class RemailerAddress:
+	def __init__(self, user, service):
+		self.user = user
+		self.service = service
+
+	def __eq__(self, remailer_address):
+		return remailer_address is not None and self.user == remailer_address.user and self.service == remailer_address.service
+
+class Remailer:
+	'''The remailer feature improves user privacy by hiding real mail addresses
+	from services and instead providing them with autogenerated pseudonymous
+	remailer addresses. If a service sends a mail to a remailer address, the mail
+	service uses an uffd API endpoint to get the real mail address and rewrites
+	the remailer address with it. In case of a leak of user data from a service,
+	the remailer addresses are useless for third-parties.'''
+
+	# pylint: disable=no-self-use
+
+	@property
+	def configured(self):
+		return bool(current_app.config['REMAILER_DOMAIN'])
+
+	def get_serializer(self):
+		secret = current_app.config['REMAILER_SECRET_KEY'] or current_app.secret_key
+		return itsdangerous.URLSafeSerializer(secret, salt='remailer_address_v1')
+
+	def build_local_part(self, user, service):
+		return 'v1-' + self.get_serializer().dumps([service.id, user.id])
+
+	def build_address(self, user, service):
+		return self.build_local_part(user, service) + '@' + current_app.config['REMAILER_DOMAIN']
+
+	def is_remailer_domain(self, domain):
+		domains = {domain.lower().strip() for domain in current_app.config['REMAILER_OLD_DOMAINS']}
+		if current_app.config['REMAILER_DOMAIN']:
+			domains.add(current_app.config['REMAILER_DOMAIN'].lower().strip())
+		return domain.lower().strip() in domains
+
+	def parse_address(self, address):
+		# With a top-level import we get circular import problems
+		# pylint: disable=import-outside-toplevel
+		from uffd.service.models import Service
+		if '@' not in address:
+			return None
+		local_part, domain = address.rsplit('@', 1)
+		if not self.is_remailer_domain(domain) or not local_part.startswith('v1-'):
+			return None
+		data = local_part[len('v1-'):]
+		try:
+			service_id, user_id = self.get_serializer().loads(data)
+		except itsdangerous.BadData:
+			return None
+		service = Service.query.get(service_id)
+		user = User.query.get(user_id)
+		if not service or not user:
+			return None
+		return RemailerAddress(user, service)
+
+remailer = Remailer()
+
 def next_id_expr(column, min_value, max_value):
 	# db.func.max(column) + 1: highest used value in range + 1, NULL if no values in range
 	# db.func.min(..., max_value): clip to range
diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py
index 95f8643e625fa404156fafab9297737e28115587..d9a8800b3738b40882babef1ce3f2203ad6627d7 100644
--- a/uffd/user/views_user.py
+++ b/uffd/user/views_user.py
@@ -12,11 +12,12 @@ from uffd.session import login_required
 from uffd.role.models import Role
 from uffd.database import db
 
-from .models import User
+from .models import User, remailer
 
 bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')
 
 bp.add_app_template_global(User, 'User')
+bp.add_app_template_global(remailer, 'remailer')
 
 def user_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])