From b391e176db7f1e8326910b9f89bfa3d955fc2001 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@cccv.de>
Date: Wed, 19 Oct 2022 22:18:54 +0200
Subject: [PATCH] Per-service email preferences

Also fixes a minor email-related bug in the admin interface and bad
texts/translations in the selfservice UI.
---
 tests/test_selfservice.py                     |  73 +++++---
 tests/test_services.py                        |  70 +++++++-
 tests/test_user.py                            |  11 +-
 ...b733ec856_per_service_email_preferences.py |  42 +++++
 uffd/models/service.py                        |  30 +++-
 uffd/templates/selfservice/self.html          |  44 ++++-
 uffd/templates/service/show.html              |   7 +
 uffd/templates/user/show.html                 |  13 +-
 uffd/translations/de/LC_MESSAGES/messages.mo  | Bin 38331 -> 38702 bytes
 uffd/translations/de/LC_MESSAGES/messages.po  | 170 ++++++++++--------
 uffd/views/selfservice.py                     |  18 +-
 uffd/views/service.py                         |   1 +
 uffd/views/user.py                            |  20 ++-
 13 files changed, 378 insertions(+), 121 deletions(-)
 create mode 100644 uffd/migrations/versions/e13b733ec856_per_service_email_preferences.py

diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py
index b31e579e..6f686f4b 100644
--- a/tests/test_selfservice.py
+++ b/tests/test_selfservice.py
@@ -5,11 +5,10 @@ import unittest
 from flask import url_for, request
 
 from uffd import create_app, db
-from uffd.models import PasswordToken, User, UserEmail, Role, RoleGroup
+from uffd.models import PasswordToken, User, UserEmail, Role, RoleGroup, Service, ServiceUser
 
 from utils import dump, UffdTestCase
 
-
 class TestSelfservice(UffdTestCase):
 	def test_index(self):
 		self.login_as('user')
@@ -185,10 +184,14 @@ class TestSelfservice(UffdTestCase):
 
 	def test_update_email_preferences(self):
 		self.login_as('user')
+		user_id = self.get_user().id
 		email = UserEmail(user=self.get_user(), address='new@example.com', verified=True)
 		db.session.add(email)
+		service = Service(name='service', enable_email_preferences=True)
+		db.session.add(service)
 		db.session.commit()
 		email_id = email.id
+		service_id = service.id
 		old_email_id = self.get_user().primary_email.id
 		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
 			data={'primary_email': str(email_id), 'recovery_email': 'primary'},
@@ -203,42 +206,68 @@ class TestSelfservice(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(self.get_user().primary_email.id, old_email_id)
 		self.assertEqual(self.get_user().recovery_email.id, email_id)
+		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+			data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': 'primary'},
+			follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertIsNone(ServiceUser.query.get((service_id, user_id)).service_email)
+		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+			data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': str(email_id)},
+			follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(ServiceUser.query.get((service_id, user_id)).service_email.id, email_id)
 
 	def test_update_email_preferences_unverified(self):
 		self.login_as('user')
+		user_id = self.get_user().id
 		email = UserEmail(user=self.get_user(), address='new@example.com')
 		db.session.add(email)
+		service = Service(name='service', enable_email_preferences=True)
+		db.session.add(service)
 		db.session.commit()
 		email_id = email.id
+		service_id = service.id
 		old_email_id = self.get_user().primary_email.id
-		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
-			data={'primary_email': str(email_id), 'recovery_email': 'primary'},
-			follow_redirects=True)
-		self.assertEqual(r.status_code, 400)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': str(email_id), 'recovery_email': 'primary'},
+				follow_redirects=True)
 		self.assertEqual(self.get_user().primary_email.address, 'test@example.com')
-		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
-			data={'primary_email': str(old_email_id), 'recovery_email': str(email_id)},
-			follow_redirects=True)
-		self.assertEqual(r.status_code, 400)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': str(old_email_id), 'recovery_email': str(email_id)},
+				follow_redirects=True)
 		self.assertIsNone(self.get_user().recovery_email)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': str(email_id)},
+				follow_redirects=True)
+		self.assertIsNone(ServiceUser.query.get((service_id, user_id)).service_email)
 
 	def test_update_email_preferences_invalid(self):
 		self.login_as('user')
+		user_id = self.get_user().id
 		email = UserEmail(user=self.get_user(), address='new@example.com', verified=True)
 		db.session.add(email)
+		service = Service(name='service', enable_email_preferences=True)
+		db.session.add(service)
 		db.session.commit()
-		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
-			data={'primary_email': str(email.id), 'recovery_email': '2342'},
-			follow_redirects=True)
-		self.assertEqual(r.status_code, 400)
-		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
-			data={'primary_email': 'primary', 'recovery_email': 'primary'},
-			follow_redirects=True)
-		self.assertEqual(r.status_code, 400)
-		r = self.client.post(path=url_for('selfservice.update_email_preferences'),
-			data={'primary_email': '2342', 'recovery_email': 'primary'},
-			follow_redirects=True)
-		self.assertEqual(r.status_code, 400)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': str(email.id), 'recovery_email': '2342'},
+				follow_redirects=True)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': str(email.id), 'recovery_email': 'primary', f'service_{service_id}_email': '2342'},
+				follow_redirects=True)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': 'primary', 'recovery_email': 'primary'},
+				follow_redirects=True)
+		with self.assertRaises(Exception):
+			r = self.client.post(path=url_for('selfservice.update_email_preferences'),
+				data={'primary_email': '2342', 'recovery_email': 'primary'},
+				follow_redirects=True)
 
 	def test_change_password(self):
 		self.login_as('user')
diff --git a/tests/test_services.py b/tests/test_services.py
index 0c697023..14ec4f70 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -7,7 +7,7 @@ from utils import dump, UffdTestCase
 from uffd.remailer import remailer
 from uffd.tasks import cleanup_task
 from uffd.database import db
-from uffd.models import Service, ServiceUser, User
+from uffd.models import Service, ServiceUser, User, UserEmail
 
 class TestServiceUser(UffdTestCase):
 	def setUp(self):
@@ -43,11 +43,26 @@ class TestServiceUser(UffdTestCase):
 		db.session.commit()
 		self.assertEqual(ServiceUser.query.count(), service_count  * user_count)
 
+	def test_service_email(self):
+		user = self.get_user()
+		service = Service.query.filter_by(name='service1').first()
+		service_user = ServiceUser.query.get((service.id, user.id))
+		self.assertEqual(service_user.service_email, None)
+		service_user.service_email = UserEmail(user=user, address='foo@bar', verified=True)
+		with self.assertRaises(Exception):
+			service_user.service_email = UserEmail(user=user, address='foo2@bar', verified=False)
+		with self.assertRaises(Exception):
+			service_user.service_email = UserEmail(user=self.get_admin(), address='foo3@bar', verified=True)
+
 	def test_real_email(self):
 		user = self.get_user()
 		service = Service.query.filter_by(name='service1').first()
 		service_user = ServiceUser.query.get((service.id, user.id))
 		self.assertEqual(service_user.real_email, user.primary_email.address)
+		service_user.service_email = UserEmail(user=user, address='foo@bar', verified=True)
+		self.assertEqual(service_user.real_email, user.primary_email.address)
+		service.enable_email_preferences = True
+		self.assertEqual(service_user.real_email, service_user.service_email.address)
 
 	def test_remailer_email(self):
 		user = self.get_user()
@@ -161,6 +176,59 @@ class TestServiceUser(UffdTestCase):
 		self.assertEqual(run_query(remailer_email2_1), set())
 		self.assertEqual(run_query('invalid'), set())
 
+	def test_filter_query_by_email_prefs(self):
+		def run_query(value):
+			return {(su.service_id, su.user_id) for su in ServiceUser.filter_query_by_email(ServiceUser.query, value)}
+
+		user1 = self.get_user()
+		service1 = Service.query.filter_by(name='service1').first() # use_remailer=False
+		service2 = Service.query.filter_by(name='service2').first() # use_remailer=True
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		remailer_email1_1 = remailer.build_address(service1.id, user1.id)
+		remailer_email2_1 = remailer.build_address(service2.id, user1.id)
+
+		self.app.config['REMAILER_DOMAIN'] = ''
+		self.assertEqual(run_query(user1.primary_email.address), {
+			(service1.id, user1.id),
+			(service2.id, user1.id),
+		})
+		self.assertEqual(run_query('addr1-1@example.com'), set())
+		self.assertEqual(run_query('addr2-1@example.com'), set())
+		self.assertEqual(run_query(remailer_email1_1), set())
+		self.assertEqual(run_query(remailer_email2_1), set())
+
+		ServiceUser.query.get((service1.id, user1.id)).service_email = UserEmail(user=user1, verified=True, address='addr1-1@example.com')
+		ServiceUser.query.get((service2.id, user1.id)).service_email = UserEmail(user=user1, verified=True, address='addr2-1@example.com')
+		self.assertEqual(run_query(user1.primary_email.address), {
+			(service1.id, user1.id),
+			(service2.id, user1.id),
+		})
+		self.assertEqual(run_query('addr1-1@example.com'), set())
+		self.assertEqual(run_query('addr2-1@example.com'), set())
+		self.assertEqual(run_query(remailer_email1_1), set())
+		self.assertEqual(run_query(remailer_email2_1), set())
+		service1.enable_email_preferences = True
+		service2.enable_email_preferences = True
+		self.assertEqual(run_query(user1.primary_email.address), set())
+		self.assertEqual(run_query('addr1-1@example.com'), {
+			(service1.id, user1.id),
+		})
+		self.assertEqual(run_query('addr2-1@example.com'), {
+			(service2.id, user1.id),
+		})
+		self.assertEqual(run_query(remailer_email1_1), set())
+		self.assertEqual(run_query(remailer_email2_1), set())
+		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
+		self.assertEqual(run_query(user1.primary_email.address), set())
+		self.assertEqual(run_query('addr1-1@example.com'), {
+			(service1.id, user1.id),
+		})
+		self.assertEqual(run_query('addr2-1@example.com'), set())
+		self.assertEqual(run_query(remailer_email1_1), set())
+		self.assertEqual(run_query(remailer_email2_1), {
+			(service2.id, user1.id),
+		})
+
 class TestServices(UffdTestCase):
 	def setUpApp(self):
 		self.app.config['SERVICES'] = [
diff --git a/tests/test_user.py b/tests/test_user.py
index b023510c..81f3a292 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -6,7 +6,7 @@ import sqlalchemy
 
 from uffd import create_app, db
 from uffd.remailer import remailer
-from uffd.models import User, UserEmail, Group, Role, RoleGroup, Service
+from uffd.models import User, UserEmail, Group, Role, RoleGroup, Service, ServiceUser
 
 from utils import dump, UffdTestCase
 
@@ -346,9 +346,14 @@ class TestUserViews(UffdTestCase):
 	def test_update_email(self):
 		user = self.get_user()
 		email = UserEmail(user=user, address='foo@example.com')
+		service1 = Service(name='service1', enable_email_preferences=True)
+		service2 = Service(name='service2', enable_email_preferences=True)
+		db.session.add_all([service1, service2])
 		db.session.commit()
 		email1_id = user.primary_email.id
 		email2_id = email.id
+		service1_id = service1.id
+		service2_id = service2.id
 		r = self.client.post(path=url_for('user.update', id=user.id),
 			data={'loginname': 'testuser',
 			f'email-{email1_id}-present': '1',
@@ -357,6 +362,8 @@ class TestUserViews(UffdTestCase):
 			f'newemail-1-address': 'new1@example.com',
 			f'newemail-2-address': 'new2@example.com', f'newemail-2-verified': '1',
 			'primary_email': email2_id, 'recovery_email': email1_id,
+			f'service_{service1_id}_email': 'primary',
+			f'service_{service2_id}_email': email2_id,
 			'displayname': 'Test User', 'password': ''},
 			follow_redirects=True)
 		dump('user_update_email', r)
@@ -364,6 +371,8 @@ class TestUserViews(UffdTestCase):
 		user = self.get_user()
 		self.assertEqual(user.primary_email.id, email2_id)
 		self.assertEqual(user.recovery_email.id, email1_id)
+		self.assertEqual(ServiceUser.query.get((service1.id, user.id)).service_email, None)
+		self.assertEqual(ServiceUser.query.get((service2.id, user.id)).service_email.id, email2_id)
 		self.assertEqual(
 			{email.address: email.verified for email in user.all_emails},
 			{
diff --git a/uffd/migrations/versions/e13b733ec856_per_service_email_preferences.py b/uffd/migrations/versions/e13b733ec856_per_service_email_preferences.py
new file mode 100644
index 00000000..e7afe43e
--- /dev/null
+++ b/uffd/migrations/versions/e13b733ec856_per_service_email_preferences.py
@@ -0,0 +1,42 @@
+"""Per-service email preferences
+
+Revision ID: e13b733ec856
+Revises: b273d7fdaa25
+Create Date: 2022-10-17 02:13:11.598210
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = 'e13b733ec856'
+down_revision = 'b273d7fdaa25'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+	with op.batch_alter_table('service', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('enable_email_preferences', sa.Boolean(), nullable=False, server_default=sa.false()))
+	with op.batch_alter_table('service_user', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('service_email_id', sa.Integer(), nullable=True))
+		batch_op.create_foreign_key(batch_op.f('fk_service_user_service_email_id_user_email'), 'user_email', ['service_email_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL')
+	meta = sa.MetaData(bind=op.get_bind())
+	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.Column('enable_email_preferences', sa.Boolean(), nullable=False, server_default=sa.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.alter_column('enable_email_preferences', server_default=None)
+
+def downgrade():
+	with op.batch_alter_table('service_user', schema=None) as batch_op:
+		batch_op.drop_constraint(batch_op.f('fk_service_user_service_email_id_user_email'), type_='foreignkey')
+		batch_op.drop_column('service_email_id')
+	with op.batch_alter_table('service', schema=None) as batch_op:
+		batch_op.drop_column('enable_email_preferences')
diff --git a/uffd/models/service.py b/uffd/models/service.py
index 131400f0..f6927e28 100644
--- a/uffd/models/service.py
+++ b/uffd/models/service.py
@@ -1,7 +1,7 @@
 from flask import current_app
 from flask_babel import get_locale
 from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
-from sqlalchemy.orm import relationship
+from sqlalchemy.orm import relationship, validates
 
 from uffd.database import db
 from uffd.remailer import remailer
@@ -27,6 +27,7 @@ class Service(db.Model):
 	api_clients = relationship('APIClient', back_populates='service', cascade='all, delete-orphan')
 
 	use_remailer = Column(Boolean(), default=False, nullable=False)
+	enable_email_preferences = Column(Boolean(), default=False, nullable=False)
 
 class ServiceUser(db.Model):
 	'''Service-related configuration and state for a user
@@ -50,9 +51,25 @@ class ServiceUser(db.Model):
 	def has_access(self):
 		return not self.service.limit_access or self.service.access_group in self.user.groups
 
+	service_email_id = Column(Integer(), ForeignKey('user_email.id', onupdate='CASCADE', ondelete='SET NULL'))
+	service_email = relationship('UserEmail')
+
+	@validates('service_email')
+	def validate_service_email(self, key, value): # pylint: disable=unused-argument
+		if value is not None:
+			if not value.user:
+				value.user = self.user
+			if value.user != self.user:
+				raise ValueError('UserEmail assigned to ServiceUser.service_email is not associated with user')
+			if not value.verified:
+				raise ValueError('UserEmail assigned to serviceUser.service_email is not verified')
+		return  value
+
 	# Actual e-mail address that mails from the service are sent to
 	@property
 	def real_email(self):
+		if self.service.enable_email_preferences and self.service_email:
+			return self.service_email.address
 		return self.user.primary_email.address
 
 	@property
@@ -92,10 +109,12 @@ class ServiceUser(db.Model):
 
 		AliasedUser = db.aliased(User)
 		AliasedPrimaryEmail = db.aliased(UserEmail)
+		AliasedServiceEmail = db.aliased(UserEmail)
 		AliasedService = db.aliased(Service)
 
 		query = query.join(cls.user.of_type(AliasedUser))
 		query = query.join(AliasedUser.primary_email.of_type(AliasedPrimaryEmail))
+		query = query.outerjoin(cls.service_email.of_type(AliasedServiceEmail))
 		query = query.join(cls.service.of_type(AliasedService))
 
 		remailer_enabled_expr = AliasedService.use_remailer if remailer.configured else False
@@ -104,7 +123,14 @@ class ServiceUser(db.Model):
 				remailer_enabled_expr,
 				AliasedUser.loginname.in_(current_app.config['REMAILER_LIMIT_TO_USERS']),
 			)
-		return query.filter(db.and_(db.not_(remailer_enabled_expr), AliasedPrimaryEmail.address == email))
+		real_email_matches_expr = db.case(
+			whens=(
+				# pylint: disable=singleton-comparison
+				(db.and_(AliasedService.enable_email_preferences, cls.service_email != None), AliasedServiceEmail.address == email),
+			),
+			else_=(AliasedPrimaryEmail.address == email)
+		)
+		return query.filter(db.and_(db.not_(remailer_enabled_expr), real_email_matches_expr))
 
 @db.event.listens_for(db.Session, 'after_flush') # pylint: disable=no-member
 def create_service_users(session, flush_context): # pylint: disable=unused-argument
diff --git a/uffd/templates/selfservice/self.html b/uffd/templates/selfservice/self.html
index 39a36499..6fcb2ed2 100644
--- a/uffd/templates/selfservice/self.html
+++ b/uffd/templates/selfservice/self.html
@@ -12,7 +12,7 @@
 <div class="row">
 	<div class="col-12 col-md-5">
 		<h5>{{_("Profile")}}</h5>
-		<p>{{_("Your profile information is used by all services that are integrated into the Single-Sign-On. Your e-mail address is also used for password recovery.")}}</p>
+		<p>{{_("Your profile information is used by all services that are integrated into the Single-Sign-On.")}}</p>
 		<p>{{_("Changes may take several minutes to be visible in all services.")}}</p>
 	</div>
 	<div class="col-12 col-md-7">
@@ -77,7 +77,13 @@
 <div class="row">
 	<div class="col-12 col-md-5">
 		<h5>{{_("E-Mail Preferences")}}</h5>
-		<p>{{_("Choose which of your verified address to use as your primary or recovery address.")}}</p>
+		<p>
+			{{ _("Choose your primary e-mail address and the address password recovery e-mails will be sent to.") }}
+			{% if user.service_users|map(attribute='service')|selectattr('enable_email_preferences')|list|length %}
+			{{ _("You can also select different addresses for different services.") }}
+			{% endif %}
+		</p>
+		<p>{{ _("Adresses must be verified before you can select them here.") }}</p>
 	</div>
 	<div class="col-12 col-md-7">
 		<form class="form" action="{{ url_for("selfservice.update_email_preferences") }}" method="POST">
@@ -90,15 +96,33 @@
 				</select>
 			</div>
 			<div class="form-group">
-				<label>{{_("Recovery Address")}}</label>
+				<label>{{_("Address for Password Reset E-Mails")}}</label>
 				<select name="recovery_email" class="form-control">
 					<option value="primary" {{ 'selected' if not user.recovery_email }}>{{ _('Use primary address') }}</option>
 					{% for email in user.all_emails if email.verified %}
 					<option value="{{ email.id }}" {{ 'selected' if email == user.recovery_email }}>{{ email.address }}</option>
 					{% endfor %}
 				</select>
-				<small class="text-muted">{{ _('Password reset e-mails will be sent to this address') }}</small>
 			</div>
+			{% set collapse_email_prefs = user.service_users|map(attribute='service')|selectattr('enable_email_preferences')|list|length > 2 %}
+			{% for service_user in user.service_users if service_user.service.enable_email_preferences %}
+			{% if collapse_email_prefs and loop.index == 2 %}
+			<div id="collapsed-email-prefs">
+			{% endif %}
+			<div class="form-group">
+				<label>{{ _('Address for Service "%(name)s"', name=service_user.service.name) }}</label>
+				<select name="service_{{ service_user.service.id }}_email" class="form-control">
+					<option value="primary" {{ 'selected' if not service_user.service_email }}>{{ _('Use primary address') }}</option>
+					{% for email in user.all_emails if email.verified %}
+					<option value="{{ email.id }}" {{ 'selected' if email == service_user.service_email }}>{{ email.address }}</option>
+					{% endfor %}
+				</select>
+			</div>
+			{% endfor %}
+			{% if collapse_email_prefs %}
+			</div>
+			<button type="button" class="btn btn-sm btn-link pl-0 mb-1 showmore" data-target="#collapsed-email-prefs" style="display: none;" aria-expanded="false" aria-controls="collapsed-email-prefs">{{ _("Show more settings ...") }}</button>
+			{% endif %}
 			<button type="submit" class="btn btn-primary btn-block">{{_("Update E-Mail Preferences")}}</button>
 		</form>
 	</div>
@@ -194,4 +218,16 @@
 	</div>
 </div>
 
+<script>
+$(".showmore").each(function () {
+	$(this).show()
+	$($(this).data("target")).hide()
+})
+$(".showmore").on("click", function () {
+	$(this).slideUp(200)
+	$(this).prop("ariaExpanded", true)
+	$($(this).data("target")).slideDown()
+})
+</script>
+
 {% endblock %}
diff --git a/uffd/templates/service/show.html b/uffd/templates/service/show.html
index c1f075de..55a12328 100644
--- a/uffd/templates/service/show.html
+++ b/uffd/templates/service/show.html
@@ -39,6 +39,13 @@
 				{% endif %}
 			</div>
 		</div>
+		<div class="form-group col">
+			<div class="form-check">
+				<input class="form-check-input" type="checkbox" id="service-enable-email-preferences" name="enable_email_preferences" value="1" aria-label="enabled" {{ 'checked' if service.enable_email_preferences }}>
+				<label class="form-check-label" for="service-enable-email-preferences">{{ _('Enable user mail preferences') }}</label>
+			</div>
+		</div>
+
 	</form>
 
 	{% if service.id %}
diff --git a/uffd/templates/user/show.html b/uffd/templates/user/show.html
index 8b814f5d..30a81738 100644
--- a/uffd/templates/user/show.html
+++ b/uffd/templates/user/show.html
@@ -142,6 +142,17 @@
 					{% endfor %}
 				</select>
 			</div>
+			{% for service_user in user.service_users if service_user.service.enable_email_preferences %}
+			<div class="form-group col">
+				<label>{{ _("Address for %(name)s", name=service_user.service.name) }}</label>
+				<select name="service_{{ service_user.service.id }}_email" class="form-control">
+					<option value="primary" {{ 'selected' if not service_user.service_email }}>{{ _('Use primary address') }}</option>
+					{% for email in user.all_emails if email.verified %}
+					<option value="{{ email.id }}" {{ 'selected' if email == service_user.service_email }}>{{ email.address }}</option>
+					{% endfor %}
+				</select>
+			</div>
+			{% endfor %}
 			{% endif %}
 
 			<div class="form-group col">
@@ -239,7 +250,7 @@ $('#email-rows').on('click', '.delete-new-email-row', function () {
 let new_email_id = 1;
 $('#email-rows').on('input', 'tr:last input', function () {
 	$('#email-rows tr:last button.delete-new-email-row').removeClass('d-none');
-	$('#email-rows').append({{ new_email_row('TMPID')|tojson }}.replace('TMPID', new_email_id));
+	$('#email-rows').append({{ new_email_row('TMPID')|tojson }}.replace(/TMPID/g, new_email_id));
 	new_email_id ++;
 });
 </script>
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index d40e990ed5133f9eed8b87e4832fabb4c5e23389..8187048476a9226c2764f6b1fe76560a34e30c9d 100644
GIT binary patch
delta 7376
zcmdnJnrYoSruutAEK?a67#O-385m?37#MPRKs*HABf-GH&%nU2SAu~-n1O-etONrC
z8v_HwLkR{39|i`77ZMB%AT6en3=CWh3=B?^5WcS@1A`U=149&4yj7BcftP`SVS*$B
zgAfA)!#qi_dG!n%Aq<A2k_-$|3=9l6B^eml85kJ8NHQ>pFfcIukz`=tW?*0tlwx40
zW?*2Dm11B}WME*JDFsotTMDA?uoMG>5(5Ln4Jif&IR*xX-%<<=_6!URQql|zRv?E+
zGcXh~FfdG&hRCzafE-lMz@R3>z!1&Az+fW7z_1+TLKy}IcLoLq3t0w+S_TG&23d%~
z|6~~$#2FYE6y+ear5po;7y|=Cpd15(2?GN|h8)BLbLAKq+!z=bcE~X>NHQ=m{D-QO
zl!t_*fjk3)1p@<vgFFKRS3Ls*L!~?<NL%C?7*rS-7`mbS4Nwi+<sm*fF3-Tg%D}+D
zuE4+$%fP_EtH8j($-uyntpM@~14F3-M7~*pfq{jAfuU1@fx(c0fuT<UV(|e51_oIM
z28Ig?3=E<S3=HoS7#QlM85kH?6d4!<85kH8ptO-90|N&G1A~hq#D~6$3=AN3DT?4Y
zV`xxhVBlk5V3?u^iJHZVkhotD)wfF#;-SNe5Qm*ogjje(5t26ELG}GsWT*$nF}o5(
zLIz4}DnWwIOo@R(lYxODKnY@Tl@bGkCCEog5DQK~)t^^_q=`F95QqPRinA+2EEZ8_
zU{GgZV9-*AL|u?FB&xEM>mdqSl_5Tw4W(BpGcX7+Ffi;?hNRY0%8;PDs|*Q|-^vgN
z3aLO0)KP&L=%T{FpwGa-5U&DJ-><^JpuoVuuvP^UHRn_q7_1o>7_O*5qEw(>6_V`&
zR3WK3SCxSwlYxPu6w1G^3h~i%Rfvy2s6rg}T@@0i9BPm#kWgb_U}j)oP*H<Km4+H5
zmAj}xEKF8|M8$kHh<R(F{Q7NB#t}70P+wAmSoB8?5+cm%kf0S(XJEJr$|dRy3>6Fv
z3|txv3=<g`7@9R87K&;@^ciY0Ferm^k0!){F`5u_a-jTbO-OF))?{GV%)n63unsDb
zqy-6qA}vVJR%t<kc8V4x?$>BR(#QcVNC=$Of;jk|79<2Yv?1zLwINYtsSObi(1tiH
zR~r&iB~Wz@+8~G3GcZiihWK!?HUonO0|UcRZAdnJ2Q^4o2NG9WIuHkD=|BuD(qUlG
z24!CzNZc;ffn>YYI*@F+K?f4|yL1>BR2di;ZtFli#Hb628cAIS1{YBNx6p;8&RSha
z&@I)47<f__lC91|<!|Xi3}VuQBw7tU1_pTs1_nDlNQlMhK@wqw9>jr@^&mc;r3dlY
zGChcc&g(%!@`D}&Lp`V<_^ZdjFol7EL0F%Gp@V^eVTC><^%@#LH252Uf{KBmzyJ~g
z^#%+Kx(o~q?FJABZZ&|!^#LgVf&s+h+Xj%3cy0j66(0>CA*5)?z!1v7z@TQxP!Fy&
zY78N%x7`qu-MS4SE}UlwF=(|RB+j=(<xdzwLh805#Df2ZkdWXsf<&FB5yTunBZ$64
zBL)UX1_p*ABL;>>1_p*LMvywg%or5^3=9n3#taM_85kHI7(=qhBohcd+XQ035)(+`
z+G+wxbSF(9iR?3!&t(d6sEH}W;SQ#dG~;W^z>v<sz|d<738Am15Phs>5cM+kW{@D&
zFoRfNU<L_l7c)p=2{vP3$Oo12W(*8%phRQ_$;V0N5W2t|;?O#Ch{5ep@p<MD^VXO%
zFxWCMFdQ&vU@&4}VEAXwz#zfEz+h+rNh9@M77&*OT0nv_(E{RtYzs&#t+Ie5s+ks$
z`h1H8Br)B$fCMpzC4`o?gy`3^ggDFs%6GGbB<281NYNc;2{CViCD@#LhSipkL~+Fu
z;-Z_D3=9#VsIY_tk);*H#jaM6ppApl*;bG^tgwQFK(7^~5}RfP@xThGy6sjB3^N!Q
z7>+>I#acrgm}U)%+Ij|t5^IP-t=5nroMjDh$VO|3&vsZtQu%49{55Mx%jc~%q$ISm
zfjD@x4Fkg@1_lNmTZsPowveb@2Bp{8LhRXQ3#pb*+cGe;g7W`gTLy*+prX_cl8;5~
zA#tZ?&%m%4)TFX!U<hDfV0dQFz+l6`z+mpcz+k|@z)<1<NsLP!AVGWH0n%{cbcC1_
z=m^Qa*^ZFZ-{Z)@;K0DZaLy5ud-$Cg80tX{1!E^j99cL)3Klyj28Oo`3=APo3=HNV
z2RK8@gDPi8Hl5)N3OWXcInEG^);dFi{J1m3=hvJW81fhx816bVFnBO9F!;DIFnEEQ
zUM>)iJ#c}9%oi7idImdC8_xw2x7Mx@hj>6~KUYW^2yulJ(Fv{)mp8dW66Z2kNZcQF
zg~a_iSBL{1xI!HE+!f*>MmLDKxEmx&<=i0I)6lJ+fgy^4fuYb1(gr*Or8V3kaoOU|
zz>vzoz##6yz+lF}z)<VKzz_o}Ks+F2zn~`r!!ZU123t>vk6FARZNFkK28I#_28LZ;
z3=GB$3=I0-kPu4thD1e?H^c#3>b)V=;TLZP1~rgHK9Iy^=L5;F-98Kqt_%ze`+Oi}
zKZ`E|IK;GkA=PfCF9Sm<0|P^=FC;De_Jx!is(#??#bE0PDXP=_7#Jc!O*TIUhD1=?
z4@%bu_(Kfd<j=rx1k`-?hcqtd20*gO?*K?pat1=8LOBqUUG)PYL2nWW$sM+V3=B*R
z3=AHDkht}R(!o$V8mcZi5E9jSf#7ndo}oIBfuWa?fuTDP5{Ht(kVK*#46(pI7!u?j
z!H`6^G#KKLZNU%+9tvh)Fa@=0gCP!K3xUM3U<f26R6-!Rz$OIZk=PJONSB9z(po(O
z!@LkkB3Tjwv3OSq1H&0mHVJ{0c(X$xJ~|c(QFksB68BF+AyFp~1_@f@Fi70@hA}Y2
zf!cUskdQSFhvfUna0Z5C1_p+_a0Z4o3=9mH!WkGq>5rj4f`Orkfq~&z1f-g@jD!@C
znURnJsU;FpyUmV-wB>e3LRvQ8BN-Ti85kHuq9AD@BMRccmMBOF%!-19%-SeO$UKgM
z6w&XZAc^uv6azy&sH>$D4GCJ4XowG7qaktB6V1R72I^`>Lo|Mfh6MSqXo!IvF_5$%
z90N(s5;2hKHzEcSGPO{8K@7x0hhrd7d@lwP(jQ_N>cO2!mRN|v+_4Y?Bx4~O>|&uo
z7YoTgc~H7G7UH0uSV$U~77Ovf{8&iHY>0(ebT$^^k^4|{-az?`aS(C7xO#|1(s7U?
z)hrGYl<9GhqO>^<68ER$AO>EBs(Ts-NrYnY5Lzi75><Nf3=Db<3=FREki=FJ4=FkO
z;vrGECLZGRGw~1)J*bZdX9I@!@erT>kB6jIkpxJsXPN*p$S(on(2N8~$=93!>D4ZR
z^6w@vFzA3v(ga9Jr<n*TXbch|X{0C-60*k<L8+dB;dLUUb6fv05fYdANsu6^N`m;f
zJqhB{{v=3ZTAT!N=(Z$C9dIlOl8sI!K?<0UNem1j3=9nN$&ipLNrsr)mJHE1F&Sds
z!emJAcWW}(o_dCN$&k4FmJDeHv!^gH`~Vf1DGUsi3=9k!sSFIO85kHAr9!GrzcfgY
zH>5#|<eoH$0}rP`EWVZoNgFTHAR+lFje+41sBxPPamclFi2Sp3P}I~jFnmadWE;K=
zND!-JKpbSB0Wmlr0}`~!86XV|3==XS4qXSOuVp~e&c6&uoa$#nXuC{^dA^wtpJ!x3
ziu9UHNV&2;lYv16RR7<~grwU4nUH#3CJW-y+$@MsYqKD2xXvty%a3P4vfafjNb0|q
z1<7vrvmg#*%7*Z@vLWS(Z#JYrs?BC#@MB<LxRVVD8SNYfhI&vx-zNuRQD_dNz(~x2
zq~hs0ki>T^2ND7gav&kVn+wS$+PM%1xaUIBPCzb1Uu-VK;+|YcNbJdlc<^*C1H(no
zm{BeR!#+^$n8#2L9?PlBha{re`H(m~nGZ=kxAP%GE6<>O=>mwnS^*?GSrkBg5>WsN
z;>rR@3E5i!389k(kh<es0VFE53n7Wut`MT$sjwd6!mvV!4^j&u3X2LMl}BqKq#B(I
zl|NPp$wrq8ArAgg2+5{uMG$@VMGOq#3=9lGMGOpe3=9mbiXi2JW-$Z9O3;{3F$2Q^
z1_p*##Sjm!t}lTE;kFWp0sBiJ8ZMPUg7RevB%4W;LfUTjr4R=<ltR)-e<`GiHNO<n
z;CN9ANxZRT5D(;)LHKQDkaA%<ls^wDU%$Q#(zH5W#=y|Sz`*dbjDcYt0|P@@IiyHd
zt$+l%aRnrVTq_`{I;aBTklYGL+}2buFgStgiV8^nzEc57JEE14M5|v3Dc~$CA?Ep2
zf<0Q#kWdM!PRlDHak{t?(#kzt32A&XR6)cosvwCjq6!jpB~_3_TMy-TS3!I@tqPI`
z=2k%*cC!kShQ3un9HLzfp-rn97;+iv85r!VAqK6jhSb+Pt06wUUkwT3H`S0h{!<Mu
zu^2>aAW<a?r44Ez>YQsB7*;VbFa*~?LhfG;#K%0f5dDg^kPxw|g=Ei=S_TGDQ2sBk
zg;?BM3kjOdwU9)1v=)+jU)3@&%wk|*aIb@u*^lZVJ(`4ih>w5PLmbA?0PzS%1H@vH
z21vHmY+zuR$-ux6)&ObLerSL=pt+HOp&m4jx2zFTL~d<_bT%uRAaVJkiGjg|fq_B0
z8Pd2cXl7t=W?*2L)y%*U#K6FCuNhJv7`8wx@^4{a@Md6OsA*wf@MK_MIMD(r@q}6-
z+0L~Uk}W-3AtkO~D`@<Wk%1wl71FA$Y=dOC9c>T|58EIXJZ*#2dd%$*jY{p1D6(j0
zV8{klx9t%98`~ike`tp!QmYO~S_$o7U<hPjU`XhIgv_Q6NUl5H0S&qf9gv{A(E;)4
zlMYDs<LiVZI@?Z&d}Jr2;7IL+IH0l<QUJAeLK5-vPKbq<J0aQbUMHj-@xGG*+{82Q
zf(-Ai>S6$oqKVXZGcarb4G?rg%4oYDNE2*s4+ALSGd$^mR2K2Q3=E$@!}Pt7_I_<2
zq~r3r4>Dxq*AGeM*ZU!9h-U(%NhUV|(w~T%0LcxjCqO)SVgdt$F9QQZ{ig|#YSdvO
z#HHmEAtl@Xi4X(cPJ~2(?j%Se@}2~#?Ve78gpAl^NJGPDGQ{AL$q=8;n+&PG1*bsx
z-BTbA_&0@t;WGmRgY;AehBVOlAOAFni_4}#;;dsDBxn~;gCv#>(;z{=6-pn3il3bZ
zN%fbeLE`cjl>c}dB<NpHgOq@Ora`iu$aDq<233uW{L-Rg%@hSN&7iuuPU1UbNJ>#^
zaj`;LevyKzMqXlWs%9~RV+ukrAhEc(JijPKAt<#twM4;H*EcaU2USUMYEfBca;kz7
z%mAgyMN&={U^7yS6>>|9OB9k)70ObJGSf0sQxuX?)AEZ_6)N*f6_OM46pB-GQj<#*
zN-|P&6*5wbQa5juYGetm%r7lcC@9LzO)RQZNY%|v%*;_pgt{v+F9oC=&M$zuu_!e;
zzbqB5p;)0jGbaaRcX4W7i9$*K=4$!3EMf|!#i>OKU=s_9Qqxk4QuC5ii#I2#^z(04
zF$rSa+-~Z}Dw>gBu8<3IXK`vtNoHPpv4WnS-sa0zoB~#fc?yX+#rZIor(~vq4Jm;+
zC>0W(2=QV_2o>vXzUIZwI$1Q}Be$V}uA!xZk&%_5$!5Vo6K2k&)Z&uEOG+}+C;NvO
z8>bd!9A1)BT%wRts*swQmkJGdh1|>%Fh5tp38ua@FTFS?GcSAc)(|r#NT4gE9o~Zy
zpP)ESg@{hhtrFKlL?e=nOJ-_baf!mChIz1veAF;!^2?AI7l^5O3b}{(6c?xFDZt%Y
zqEMWgnWv+Wk_d8GN~uC|W~xF)W@?c_Vrg;t;UyV4sd)<7iFtX&C7Yc>&#~yFfIS0>
z*TYMSQWYE_CM%TYr6{CiBKTnUgIvlDbt%Za%^6WQSXDwnkp~ggb<NBxE=kSFDa}hy
z%~MFsF3Bv*1V!g&`vg7V$#;v@Hs2|3W}NJh$hNt@bRx4PMwlt&rIr?_=A~>_t$fU+
z9G;q4l3J9ifM#r7Rca>4d_6rqhRKeZVw>e^)^bS&6lLb3_@OMds5~_<B{gsJlQv!s
zPl&G}VP2Y7l3G-poROKDl9QR7k*WX*>Qn^`321=ifpQDPG`-Dl`%be?W}R9ln3tKH
S0m@3ygi-=>jp*cgQ)K}<d6cjK

delta 6969
zcmZ3tj%oL5ruutAEK?a67#La^85m?37#Li5Ks*G_kziopXJBB+m0(~HW?*2bm0)0C
zV_;yID8azs!@$5WLxO<;q~)mu0|OTW1H&gM|E~lCgBAk=1D7O3+*Fc*ftP`S!9kLN
zL5P8Y!A}xwUOhu1guzfM$-p4Rz`)Qc$-uzQz`(FXl7T^lfq`L-Bm)CC0|UcBNd|^$
z1_p++k_-%r3=9mOQV@07QVb0C3=9m#QVa}*3=9mLr5G6G7#J8-r6Cr&NHZ`*GcYg&
zNi#4kXJBAhC(Xd%&cMJBCd0r`%TUk2Fi(boL5YEZK}ME=L7ahs!5&J7%Q7&CF)%RX
z%Q7&SFfcH*$wC~sTb6;rje&vTnk)l@Bm)D3tQ<t0xf~><{NxxIEEpIV;^ZJcpDqUp
zu|;wW3@Qu^3@f4hb0GEg3=9lc<sd$ID#yUU3UawT14AGK1B13a0|O^0$mJnEX_tq{
zPnBn2U}0cjm@Uu1V93D0us|MS@g;c%23ZCMhDY)Y45FYIRA69`W?*0tQ(#~aWME)0
zg3>Mu3=ABg5Kv%Xs0aBl9;&cR0URd`lN1;j_!t-%mMK7jeyak+CkLS#&nQ5AbX5W3
zusaG63tuTf(gG-cKvBl42#G>TMToc_l(trcgq(*WLp_5g0|P^nBE;ezMFs{-kdG80
z7Tkhrc&G@8^LL66mkTIC43t!YSgfYRz@W~+z+j^UiNX{mNL1B8<)<q_JhT={?@?l4
z5MW?nIIUC<NsV`uAVK$D2@;pQ$`BW-C_@aiQ-&BArp&;g&%nS?s0>lRP?>>2fq{Wx
zzcM6h9w;*~STis%JX3~5sgepLktV4yFk~_?Fl0gbS5zP#xl^wK@##|)h>PB+K;rVR
z3MA-xRT&tV85kHORUvUHs|rcomZ}hoB2*z!Fhvz&;36o0HI&||$^Z`G<EjvQKB_`O
z;)g0EMC;kr7#OZHFfeGUF)&mxFfjaAV_=xbz`#(U4zZ9+1ENt)gMmSrfq_9+1LD9y
z4TwRBP=0|1Bo{SkFfeRpU|?7b6%W^hgh09`Bt-KyAtBnS36Ao5hJ~7txZJ7<34ueJ
z5Eoz8gcL}BAqp9!v>;KVrv(vr)q*%INedEEnOa~28A`Pv4(rr{_;7|61A_(w1H&#Y
zNcMXSHHSkR6h-w64D#9#7shKt3{2N%V9;h@V93*k#OW+;NOoJG4ats6wIOl7UYmhI
zm4Sibf;PlQ-?Sl7!>7Z*-~tLM9Y~@q(t(8BEFFk>`*a|==?F+3l>g7`Kn(h>14+%Y
zx(p2R3=9m$x{x3X)`cX#TwRC*J9HsF?$?F*Y_=}MK}U2UA^B7nV$LUB28JmN3=AB4
z3=AC%3=DJiAc<B@AEMqxpMjwsl+RQ3At6wr&%mI|z`#(W4{_lteMnqyh4PQ;LoB|a
z4+)7o`jFi4Odk?LVg?Khp$rTR(gqOyg$9tsTVnvpWeo-p2TnGCn6toup&pzM*FY8Q
zHGl-w1p|l$Uko52@y`GfcXEahgPaW^X(h~%fx(f1fg#<HfuRvpv>HMR9Bm_r&mD{y
z7&d}RSR+X8Xg7w?6O1AHXByW-63r@ONTS<k%)nsGz`*dr7?RjzOd#UMCJ-0Lnm~Mz
zX#z?-3=Cx^3=HWE3=G>$AR#4Z3el%$3Q_NB3JKXjQ;0p0rVxvBO(BW2uHF=qczR74
z81fky7?zqcFtjl+Fo>E#^7R}jy~+&Y@*QRn3l2iXZ<s+0erCqNV9UV3@XL&W!H9u@
z!NQz@L4tvSA=@01W?IZ44(v9EgzRi{hy&`EnnP0U7IR1<yKD}rx8IpV5|^X}B*>kh
zbf^WyfD8+W1B;;i1`9~)?y`Us;S($%=AE*DnDf*El1BI}Ar2C@WMGJ3V5n!1vV;Ux
zu_eUC^_Gwzo&lwoT0-J<lO-esj$1-%vkR6GA3TDp`)J9)FoS`C;U84pbSsDh7g#}p
ze4Q1<oc&f12VAiNd90p+;kFedwZ5~01no~NNXtgR8d4(0T7zB6aLt;5VG;uagT4*K
zfI~KrxIGD_FWNvXxM>5amfzVhFtjo-FeunEFiZf|rnZpmY;FgMvOqfqhQ*-#pK8az
z5Wv8|z-iCGV8g(`5M|H6V8Fn@Fx?)K_)geEg7lL;q~)UH05Pb<0g{jV93Z)1y#oV-
z0|NuYM+ZnQGIE3jd6*+4YN8z><wBw(1H)Se28MD+28Mcb1_p*yCrH6C*9nqecRN9X
zY`+u4q6<!tpnl~9@%c|D28KLP1H+kt!2{Gna%N!gVqjp{;SBK^vkN3-#9bH|>=+mr
z<Xs?98s`FWNH&x%bYZ9mx9!SZAZ2y43&iD1T_B0_qzfd@U${Wx{G$uR0nDxthjF<=
ze5B$E5w~&$#VG@WqbnqPhPW~?L@_WhOmT&@0pCMuZ#PI(E_bVEU`S<PV6bv$U@!wU
zJlq)=Vi*`0uDe6(022=ehGPs23<(|(AFF#p+J4hK85l|!7#Qw)LduIEFGvWrc|oFL
zsu!d-yza%opa#m8UXV1U=ncu9dEN{Rt_%ze3+ufhW&d+;NKo<mK&o9I9|nd}1_p)<
zA4nRw=L0DxID8@5Nx>IVRD1d|Fhqjl(wBiDk%58XB9u1vgP1$jkAdL`0|UcuKL!R+
zJHNpnk}JdlAR$<<5&+45b^(yY;uQc1a{mBGu80U=U|?ckU`P&t#AzCo&V$lrP<3?y
zkT~uPfD|;-0vH&285tOs1wf+CBnXl=+=3wHM+JdHx}G692$I+i1VJ2fF$m(qyFm;L
zrVI=WuY({CQ4EH}txhl`B<zDBsXsgz;**MCNKj7-h9t6`!H~4EKNw>1)nEpOvkVLj
zFM}Zk-1ZQVhw2#^9)&=H@Kp!{Lma3F6bcEt#i5Y=d@_`QA(?@J;a(^M!x{z#2B$Dc
z8h9VZz)-}%z+e^*sRdVtLrT1x;gE9VOE{$R;*Nkc)wClZ4VUx?28Lh;28Nah1_o(R
z{=X3cap{)`h!43UAwHLlg!nu-5>n*GMM4r=MkL7P3=DH3AtARU65^n(k&r0(6Uo4k
z04h(SAo{YR7{Fbxk|>CIO;M1j?u}w#s0VeOCPhK2*Go~5RQnxDOGQI`WE%~M!-!}|
zP-aC#vRhp=#NgIwi2lh?^*f>=A$K_%k}F<7>F?1H2eHLK(ttn=wEmZjfdq|K48$V$
z7>G|IV;}~lLHX5Cx+4bSu&FVSl5#@~B-cEQfs~McV;~{q8VfPcHx{BUJ{FS3CP3*q
zvGtI+S`o{@pa*Kl$3ha*yI4qx$PovLL$x@F&)wo6K8lKir1H!-hyyF*Ac?Ur4pND&
zkAs+VA`arvM{$q>?Qa|dgB7T+84uxy*T*w3=rAxal*L1Wba6bS09h3eNhELLA*tCR
z0n}S%U~o%-#Nm+yNC=%zfcW%I0>pt&5+G^lM*_rw+=-APT{;nxJLD4~<%U}#149S{
z14DgDA|!}TCPECpoe0tRED>Vhw?s&9mMaNjk#iCx(Rn99TDLJt3=BU&sX2*(p%T<t
zO=e(N&A`C$JsDC9E>3|2`PCFi3HvYw;y{U1u)XyR+NqF4VVepGLibb#hC`r|FBRet
z?KFtIbs8i}T+<-AB{>Zew3TTP2Te(X7`!wM5~4ex>Yt`T9Lk&yp|#T?X(2cr6qWT1
z3@u;=1H<HWh=GgJAwJ)m4k@cIrb7x0mJCQL*Ux|?){qQHwOyD2ap<88h)*wNK$>Fr
zG9V6@$%JG#wM<B=*Up6GGLuXO26a&W4~GiWXF>{+MVXL-=u##FgC7F}gJBjVWE!#{
z1}w~iShOk&Qci5kf+XHIS&&30oec>A(`-nHC1pc$M?*G5|D0?{+F6>-z)%lL^_#LG
z7C+1e1qlO#U=GBG3ONi67a15BY;zbG_AxLp@aICR>2tY|MD!sS9ES{Yd62YYkOvtl
zv4-*s@*wind5~Pxn+Ng8+B`@IpUY#Y2X~hr<w1f-E+1SeFev9k;<6zhl6oiSLo`g!
zhd6L`KEwyRpz4n2L+XTE`H<T26I5Qh01{;y1rP`O7C^FRbpgcuDFyWm4B-q649g1`
z80tX1-vUT^P*=#nu#$m+VQL`*!vWBkP!Yt348@Qj<SvF7AW{rbuU-s^0=r^Jizv4k
zlHIlyLz-lFiy;n|D}kgH-4cj8+xiknL!zw&l8S$qKzzbe3gN4iLdpeGDBq?O634!!
z5OwLLkSM7yWnkz5H7H6M7}hZ`FzA&*isrjzkSKUv1_`mhWspS9RSt1by=plmu1(4r
z7@Qaw7y`>7`F%<`#G<q1kW~D<98%JKEQc7#Rsr$3L<OX!q+bDv<LC-VBeuGNfdSOE
z+yNE;PyunUKqVw3H7dc0xSqinDqvp;@u^oOq`C~OggC6X5|XG^RYDwcA4<QhWMIf;
zU|{$RF^D0t3Q}$7RzZ9^xe5~03#uS-zM%?IfSs&@MBPO&UC+Sq1gh|R6$8U61_lQ9
zYDmy+s)qRZKsCgGtJRPYc~=c7fH-O(7RuE?Ld2y8lGws(Ac?rThJj%g0|Ud28c5My
zTnp)-e5{3dw5yJRfgP0pC)R<3f?;MI#KNU@ko>!=j)7q&0|UeJI!J4^u^v)zh%`Xr
z&b$FqQo1!j`i`89kSMHdgbYCJX@s;L8JidwoI&G>O$-b{pyqlLq+B@E#K2Gws(c<a
zF)(;DFfi~oGcb5EFfc?mLrT8I&5-POtr?O%Z#6@Fe!rQ4L4=Wk;cGLb(aO~d$#$Nt
z5cNf^5DUs$A+_9;R*1gMt&pfW*~-9>&A`BLt(Bo3+(B?^gIL_y1_|QRZIDFrv<)&6
z`k@UHGEVJ~>=)h+iOYm`NXTWhLws7&4#|e|+94(0S*ZNWc1Zd0wH@LB&JIXHB-T+6
zNxkMB5DSw#AlWXj1JaPF?|`)Nj(0GC$M38<8Nj1tOF9`CHh{){yC6mNxh_Z(Ot+hX
zp^AZlp`;s9XT0xbVEDwqz#!EFY0mTZLOLYPy^tZ9d-c7L)Scc3Nknt{AU&6LeUQ}u
zt`Fif?tTUaUj_yS(|$-b+T0Iu;I4j1$;LDRqR(IgB<Kq!K+;a%1W0YCF%c3XkrN^9
zi_(b@bL+QHg!u5`L`eM|G6}*zHwoeZ$H@!~pFyM9lOf}JK~o?OWt$2qNF=60g4B2_
zB#k&sg+z%fln#W7$4-SrS<+NU6lOvA(hS8@A=#~ZDx`quoC-+;3#V@0CBe-!Sw`A^
z^EBx?7SHmG%;XG({4|Bi{L&(Yvecr?w9M2Lg~XJUqSWGIg_3-Q(&AKw#A1kiK~ZLI
zVo{|+evv{^YI1&AYEdOj>E=v@w=A2BRXh0E6pB)dQ%fcXDoF4ulw@QU!<Bq8iDqPT
z1RK5C$t;p}^CoL{fz3a?8ClI0B0=`2>gFb9=D^e|fVAcmgPo9~kd|MhP>@($T%KQ)
z0=Iqg&wx+d1{S(T77B*uRz`-KzXh5wb3wdOm6=*J**MhLxID8cMIkjaPr)g*xa9DX
zlFandy!7In%)D%c#5{$R%v1$Ouy0cp(o>6*GcuF2OD4|@HJfY_7Pa|m*l8B4#L{Ag
zl+?_;)I4Yifs8Im&4cNK7y=5M!%K<~rj_QUOlC|LXHSHfzWH7B4c5)C6Lp0>Ty=dD
zGjoa+s!EGM`0$?O?Bdjts?<D%l+<E{0EjP3iWSOJi&9eapsp-Nn4F$koSK)CS~A(L
zRDH8uX)_}S)DDKpjxl1JkCshl77YW(Wp-joW`3SRUTSG^Nn%mS=EADSOp_PHh;Htw
zUB@LEmReK}GDrd4?VCe7csVw&n{b@fN5LgCRRQ8)U067QLo~7U@O+T_auw1J?<oR@
x<D-UoFvo*JDm}HhI5qE4!yJY5)V$J?s*=ei)5>{E^APb<GI@TC*kr-!vH%<X5QYE%

diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po
index 293df9db..4a00b8e8 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-08-28 17:21+0200\n"
+"POT-Creation-Date: 2022-10-19 22:14+0200\n"
 "PO-Revision-Date: 2021-05-25 21:18+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -140,8 +140,8 @@ msgstr "Über uffd"
 
 #: uffd/templates/group/list.html:8 uffd/templates/invite/list.html:6
 #: uffd/templates/mail/list.html:8 uffd/templates/role/list.html:8
-#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:50
-#: uffd/templates/service/show.html:78 uffd/templates/user/list.html:8
+#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:57
+#: uffd/templates/service/show.html:85 uffd/templates/user/list.html:8
 msgid "New"
 msgstr "Neu"
 
@@ -156,18 +156,18 @@ msgstr "GID"
 #: uffd/templates/mfa/setup.html:157 uffd/templates/mfa/setup.html:158
 #: uffd/templates/mfa/setup.html:169 uffd/templates/role/list.html:14
 #: uffd/templates/rolemod/list.html:9 uffd/templates/rolemod/show.html:44
-#: uffd/templates/selfservice/self.html:165
+#: uffd/templates/selfservice/self.html:189
 #: uffd/templates/service/index.html:14 uffd/templates/service/show.html:20
-#: uffd/templates/service/show.html:84 uffd/templates/user/show.html:178
-#: uffd/templates/user/show.html:210
+#: uffd/templates/service/show.html:91 uffd/templates/user/show.html:189
+#: uffd/templates/user/show.html:221
 msgid "Name"
 msgstr "Name"
 
 #: uffd/templates/group/list.html:16 uffd/templates/group/show.html:33
 #: uffd/templates/invite/new.html:36 uffd/templates/role/list.html:15
 #: uffd/templates/role/show.html:48 uffd/templates/rolemod/list.html:10
-#: uffd/templates/rolemod/show.html:26 uffd/templates/selfservice/self.html:166
-#: uffd/templates/user/show.html:179 uffd/templates/user/show.html:211
+#: uffd/templates/rolemod/show.html:26 uffd/templates/selfservice/self.html:190
+#: uffd/templates/user/show.html:190 uffd/templates/user/show.html:222
 msgid "Description"
 msgstr "Beschreibung"
 
@@ -191,9 +191,9 @@ msgstr "Abbrechen"
 
 #: uffd/templates/group/show.html:11 uffd/templates/role/show.html:19
 #: uffd/templates/role/show.html:21 uffd/templates/selfservice/self.html:61
-#: uffd/templates/selfservice/self.html:180 uffd/templates/service/api.html:11
+#: uffd/templates/selfservice/self.html:204 uffd/templates/service/api.html:11
 #: uffd/templates/service/oauth2.html:11 uffd/templates/service/show.html:12
-#: uffd/templates/user/show.html:25 uffd/templates/user/show.html:166
+#: uffd/templates/user/show.html:25 uffd/templates/user/show.html:177
 msgid "Are you sure?"
 msgstr "Wirklich fortfahren?"
 
@@ -234,7 +234,7 @@ msgid "Created by"
 msgstr "Erstellt durch"
 
 #: uffd/templates/invite/list.html:14 uffd/templates/service/api.html:34
-#: uffd/templates/service/show.html:85
+#: uffd/templates/service/show.html:92
 msgid "Permissions"
 msgstr "Berechtigungen"
 
@@ -262,7 +262,7 @@ msgstr "Account-Registrierung"
 msgid "user signups"
 msgstr "Account-Registrierungen"
 
-#: uffd/templates/invite/list.html:49 uffd/templates/user/show.html:163
+#: uffd/templates/invite/list.html:49 uffd/templates/user/show.html:174
 msgid "Disabled"
 msgstr "Deaktiviert"
 
@@ -455,8 +455,8 @@ msgstr ""
 msgid "One address per line"
 msgstr "Eine Adresse pro Zeile"
 
-#: uffd/templates/mfa/auth.html:6 uffd/templates/selfservice/self.html:134
-#: uffd/templates/user/show.html:161
+#: uffd/templates/mfa/auth.html:6 uffd/templates/selfservice/self.html:158
+#: uffd/templates/user/show.html:172
 msgid "Two-Factor Authentication"
 msgstr "Zwei-Faktor-Authentifizierung"
 
@@ -563,11 +563,11 @@ msgstr ""
 msgid "Disable two-factor authentication"
 msgstr "Zwei-Faktor-Authentifizierung (2FA) deaktivieren"
 
-#: uffd/templates/mfa/setup.html:18 uffd/templates/selfservice/self.html:140
+#: uffd/templates/mfa/setup.html:18 uffd/templates/selfservice/self.html:164
 msgid "Two-factor authentication is currently <strong>enabled</strong>."
 msgstr "Die Zwei-Faktor-Authentifizierung ist derzeit <strong>aktiviert</strong>."
 
-#: uffd/templates/mfa/setup.html:20 uffd/templates/selfservice/self.html:142
+#: uffd/templates/mfa/setup.html:20 uffd/templates/selfservice/self.html:166
 msgid "Two-factor authentication is currently <strong>disabled</strong>."
 msgstr ""
 "Die Zwei-Faktor-Authentifizierung ist derzeit "
@@ -595,7 +595,7 @@ msgid "Reset two-factor configuration"
 msgstr "Zwei-Faktor-Authentifizierung zurücksetzen"
 
 #: uffd/templates/mfa/setup.html:46 uffd/templates/mfa/setup_recovery.html:5
-#: uffd/templates/user/show.html:164
+#: uffd/templates/user/show.html:175
 msgid "Recovery Codes"
 msgstr "Wiederherstellungscodes"
 
@@ -633,7 +633,7 @@ msgstr "Generiere neue Wiederherstellungscodes"
 msgid "You have no remaining recovery codes."
 msgstr "Du hast keine Wiederherstellungscodes übrig."
 
-#: uffd/templates/mfa/setup.html:85 uffd/templates/user/show.html:164
+#: uffd/templates/mfa/setup.html:85 uffd/templates/user/show.html:175
 msgid "Authenticator Apps (TOTP)"
 msgstr "Authentifikator-Apps (TOTP)"
 
@@ -663,7 +663,7 @@ msgstr "Registriert am"
 msgid "No authenticator apps registered yet"
 msgstr "Bisher keine Authentifikator-Apps registriert"
 
-#: uffd/templates/mfa/setup.html:134 uffd/templates/user/show.html:164
+#: uffd/templates/mfa/setup.html:134 uffd/templates/user/show.html:175
 msgid "U2F and FIDO2 Devices"
 msgstr "U2F und FIDO2 Geräte"
 
@@ -996,12 +996,10 @@ msgstr "Profil"
 #: uffd/templates/selfservice/self.html:15
 msgid ""
 "Your profile information is used by all services that are integrated into"
-" the Single-Sign-On. Your e-mail address is also used for password "
-"recovery."
+" the Single-Sign-On."
 msgstr ""
 "Deine Profilangaben werden von allen Diensten verwendet, die an das "
-"Single-Sign-On angeschlossen sind. Die E-Mail-Adresse wird außerdem für "
-"die „Passwort vergessen“ genutzt."
+"Single-Sign-On angeschlossen sind."
 
 #: uffd/templates/selfservice/self.html:16
 msgid "Changes may take several minutes to be visible in all services."
@@ -1026,8 +1024,8 @@ msgid ""
 "verify new addresses by opening a link set to them."
 msgstr ""
 "Füge neue Adressen zu deinem Account hinzu oder löschen vorhandene. Neue "
-"Adressen müssen verifiziert werden, bevor sie verwendet werden können. "
-"Dazu wird ein Bestätigungslink an die Adresse geschickt."
+"Adressen müssen bestätigt werden, bevor sie verwendet werden können. Dazu"
+" erhälst du eine E-Mail mit einem Bestätigungslink."
 
 #: uffd/templates/selfservice/self.html:43
 msgid "Email"
@@ -1047,7 +1045,7 @@ msgstr "primär"
 
 #: uffd/templates/selfservice/self.html:57
 msgid "unverified"
-msgstr "unverifiziert"
+msgstr "nicht bestätigt"
 
 #: uffd/templates/selfservice/self.html:62 uffd/views/selfservice.py:171
 msgid "Cannot delete primary e-mail address"
@@ -1055,47 +1053,64 @@ msgstr "Primäre E-Mail-Adresse kann nicht gelöscht werden"
 
 #: uffd/templates/selfservice/self.html:65
 msgid "Retry verification"
-msgstr "Verifikation neustarten"
+msgstr "Bestätigungslink neusenden"
 
 #: uffd/templates/selfservice/self.html:79
 msgid "E-Mail Preferences"
 msgstr "E-Mail-Einstellungen"
 
-#: uffd/templates/selfservice/self.html:80
+#: uffd/templates/selfservice/self.html:81
 msgid ""
-"Choose which of your verified address to use as your primary or recovery "
-"address."
+"Choose your primary e-mail address and the address password recovery "
+"e-mails will be sent to."
 msgstr ""
-"Wähle aus deinen verifizierten Adressen die primäre Adresse und die "
-"Wiederherstellungsadresse."
+"Wähle deine primäre Adresse und die Adresse für Passwort-"
+"Zurücksetzen-E-Mails aus."
 
-#: uffd/templates/selfservice/self.html:85
+#: uffd/templates/selfservice/self.html:83
+msgid "You can also select different addresses for different services."
+msgstr ""
+"Du kannst für unterschiedliche Dienste unterschiedliche Adressen "
+"verwenden."
+
+#: uffd/templates/selfservice/self.html:86
+msgid "Adresses must be verified before you can select them here."
+msgstr "Adressen müssen bestätigt sein, damit du sie hier auswählen kannst."
+
+#: uffd/templates/selfservice/self.html:91
 msgid "Primary Address"
 msgstr "Primäre Adresse"
 
-#: uffd/templates/selfservice/self.html:93
-msgid "Recovery Address"
-msgstr "Wiederherstellungsadresse"
+#: uffd/templates/selfservice/self.html:99
+msgid "Address for Password Reset E-Mails"
+msgstr "Adresse für Passwort-Zurücksetzen-E-Mails"
 
-#: uffd/templates/selfservice/self.html:95 uffd/templates/user/show.html:139
+#: uffd/templates/selfservice/self.html:101
+#: uffd/templates/selfservice/self.html:115 uffd/templates/user/show.html:139
+#: uffd/templates/user/show.html:149
 msgid "Use primary address"
-msgstr "Verwende primäre Adresse"
+msgstr "Primäre Adresse verwenden"
+
+#: uffd/templates/selfservice/self.html:113
+#, python-format
+msgid "Address for Service \"%(name)s\""
+msgstr "Adresse für Dienst „%(name)s“"
 
-#: uffd/templates/selfservice/self.html:100
-msgid "Password reset e-mails will be sent to this address"
-msgstr "E-Mails zur zurücksetzen des Passworts werden an diese Adresse gesendet"
+#: uffd/templates/selfservice/self.html:124
+msgid "Show more settings ..."
+msgstr "Weitere Einstellungen anzeigen ..."
 
-#: uffd/templates/selfservice/self.html:102
+#: uffd/templates/selfservice/self.html:126
 msgid "Update E-Mail Preferences"
 msgstr "E-Mail-Einstellungen speichern"
 
-#: uffd/templates/selfservice/self.html:111
+#: uffd/templates/selfservice/self.html:135
 #: uffd/templates/session/login.html:16 uffd/templates/signup/start.html:36
-#: uffd/templates/user/show.html:148
+#: uffd/templates/user/show.html:159
 msgid "Password"
 msgstr "Passwort"
 
-#: uffd/templates/selfservice/self.html:112
+#: uffd/templates/selfservice/self.html:136
 msgid ""
 "Your login password for the Single-Sign-On. Only enter it on the Single-"
 "Sign-On login page! No other legit websites will ask you for this "
@@ -1106,22 +1121,22 @@ msgstr ""
 " Webseite wird dich nach diesem Passwort fragen. Es wird auch niemals für"
 " Support-Anfragen benötigt."
 
-#: uffd/templates/selfservice/self.html:117
+#: uffd/templates/selfservice/self.html:141
 #: uffd/templates/selfservice/set_password.html:9
 msgid "New Password"
 msgstr "Neues Passwort"
 
-#: uffd/templates/selfservice/self.html:123
+#: uffd/templates/selfservice/self.html:147
 #: uffd/templates/selfservice/set_password.html:16
 #: uffd/templates/signup/start.html:43
 msgid "Repeat Password"
 msgstr "Passwort wiederholen"
 
-#: uffd/templates/selfservice/self.html:125
+#: uffd/templates/selfservice/self.html:149
 msgid "Change Password"
 msgstr "Passwort ändern"
 
-#: uffd/templates/selfservice/self.html:135
+#: uffd/templates/selfservice/self.html:159
 msgid ""
 "Setting up Two-Factor Authentication (2FA) adds an additional step to the"
 " Single-Sign-On login and increases the security of your account "
@@ -1131,17 +1146,17 @@ msgstr ""
 "Anmeldung im Single-Sign-On hinzu und verbessert damit die Sicherheit "
 "deines Accounts erheblich."
 
-#: uffd/templates/selfservice/self.html:145
+#: uffd/templates/selfservice/self.html:169
 msgid "Manage two-factor authentication"
 msgstr "Zwei-Faktor-Authentifizierung (2FA) verwalten"
 
-#: uffd/templates/selfservice/self.html:153 uffd/templates/user/list.html:20
-#: uffd/templates/user/show.html:35 uffd/templates/user/show.html:173
+#: uffd/templates/selfservice/self.html:177 uffd/templates/user/list.html:20
+#: uffd/templates/user/show.html:35 uffd/templates/user/show.html:184
 #: uffd/views/role.py:21
 msgid "Roles"
 msgstr "Rollen"
 
-#: uffd/templates/selfservice/self.html:154
+#: uffd/templates/selfservice/self.html:178
 msgid ""
 "Aside from a set of base permissions, your roles determine the "
 "permissions of your account."
@@ -1149,7 +1164,7 @@ msgstr ""
 "Deine Berechtigungen werden, von einigen Basis-Berechtigungen abgesehen, "
 "von deinen Rollen bestimmt"
 
-#: uffd/templates/selfservice/self.html:156
+#: uffd/templates/selfservice/self.html:180
 #, python-format
 msgid ""
 "See <a href=\"%(services_url)s\">Services</a> for an overview of your "
@@ -1158,13 +1173,13 @@ msgstr ""
 "Auf <a href=\"%(services_url)s\">Dienste</a> erhälst du einen Überblick "
 "über deine aktuellen Berechtigungen."
 
-#: uffd/templates/selfservice/self.html:160
+#: uffd/templates/selfservice/self.html:184
 msgid "Administrators and role moderators can invite you to new roles."
 msgstr ""
 "Accounts mit Adminrechten oder Rollen-Moderationsrechten können dich zu "
 "Rollen einladen."
 
-#: uffd/templates/selfservice/self.html:175
+#: uffd/templates/selfservice/self.html:199
 msgid ""
 "Some permissions in this role require you to setup two-factor "
 "authentication"
@@ -1172,11 +1187,11 @@ msgstr ""
 "Einige Berechtigungen dieser Rolle erfordern das Einrichten von Zwei-"
 "Faktor-Authentifikation"
 
-#: uffd/templates/selfservice/self.html:181
+#: uffd/templates/selfservice/self.html:205
 msgid "Leave"
 msgstr "Verlassen"
 
-#: uffd/templates/selfservice/self.html:188
+#: uffd/templates/selfservice/self.html:212
 msgid "You currently don't have any roles"
 msgstr "Du hast derzeit keine Rollen"
 
@@ -1220,7 +1235,7 @@ msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
 msgid "Access uffd metrics"
 msgstr "Zugriff auf uffd-Metriken"
 
-#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:56
+#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:63
 msgid "Client ID"
 msgstr "Client-ID"
 
@@ -1293,6 +1308,10 @@ msgstr "Mitglieder der Gruppe \"%(group_name)s\" haben Zugriff"
 msgid "Hide mail addresses with remailer"
 msgstr "Verstecke Mailadressen mit dem Remailer"
 
+#: uffd/templates/service/show.html:45
+msgid "Enable user mail preferences"
+msgstr "User E-Mail-Einstellungen aktivieren"
+
 #: uffd/templates/session/deviceauth.html:15
 msgid "Log into a service on another device without entering your password."
 msgstr ""
@@ -1550,23 +1569,28 @@ msgstr "Primäre E-Mail-Adresse"
 msgid "Recovery E-Mail Address"
 msgstr "Wiederherstellungs-E-Mail-Adresse"
 
-#: uffd/templates/user/show.html:152
+#: uffd/templates/user/show.html:147
+#, python-format
+msgid "Address for %(name)s"
+msgstr "Adresse für %(name)s"
+
+#: uffd/templates/user/show.html:163
 msgid "E-Mail to set it will be sent"
 msgstr "Mail zum Setzen wird versendet"
 
-#: uffd/templates/user/show.html:163
+#: uffd/templates/user/show.html:174
 msgid "Status:"
 msgstr "Status:"
 
-#: uffd/templates/user/show.html:163
+#: uffd/templates/user/show.html:174
 msgid "Enabled"
 msgstr "Aktiv"
 
-#: uffd/templates/user/show.html:166
+#: uffd/templates/user/show.html:177
 msgid "Reset 2FA"
 msgstr "2FA zurücksetzen"
 
-#: uffd/templates/user/show.html:206
+#: uffd/templates/user/show.html:217
 msgid "Resulting groups (only updated after save)"
 msgstr "Resultierende Gruppen (wird nur aktualisiert beim Speichern)"
 
@@ -1810,7 +1834,7 @@ msgid "E-Mail address already exists"
 msgstr "E-Mail-Adresse existiert bereits"
 
 #: uffd/views/selfservice.py:124 uffd/views/selfservice.py:158
-#: uffd/views/selfservice.py:221
+#: uffd/views/selfservice.py:223
 #, python-format
 msgid "E-Mail to \"%(mail_address)s\" could not be sent!"
 msgstr "E-Mail an \"%(mail_address)s\" konnte nicht gesendet werden!"
@@ -1835,11 +1859,11 @@ msgstr "E-Mail-Adresse verifiziert"
 msgid "E-Mail address deleted"
 msgstr "E-Mail-Adresse gelöscht"
 
-#: uffd/views/selfservice.py:192
+#: uffd/views/selfservice.py:194
 msgid "E-Mail preferences updated"
 msgstr "E-Mail-Einstellungen geändert"
 
-#: uffd/views/selfservice.py:203
+#: uffd/views/selfservice.py:205
 #, python-format
 msgid "You left role %(role_name)s"
 msgstr "Rolle %(role_name)s verlassen"
@@ -1906,29 +1930,29 @@ msgstr "Accounts"
 msgid "Login name does not meet requirements"
 msgstr "Anmeldename entspricht nicht den Anforderungen"
 
-#: uffd/views/user.py:96
+#: uffd/views/user.py:98
 msgid "Display name does not meet requirements"
 msgstr "Anzeigename entspricht nicht den Anforderungen"
 
-#: uffd/views/user.py:102
+#: uffd/views/user.py:104
 msgid "Password is invalid"
 msgstr "Passwort ist ungültig"
 
-#: uffd/views/user.py:118
+#: uffd/views/user.py:120
 msgid "Service user created"
 msgstr "Service-Account erstellt"
 
-#: uffd/views/user.py:121
+#: uffd/views/user.py:123
 msgid "User created. We sent the user a password reset link by e-mail"
 msgstr ""
 "Account erstellt. E-Mail mit einem Link zum Setzen des Passworts wurde "
 "versendet."
 
-#: uffd/views/user.py:123
+#: uffd/views/user.py:125
 msgid "User updated"
 msgstr "Account aktualisiert"
 
-#: uffd/views/user.py:133
+#: uffd/views/user.py:135
 msgid "Deleted user"
 msgstr "Account gelöscht"
 
diff --git a/uffd/views/selfservice.py b/uffd/views/selfservice.py
index 95883536..5d9b337b 100644
--- a/uffd/views/selfservice.py
+++ b/uffd/views/selfservice.py
@@ -177,17 +177,19 @@ def delete_email(email_id):
 @login_required(selfservice_acl_check)
 def update_email_preferences():
 	verified_emails = UserEmail.query.filter_by(user=request.user, verified=True)
-	email = verified_emails.filter_by(id=request.form['primary_email']).first()
-	if not email:
-		abort(400)
-	request.user.primary_email = email
+	request.user.primary_email = verified_emails.filter_by(id=request.form['primary_email']).one()
 	if request.form['recovery_email'] == 'primary':
 		request.user.recovery_email = None
 	else:
-		email = verified_emails.filter_by(id=request.form['recovery_email']).first()
-		if not email:
-			abort(400)
-		request.user.recovery_email = email
+		request.user.recovery_email = verified_emails.filter_by(id=request.form['recovery_email']).one()
+	for service_user in request.user.service_users:
+		if not service_user.service.enable_email_preferences:
+			continue
+		value = request.form.get(f'service_{service_user.service.id}_email', 'primary')
+		if value == 'primary':
+			service_user.service_email = None
+		else:
+			service_user.service_email = verified_emails.filter_by(id=value).one()
 	db.session.commit()
 	flash(_('E-Mail preferences updated'))
 	return redirect(url_for('selfservice.index'))
diff --git a/uffd/views/service.py b/uffd/views/service.py
index cc4ee8d7..a8c9e28d 100644
--- a/uffd/views/service.py
+++ b/uffd/views/service.py
@@ -72,6 +72,7 @@ def edit_submit(id=None):
 		service.limit_access = True
 		service.access_group = Group.query.get(request.form['access-group'])
 	service.use_remailer = request.form.get('use_remailer') == '1'
+	service.enable_email_preferences = request.form.get('enable_email_preferences') == '1'
 	db.session.commit()
 	return redirect(url_for('service.show', id=service.id))
 
diff --git a/uffd/views/user.py b/uffd/views/user.py
index 3067a4cb..b3125667 100644
--- a/uffd/views/user.py
+++ b/uffd/views/user.py
@@ -1,7 +1,7 @@
 import csv
 import io
 
-from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
 from flask_babel import gettext as _, lazy_gettext
 from sqlalchemy.exc import IntegrityError
 
@@ -75,17 +75,19 @@ def update(id=None):
 			db.session.add(email)
 
 		verified_emails = UserEmail.query.filter_by(user=user, verified=True)
-		email = verified_emails.filter_by(id=request.form['primary_email']).first()
-		if not email:
-			abort(400)
-		user.primary_email = email
+		user.primary_email = verified_emails.filter_by(id=request.form['primary_email']).one()
 		if request.form['recovery_email'] == 'primary':
 			user.recovery_email = None
 		else:
-			email = verified_emails.filter_by(id=request.form['recovery_email']).first()
-			if not email:
-				abort(400)
-			user.recovery_email = email
+			user.recovery_email = verified_emails.filter_by(id=request.form['recovery_email']).one()
+		for service_user in user.service_users:
+			if not service_user.service.enable_email_preferences:
+				continue
+			value = request.form.get(f'service_{service_user.service.id}_email', 'primary')
+			if value == 'primary':
+				service_user.service_email = None
+			else:
+				service_user.service_email = verified_emails.filter_by(id=value).one()
 
 		for email in user.all_emails:
 			if request.form.get(f'email-{email.id}-delete') == '1':
-- 
GitLab