From 879a04c576f9d6db43f27a1ecb712080976c0e47 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@cccv.de>
Date: Thu, 20 Oct 2022 18:33:21 +0200
Subject: [PATCH] Remailer address format v2

Deprecates old case-sensitive format. Some software out there stores email
addresses converted to lower case, breaking v1 remailer addresses. The new
format is case-insensitive and generally more robust.

Uffd continues to use and support the v1 format for services setup before
this change. Support for the old format is planned to be remove in uffd v3.
It is possbile to gradually migrate services to the new format with a service
setting in the admin interface.

Also fixes compatability issue with very recent SQLAlchemy versions introduced
by b391e17 (whens parameter of case function).
---
 tests/test_api.py                             |  16 ++---
 tests/test_oauth2.py                          |   6 +-
 tests/test_remailer.py                        |  64 +++++++++++------
 tests/test_services.py                        |  47 +++++--------
 tests/test_utils.py                           |  12 ++++
 .../versions/2b68f688bec1_remailer_v2.py      |  66 ++++++++++++++++++
 uffd/models/__init__.py                       |   5 +-
 uffd/models/mfa.py                            |   6 +-
 uffd/models/service.py                        |  39 ++++++-----
 uffd/remailer.py                              |  53 +++++++++++---
 uffd/templates/service/show.html              |  28 ++++++--
 uffd/translations/de/LC_MESSAGES/messages.mo  | Bin 38702 -> 39636 bytes
 uffd/translations/de/LC_MESSAGES/messages.po  |  57 +++++++++++----
 uffd/utils.py                                 |  17 +++++
 uffd/views/service.py                         |   6 +-
 15 files changed, 309 insertions(+), 113 deletions(-)
 create mode 100644 tests/test_utils.py
 create mode 100644 uffd/migrations/versions/2b68f688bec1_remailer_v2.py

diff --git a/tests/test_api.py b/tests/test_api.py
index 9200fc89..678aa467 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -5,7 +5,7 @@ from flask import url_for
 from uffd.password_hash import PlaintextPasswordHash
 from uffd.remailer import remailer
 from uffd.database import db
-from uffd.models import APIClient, Service, User
+from uffd.models import APIClient, Service, User, RemailerMode
 from uffd.views.api import apikey_required
 from utils import UffdTestCase, db_flush
 
@@ -141,7 +141,7 @@ class TestAPIGetusers(UffdTestCase):
 
 	def test_with_remailer(self):
 		service = Service.query.filter_by(name='test').one()
-		service.use_remailer = True
+		service.remailer_mode = RemailerMode.ENABLED_V1
 		db.session.commit()
 		user = self.get_user()
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
@@ -149,7 +149,7 @@ class TestAPIGetusers(UffdTestCase):
 		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(service.id, user.id), 'id': 10000, 'loginname': 'testuser', 'groups': ['uffd_access', 'users']},
+			{'displayname': 'Test User', 'email': remailer.build_v1_address(service.id, user.id), 'id': 10000, 'loginname': 'testuser', 'groups': ['uffd_access', 'users']},
 		])
 
 	def test_loginname(self):
@@ -173,15 +173,15 @@ class TestAPIGetusers(UffdTestCase):
 
 	def test_email_with_remailer(self):
 		service = Service.query.filter_by(name='test').one()
-		service.use_remailer = True
+		service.remailer_mode = RemailerMode.ENABLED_V1
 		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(service.id, user.id)), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		r = self.client.get(path=url_for('api.getusers', email=remailer.build_v1_address(service.id, user.id)), 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(service.id, user.id), 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
+			{'displayname': 'Test Admin', 'email': remailer.build_v1_address(service.id, user.id), 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
 		])
 
 	def test_email_empty(self):
@@ -261,12 +261,12 @@ 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))
+		db.session.add(Service(name='service2', remailer_mode=RemailerMode.ENABLED_V1))
 
 	def test(self):
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
 		service = Service.query.filter_by(name='service2').one()
-		r = self.client.get(path=url_for('api.resolve_remailer', orig_address=remailer.build_address(service.id, self.get_user().id)), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		r = self.client.get(path=url_for('api.resolve_remailer', orig_address=remailer.build_v1_address(service.id, self.get_user().id)), headers=[basic_auth('test', 'test')], follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, {'address': self.get_user().primary_email.address})
 		r = self.client.get(path=url_for('api.resolve_remailer', orig_address='foo@bar'), headers=[basic_auth('test', 'test')], follow_redirects=True)
diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py
index 1dea4074..e47ba995 100644
--- a/tests/test_oauth2.py
+++ b/tests/test_oauth2.py
@@ -6,7 +6,7 @@ from flask import url_for, session
 from uffd import create_app, db
 from uffd.password_hash import PlaintextPasswordHash
 from uffd.remailer import remailer
-from uffd.models import User, DeviceLoginConfirmation, Service, OAuth2Client, OAuth2DeviceLoginInitiation
+from uffd.models import User, DeviceLoginConfirmation, Service, OAuth2Client, OAuth2DeviceLoginInitiation, RemailerMode
 
 from utils import dump, UffdTestCase
 
@@ -50,12 +50,12 @@ class TestViews(UffdTestCase):
 	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
+		service.remailer_mode = RemailerMode.ENABLED_V1
 		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(service.id, self.get_user().id))
+		self.assert_authorization(r, mail=remailer.build_v1_address(service.id, self.get_user().id))
 
 	def test_authorization_client_secret_rehash(self):
 		OAuth2Client.query.delete()
diff --git a/tests/test_remailer.py b/tests/test_remailer.py
index 8ef261fc..239f3743 100644
--- a/tests/test_remailer.py
+++ b/tests/test_remailer.py
@@ -5,6 +5,10 @@ from utils import UffdTestCase
 USER_ID = 1234
 SERVICE1_ID = 4223
 SERVICE2_ID = 3242
+ADDR_V1_S1 = 'v1-WzQyMjMsMTIzNF0.MeO6bHGTgIyPvvq2r3xriokLMCU@remailer.example.com'
+ADDR_V1_S2 = 'v1-WzMyNDIsMTIzNF0.p2a_RkJc0oHBc9u4_S8G9METflA@remailer.example.com'
+ADDR_V2_S1 = 'v2-lm2demrtfqytemzulu-ghr3u3drsoaizd567k3k67dlrkeqwmbf@remailer.example.com'
+ADDR_V2_S2 = 'v2-lmztenbsfqytemzulu-u5tl6rscltjidqlt3o4p2lyg6targ7sq@remailer.example.com'
 
 class TestRemailer(UffdTestCase):
 	def test_is_remailer_domain(self):
@@ -20,44 +24,62 @@ class TestRemailer(UffdTestCase):
 		self.assertTrue(remailer.is_remailer_domain('other.remailer.example.com'))
 		self.assertFalse(remailer.is_remailer_domain('example.com'))
 
-	def test_build_address(self):
+	def test_build_v1_address(self):
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		self.assertTrue(remailer.build_address(SERVICE1_ID, USER_ID).endswith('@remailer.example.com'))
-		self.assertTrue(remailer.build_address(SERVICE2_ID, USER_ID).endswith('@remailer.example.com'))
-		self.assertLessEqual(len(remailer.build_local_part(SERVICE1_ID, USER_ID)), 64)
-		self.assertLessEqual(len(remailer.build_address(SERVICE1_ID, USER_ID)), 256)
-		self.assertEqual(remailer.build_address(SERVICE1_ID, USER_ID), remailer.build_address(SERVICE1_ID, USER_ID))
-		self.assertNotEqual(remailer.build_address(SERVICE1_ID, USER_ID), remailer.build_address(SERVICE2_ID, USER_ID))
-		addr = remailer.build_address(SERVICE1_ID, USER_ID)
+		self.assertEqual(remailer.build_v1_address(SERVICE1_ID, USER_ID), ADDR_V1_S1)
+		self.assertEqual(remailer.build_v1_address(SERVICE2_ID, USER_ID), ADDR_V1_S2)
+		long_addr = remailer.build_v1_address(1000, 1000000)
+		self.assertLessEqual(len(long_addr.split('@')[0]), 64)
+		self.assertLessEqual(len(long_addr), 256)
 		self.app.config['REMAILER_OLD_DOMAINS'] = ['old.remailer.example.com']
-		self.assertEqual(remailer.build_address(SERVICE1_ID, USER_ID), addr)
-		self.assertTrue(remailer.build_address(SERVICE1_ID, USER_ID).endswith('@remailer.example.com'))
+		self.assertEqual(remailer.build_v1_address(SERVICE1_ID, USER_ID), ADDR_V1_S1)
 		self.app.config['REMAILER_SECRET_KEY'] = self.app.config['SECRET_KEY']
-		self.assertEqual(remailer.build_address(SERVICE1_ID, USER_ID), addr)
+		self.assertEqual(remailer.build_v1_address(SERVICE1_ID, USER_ID), ADDR_V1_S1)
 		self.app.config['REMAILER_SECRET_KEY'] = 'REMAILER-DEBUGKEY'
-		self.assertNotEqual(remailer.build_address(SERVICE1_ID, USER_ID), addr)
+		self.assertNotEqual(remailer.build_v1_address(SERVICE1_ID, USER_ID), ADDR_V1_S1)
 
-	def test_parse_address(self):
+	def test_build_v2_address(self):
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		addr = remailer.build_address(SERVICE2_ID, USER_ID)
+		self.assertEqual(remailer.build_v2_address(SERVICE1_ID, USER_ID), ADDR_V2_S1)
+		self.assertEqual(remailer.build_v2_address(SERVICE2_ID, USER_ID), ADDR_V2_S2)
+		long_addr = remailer.build_v2_address(1000, 1000000)
+		self.assertLessEqual(len(long_addr.split('@')[0]), 64)
+		self.assertLessEqual(len(long_addr), 256)
+		self.app.config['REMAILER_OLD_DOMAINS'] = ['old.remailer.example.com']
+		self.assertEqual(remailer.build_v2_address(SERVICE1_ID, USER_ID), ADDR_V2_S1)
+		self.app.config['REMAILER_SECRET_KEY'] = self.app.config['SECRET_KEY']
+		self.assertEqual(remailer.build_v2_address(SERVICE1_ID, USER_ID), ADDR_V2_S1)
+		self.app.config['REMAILER_SECRET_KEY'] = 'REMAILER-DEBUGKEY'
+		self.assertNotEqual(remailer.build_v2_address(SERVICE1_ID, USER_ID), ADDR_V2_S1)
+
+	def test_parse_address(self):
 		# REMAILER_DOMAIN behaviour
 		self.app.config['REMAILER_DOMAIN'] = None
-		self.assertIsNone(remailer.parse_address(addr))
+		self.assertIsNone(remailer.parse_address(ADDR_V1_S2))
+		self.assertIsNone(remailer.parse_address(ADDR_V2_S2))
 		self.assertIsNone(remailer.parse_address('foo@example.com'))
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		self.assertEqual(remailer.parse_address(addr), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V1_S2), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V2_S2), (SERVICE2_ID, USER_ID))
 		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.assertIsNone(remailer.parse_address('v2-foo@remailer.example.com'))
+		self.assertIsNone(remailer.parse_address('v2-foo-bar@remailer.example.com'))
 		self.app.config['REMAILER_DOMAIN'] = 'new-remailer.example.com'
-		self.assertIsNone(remailer.parse_address(addr))
+		self.assertIsNone(remailer.parse_address(ADDR_V1_S2))
+		self.assertIsNone(remailer.parse_address(ADDR_V2_S2))
 		self.app.config['REMAILER_OLD_DOMAINS'] = ['remailer.example.com']
-		self.assertEqual(remailer.parse_address(addr), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V1_S2), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V2_S2), (SERVICE2_ID, USER_ID))
 		# REMAILER_SECRET_KEY behaviour
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
 		self.app.config['REMAILER_OLD_DOMAINS'] = []
-		self.assertEqual(remailer.parse_address(addr), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V1_S2), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V2_S2), (SERVICE2_ID, USER_ID))
 		self.app.config['REMAILER_SECRET_KEY'] = self.app.config['SECRET_KEY']
-		self.assertEqual(remailer.parse_address(addr), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V1_S2), (SERVICE2_ID, USER_ID))
+		self.assertEqual(remailer.parse_address(ADDR_V2_S2), (SERVICE2_ID, USER_ID))
 		self.app.config['REMAILER_SECRET_KEY'] = 'REMAILER-DEBUGKEY'
-		self.assertIsNone(remailer.parse_address(addr))
+		self.assertIsNone(remailer.parse_address(ADDR_V1_S2))
+		self.assertIsNone(remailer.parse_address(ADDR_V2_S2))
diff --git a/tests/test_services.py b/tests/test_services.py
index 14ec4f70..5a1848bc 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -7,12 +7,12 @@ 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, UserEmail
+from uffd.models import Service, ServiceUser, User, UserEmail, RemailerMode
 
 class TestServiceUser(UffdTestCase):
 	def setUp(self):
 		super().setUp()
-		db.session.add_all([Service(name='service1'), Service(name='service2', use_remailer=True)])
+		db.session.add_all([Service(name='service1'), Service(name='service2', remailer_mode=RemailerMode.ENABLED_V1)])
 		db.session.commit()
 
 	def test_auto_create(self):
@@ -64,21 +64,12 @@ class TestServiceUser(UffdTestCase):
 		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()
-		service = Service.query.filter_by(name='service1').first()
-		service_user = ServiceUser.query.get((service.id, user.id))
-		with self.assertRaises(Exception):
-			service_user.remailer_email
-		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		self.assertEqual(service_user.remailer_email, remailer.build_address(service.id, user.id))
-
 	def test_get_by_remailer_email(self):
 		user = self.get_user()
 		service = Service.query.filter_by(name='service1').first()
 		service_user = ServiceUser.query.get((service.id, user.id))
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		remailer_email = remailer.build_address(service.id, user.id)
+		remailer_email = remailer.build_v1_address(service.id, user.id)
 		# 1. remailer not setup
 		self.app.config['REMAILER_DOMAIN'] = ''
 		self.assertIsNone(ServiceUser.get_by_remailer_email(user.primary_email.address))
@@ -95,21 +86,21 @@ class TestServiceUser(UffdTestCase):
 		service = Service.query.filter_by(name='service1').first()
 		service_user = ServiceUser.query.get((service.id, user.id))
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
-		remailer_email = remailer.build_address(service.id, user.id)
+		remailer_email = remailer.build_v1_address(service.id, user.id)
 		# 1. remailer not setup
 		self.app.config['REMAILER_DOMAIN'] = ''
 		self.assertEqual(service_user.email, user.primary_email.address)
-		# 2. remailer setup + service.use_remailer disabled
+		# 2. remailer setup + remailer disabled
 		self.app.config['REMAILER_DOMAIN'] = 'remailer.example.com'
 		self.assertEqual(service_user.email, user.primary_email.address)
-		# 3. remailer setup + service.use_remailer enabled + REMAILER_LIMIT_TO_USERS unset
-		service.use_remailer = True
+		# 3. remailer setup + remailer enabled + REMAILER_LIMIT_TO_USERS unset
+		service.remailer_mode = RemailerMode.ENABLED_V1
 		db.session.commit()
 		self.assertEqual(service_user.email, remailer_email)
-		# 4. remailer setup + service.use_remailer enabled + REMAILER_LIMIT_TO_USERS does not include user
+		# 4. remailer setup + remailer enabled + REMAILER_LIMIT_TO_USERS does not include user
 		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testadmin']
 		self.assertEqual(service_user.email, user.primary_email.address)
-		# 5. remailer setup + service.use_remailer enabled + REMAILER_LIMIT_TO_USERS includes user
+		# 5. remailer setup + remailer enabled + REMAILER_LIMIT_TO_USERS includes user
 		self.app.config['REMAILER_LIMIT_TO_USERS'] = ['testuser']
 		self.assertEqual(service_user.email, remailer_email)
 
@@ -121,13 +112,13 @@ class TestServiceUser(UffdTestCase):
 		user2 = User(loginname='user2', primary_email_address=user1.primary_email.address, displayname='User 2')
 		db.session.add(user2)
 		db.session.commit()
-		service1 = Service.query.filter_by(name='service1').first() # use_remailer=False
-		service2 = Service.query.filter_by(name='service2').first() # use_remailer=True
+		service1 = Service.query.filter_by(name='service1').first() # remailer disabled
+		service2 = Service.query.filter_by(name='service2').first() # remailer enabled
 		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)
-		remailer_email1_2 = remailer.build_address(service1.id, user2.id)
-		remailer_email2_2 = remailer.build_address(service2.id, user2.id)
+		remailer_email1_1 = remailer.build_v1_address(service1.id, user1.id)
+		remailer_email2_1 = remailer.build_v1_address(service2.id, user1.id)
+		remailer_email1_2 = remailer.build_v1_address(service1.id, user2.id)
+		remailer_email2_2 = remailer.build_v1_address(service2.id, user2.id)
 
 		# 1. remailer disabled
 		self.app.config['REMAILER_DOMAIN'] = ''
@@ -181,11 +172,11 @@ class TestServiceUser(UffdTestCase):
 			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
+		service1 = Service.query.filter_by(name='service1').first() # remailer disabled
+		service2 = Service.query.filter_by(name='service2').first() # remailer enabled
 		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)
+		remailer_email1_1 = remailer.build_v1_address(service1.id, user1.id)
+		remailer_email2_1 = remailer.build_v1_address(service2.id, user1.id)
 
 		self.app.config['REMAILER_DOMAIN'] = ''
 		self.assertEqual(run_query(user1.primary_email.address), {
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..3d42f443
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,12 @@
+from uffd.utils import nopad_b32decode, nopad_b32encode, nopad_urlsafe_b64decode, nopad_urlsafe_b64encode
+
+from utils import UffdTestCase
+
+class TestUtils(UffdTestCase):
+	def test_nopad_b32(self):
+		for n in range(0, 32):
+			self.assertEqual(b'X'*n, nopad_b32decode(nopad_b32encode(b'X'*n)))
+
+	def test_nopad_b64(self):
+		for n in range(0, 32):
+			self.assertEqual(b'X'*n, nopad_urlsafe_b64decode(nopad_urlsafe_b64encode(b'X'*n)))
diff --git a/uffd/migrations/versions/2b68f688bec1_remailer_v2.py b/uffd/migrations/versions/2b68f688bec1_remailer_v2.py
new file mode 100644
index 00000000..bdf95ccc
--- /dev/null
+++ b/uffd/migrations/versions/2b68f688bec1_remailer_v2.py
@@ -0,0 +1,66 @@
+"""Remailer v2
+
+Revision ID: 2b68f688bec1
+Revises: e13b733ec856
+Create Date: 2022-10-20 03:40:11.522343
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = '2b68f688bec1'
+down_revision = 'e13b733ec856'
+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('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=False, server_default='DISABLED'))
+	service = sa.table('service',
+		sa.column('id', sa.Integer),
+		sa.column('use_remailer', sa.Boolean),
+		sa.column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode')),
+	)
+	op.execute(service.update().values(remailer_mode='ENABLED_V1').where(service.c.use_remailer))
+	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),
+    sa.Column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=False, server_default='DISABLED'),
+    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('remailer_mode', server_default=None)
+		batch_op.drop_column('use_remailer')
+
+def downgrade():
+	with op.batch_alter_table('service', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('use_remailer', sa.BOOLEAN(), nullable=False, server_default=sa.false()))
+	service = sa.table('service',
+		sa.column('id', sa.Integer),
+		sa.column('use_remailer', sa.Boolean),
+		sa.column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode')),
+	)
+	op.execute(service.update().values(use_remailer=sa.true()).where(service.c.remailer_mode != 'DISABLED'))
+	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, server_default=sa.false()),
+    sa.Column('enable_email_preferences', sa.Boolean(), nullable=False),
+    sa.Column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), 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.alter_column('use_remailer', server_default=None)
+		batch_op.drop_column('remailer_mode')
diff --git a/uffd/models/__init__.py b/uffd/models/__init__.py
index 52d9709b..1644e472 100644
--- a/uffd/models/__init__.py
+++ b/uffd/models/__init__.py
@@ -5,7 +5,7 @@ from .mfa import MFAType, MFAMethod, RecoveryCodeMethod, TOTPMethod, WebauthnMet
 from .oauth2 import OAuth2Client, OAuth2RedirectURI, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation
 from .role import Role, RoleGroup, RoleGroupMap
 from .selfservice import PasswordToken
-from .service import Service, ServiceUser, get_services
+from .service import RemailerMode, Service, ServiceUser, get_services
 from .session import DeviceLoginType, DeviceLoginInitiation, DeviceLoginConfirmation
 from .signup import Signup
 from .user import User, UserEmail, Group
@@ -19,8 +19,7 @@ __all__ = [
 	'OAuth2Client', 'OAuth2RedirectURI', 'OAuth2LogoutURI', 'OAuth2Grant', 'OAuth2Token', 'OAuth2DeviceLoginInitiation',
 	'Role', 'RoleGroup', 'RoleGroupMap',
 	'PasswordToken',
-	'Service', 'get_services',
-	'Service', 'ServiceUser', 'get_services',
+	'RemailerMode', 'Service', 'ServiceUser', 'get_services',
 	'DeviceLoginType', 'DeviceLoginInitiation', 'DeviceLoginConfirmation',
 	'Signup',
 	'User', 'UserEmail', 'Group',
diff --git a/uffd/models/mfa.py b/uffd/models/mfa.py
index 9d2b3458..e6b34eaa 100644
--- a/uffd/models/mfa.py
+++ b/uffd/models/mfa.py
@@ -15,6 +15,7 @@ from flask import request, current_app
 from sqlalchemy import Column, Integer, Enum, String, DateTime, Text, ForeignKey
 from sqlalchemy.orm import relationship, backref
 
+from uffd.utils import nopad_b32decode, nopad_b32encode
 from uffd.database import db
 from .user import User
 
@@ -88,13 +89,12 @@ class TOTPMethod(MFAMethod):
 	def __init__(self, user, name=None, key=None):
 		super().__init__(user, name)
 		if key is None:
-			key = base64.b32encode(secrets.token_bytes(16)).rstrip(b'=').decode()
+			key = nopad_b32encode(secrets.token_bytes(16)).decode()
 		self.key = key
 
 	@property
 	def raw_key(self):
-		tmp = self.key + '='*(8 - (len(self.key) % 8))
-		return base64.b32decode(tmp.encode())
+		return nopad_b32decode(self.key)
 
 	@property
 	def issuer(self):
diff --git a/uffd/models/service.py b/uffd/models/service.py
index f6927e28..c5e5cbcc 100644
--- a/uffd/models/service.py
+++ b/uffd/models/service.py
@@ -1,6 +1,8 @@
+import enum
+
 from flask import current_app
 from flask_babel import get_locale
-from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
+from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Enum
 from sqlalchemy.orm import relationship, validates
 
 from uffd.database import db
@@ -8,6 +10,11 @@ from uffd.remailer import remailer
 from uffd.tasks import cleanup_task
 from .user import User, UserEmail
 
+class RemailerMode(enum.Enum):
+	DISABLED = 0
+	ENABLED_V1 = 1
+	ENABLED_V2 = 2
+
 class Service(db.Model):
 	__tablename__ = 'service'
 	id = Column(Integer, primary_key=True, autoincrement=True)
@@ -26,7 +33,7 @@ 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)
+	remailer_mode = Column(Enum(RemailerMode), default=RemailerMode.DISABLED, nullable=False)
 	enable_email_preferences = Column(Boolean(), default=False, nullable=False)
 
 class ServiceUser(db.Model):
@@ -72,12 +79,6 @@ class ServiceUser(db.Model):
 			return self.service_email.address
 		return self.user.primary_email.address
 
-	@property
-	def remailer_email(self):
-		if not remailer.configured:
-			raise Exception('ServiceUser.remailer_email accessed with unconfigured remailer')
-		return remailer.build_address(self.service_id, self.user_id)
-
 	@classmethod
 	def get_by_remailer_email(cls, address):
 		if not remailer.configured:
@@ -91,11 +92,14 @@ class ServiceUser(db.Model):
 	# E-Mail address as seen by the service
 	@property
 	def email(self):
-		use_remailer = remailer.configured and self.service.use_remailer
-		if current_app.config['REMAILER_LIMIT_TO_USERS'] is not None:
-			use_remailer = use_remailer and self.user.loginname in current_app.config['REMAILER_LIMIT_TO_USERS']
-		if use_remailer:
-			return self.remailer_email
+		if current_app.config['REMAILER_LIMIT_TO_USERS'] is None:
+			use_remailer = remailer.configured
+		else:
+			use_remailer = remailer.configured and self.user.loginname in current_app.config['REMAILER_LIMIT_TO_USERS']
+		if use_remailer and self.service.remailer_mode == RemailerMode.ENABLED_V1:
+			return remailer.build_v1_address(self.service_id, self.user_id)
+		if use_remailer and self.service.remailer_mode == RemailerMode.ENABLED_V2:
+			return remailer.build_v2_address(self.service_id, self.user_id)
 		return self.real_email
 
 	@classmethod
@@ -117,17 +121,20 @@ class ServiceUser(db.Model):
 		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
+		remailer_enabled_expr = db.and_(
+			AliasedService.remailer_mode != RemailerMode.DISABLED,
+			remailer.configured
+		)
 		if current_app.config['REMAILER_LIMIT_TO_USERS'] is not None:
 			remailer_enabled_expr = db.and_(
 				remailer_enabled_expr,
 				AliasedUser.loginname.in_(current_app.config['REMAILER_LIMIT_TO_USERS']),
 			)
 		real_email_matches_expr = db.case(
-			whens=(
+			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))
diff --git a/uffd/remailer.py b/uffd/remailer.py
index a4b37506..f3ffd079 100644
--- a/uffd/remailer.py
+++ b/uffd/remailer.py
@@ -1,13 +1,18 @@
 from flask import current_app
 import itsdangerous
 
+from uffd.utils import nopad_b32decode, nopad_b32encode, nopad_urlsafe_b64decode, nopad_urlsafe_b64encode
+
 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.'''
+	the remailer addresses are useless for third-parties.
+
+	Version 2 of the remailer address format is tolerant to case conversions at
+	the cost of being slightly longer.'''
 
 	# pylint: disable=no-self-use
 
@@ -19,11 +24,16 @@ class Remailer:
 		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, service_id, user_id):
-		return 'v1-' + self.get_serializer().dumps([service_id, user_id])
+	def build_v1_address(self, service_id, user_id):
+		payload = self.get_serializer().dumps([service_id, user_id])
+		return 'v1-' + payload + '@' + current_app.config['REMAILER_DOMAIN']
 
-	def build_address(self, service_id, user_id):
-		return self.build_local_part(service_id, user_id) + '@' + current_app.config['REMAILER_DOMAIN']
+	def build_v2_address(self, service_id, user_id):
+		data, sign = self.get_serializer().dumps([service_id, user_id]).split('.', 1)
+		data = nopad_b32encode(nopad_urlsafe_b64decode(data)).decode().lower()
+		sign = nopad_b32encode(nopad_urlsafe_b64decode(sign)).decode().lower()
+		payload = data + '-' + sign
+		return 'v2-' + payload + '@' + current_app.config['REMAILER_DOMAIN']
 
 	def is_remailer_domain(self, domain):
 		domains = {domain.lower().strip() for domain in current_app.config['REMAILER_OLD_DOMAINS']}
@@ -31,17 +41,38 @@ class Remailer:
 			domains.add(current_app.config['REMAILER_DOMAIN'].lower().strip())
 		return domain.lower().strip() in domains
 
-	def parse_address(self, address):
-		if '@' not in address:
+	def parse_v1_payload(self, payload):
+		try:
+			service_id, user_id = self.get_serializer().loads(payload)
+		except itsdangerous.BadData:
 			return None
-		local_part, domain = address.rsplit('@', 1)
-		if not self.is_remailer_domain(domain) or not local_part.startswith('v1-'):
+		return (service_id, user_id)
+
+	def parse_v2_payload(self, payload):
+		data, sign = (payload.split('-', 1) + [''])[:2]
+		try:
+			data = nopad_urlsafe_b64encode(nopad_b32decode(data.upper())).decode()
+			sign = nopad_urlsafe_b64encode(nopad_b32decode(sign.upper())).decode()
+		except ValueError:
 			return None
-		data = local_part[len('v1-'):]
+		payload = data + '.' + sign
 		try:
-			service_id, user_id = self.get_serializer().loads(data)
+			service_id, user_id = self.get_serializer().loads(payload)
 		except itsdangerous.BadData:
 			return None
 		return (service_id, user_id)
 
+	def parse_address(self, address):
+		if '@' not in address:
+			return None
+		local_part, domain = address.rsplit('@', 1)
+		if not self.is_remailer_domain(domain):
+			return None
+		prefix, payload = (local_part.strip().split('-', 1) + [''])[:2]
+		if prefix == 'v1':
+			return self.parse_v1_payload(payload)
+		if prefix.lower() == 'v2':
+			return self.parse_v2_payload(payload)
+		return None
+
 remailer = Remailer()
diff --git a/uffd/templates/service/show.html b/uffd/templates/service/show.html
index 55a12328..6c6fca94 100644
--- a/uffd/templates/service/show.html
+++ b/uffd/templates/service/show.html
@@ -30,19 +30,37 @@
 				{% 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>
+			<label for="remailer-mode">
+				{{ _('Hide e-mail addresses with remailer') }}
 				{% 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>
+			</label>
+			<select class="form-control" id="remailer-mode" name="remailer-mode">
+				<option value="{{ RemailerMode.DISABLED.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.DISABLED }}>
+					{{ _('Remailer disabled') }}
+				</option>
+				<option value="{{ RemailerMode.ENABLED_V2.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.ENABLED_V2 }}>
+					{{ _('Remailer enabled') }}
+				</option>
+				<option value="{{ RemailerMode.ENABLED_V1.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.ENABLED_V1 }}>
+					{{ _('Remailer enabled (deprecated, case-sensitive format)') }}
+				</option>
+			</select>
+			<small class="form-text text-muted">
+				{{ _('Some services notify users about changes to their e-mail address. Modifying this setting immediatly affects the e-mail addresses of all users and can cause masses of notification e-mails.') }}
+			</small>
 		</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>
+				<label class="form-check-label" for="service-enable-email-preferences">{{ _('Allow users to select a different e-mail address for this service') }}</label>
+			<small class="form-text text-muted">
+				{{ _('If disabled, the service always uses the primary e-mail address.') }}
+			</small>
 			</div>
 		</div>
 
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 8187048476a9226c2764f6b1fe76560a34e30c9d..52af6cfad16ce638fbfb8e1ea2d018fd317566d8 100644
GIT binary patch
delta 7496
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
UoKn*>^YT&^QZhGZ)gKWD06!w4ApigX

delta 6579
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
H=5yizY;R>j

diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po
index 4a00b8e8..39b09746 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-10-19 22:14+0200\n"
+"POT-Creation-Date: 2022-10-20 17:36+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:57
-#: uffd/templates/service/show.html:85 uffd/templates/user/list.html:8
+#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:73
+#: uffd/templates/service/show.html:101 uffd/templates/user/list.html:8
 msgid "New"
 msgstr "Neu"
 
@@ -158,7 +158,7 @@ msgstr "GID"
 #: uffd/templates/rolemod/list.html:9 uffd/templates/rolemod/show.html:44
 #: uffd/templates/selfservice/self.html:189
 #: uffd/templates/service/index.html:14 uffd/templates/service/show.html:20
-#: uffd/templates/service/show.html:91 uffd/templates/user/show.html:189
+#: uffd/templates/service/show.html:107 uffd/templates/user/show.html:189
 #: uffd/templates/user/show.html:221
 msgid "Name"
 msgstr "Name"
@@ -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:92
+#: uffd/templates/service/show.html:108
 msgid "Permissions"
 msgstr "Berechtigungen"
 
@@ -1227,7 +1227,7 @@ msgstr "Zugriff auf Mail-Weiterleitungen"
 msgid "Resolve remailer addresses"
 msgstr "Auflösen von Remailer-Adressen"
 
-#: uffd/templates/service/api.html:51 uffd/templates/service/show.html:38
+#: uffd/templates/service/api.html:51 uffd/templates/service/show.html:37
 msgid "This option has no effect: Remailer config options are unset"
 msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
 
@@ -1235,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:63
+#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:79
 msgid "Client ID"
 msgstr "Client-ID"
 
@@ -1305,12 +1305,43 @@ msgid "Members of group \"%(group_name)s\" have access"
 msgstr "Mitglieder der Gruppe \"%(group_name)s\" haben Zugriff"
 
 #: uffd/templates/service/show.html:36
-msgid "Hide mail addresses with remailer"
-msgstr "Verstecke Mailadressen mit dem Remailer"
+msgid "Hide e-mail addresses with remailer"
+msgstr "E-Mail-Adressen mit Remailer verstecken"
 
-#: uffd/templates/service/show.html:45
-msgid "Enable user mail preferences"
-msgstr "User E-Mail-Einstellungen aktivieren"
+#: uffd/templates/service/show.html:41
+msgid "Remailer disabled"
+msgstr "Remailer deaktiviert"
+
+#: uffd/templates/service/show.html:44
+msgid "Remailer enabled"
+msgstr "Remailer aktiviert"
+
+#: uffd/templates/service/show.html:47
+msgid "Remailer enabled (deprecated, case-sensitive format)"
+msgstr ""
+"Remailer aktiviert (veraltetes, Groß-/Kleinschreibung-unterscheidendes "
+"Format)"
+
+#: uffd/templates/service/show.html:51
+msgid ""
+"Some services notify users about changes to their e-mail address. "
+"Modifying this setting immediatly affects the e-mail addresses of all "
+"users and can cause masses of notification e-mails."
+msgstr ""
+"Einige Dienste benachrichtigen Nutzer bei Änderungen ihrer E-Mail-"
+"Adresse. Diese Einstellung zu verändern wirkt sich unmittelbar auf die E"
+"-Mail-Adressen aller Nutzer aus und kann zu massenhaftem Versand von "
+"Benachrichtigungs-E-Mails führen."
+
+#: uffd/templates/service/show.html:58
+msgid "Allow users to select a different e-mail address for this service"
+msgstr ""
+"Ermögliche Nutzern für diesen Dienst eine andere E-Mail-Adresse "
+"auszuwählen"
+
+#: uffd/templates/service/show.html:60
+msgid "If disabled, the service always uses the primary e-mail address."
+msgstr "Wenn deaktiviert, wird immer die primäre E-Mail-Adresse verwendet."
 
 #: uffd/templates/session/deviceauth.html:15
 msgid "Log into a service on another device without entering your password."
@@ -1868,7 +1899,7 @@ msgstr "E-Mail-Einstellungen geändert"
 msgid "You left role %(role_name)s"
 msgstr "Rolle %(role_name)s verlassen"
 
-#: uffd/views/service.py:32
+#: uffd/views/service.py:34
 msgid "Services"
 msgstr "Dienste"
 
diff --git a/uffd/utils.py b/uffd/utils.py
index ff40711c..dcdf8b08 100644
--- a/uffd/utils.py
+++ b/uffd/utils.py
@@ -1,5 +1,6 @@
 import secrets
 import math
+import base64
 
 def token_with_alphabet(alphabet, nbytes=None):
 	'''Return random text token that consists of characters from `alphabet`'''
@@ -18,3 +19,19 @@ def token_urlfriendly(nbytes=None):
 	'''Return random text token that is urlsafe and works around common parsing bugs'''
 	alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 	return token_with_alphabet(alphabet, nbytes=nbytes)
+
+def nopad_b32decode(value):
+	if isinstance(value, bytes):
+		value = value.decode()
+	return base64.b32decode(value + ('=' * (-len(value) % 8)))
+
+def nopad_b32encode(value):
+	return base64.b32encode(value).rstrip(b'=')
+
+def nopad_urlsafe_b64decode(value):
+	if isinstance(value, bytes):
+		value = value.decode()
+	return base64.urlsafe_b64decode(value + ('=' * (-len(value) % 4)))
+
+def nopad_urlsafe_b64encode(value):
+	return base64.urlsafe_b64encode(value).rstrip(b'=')
diff --git a/uffd/views/service.py b/uffd/views/service.py
index a8c9e28d..7f5b36e0 100644
--- a/uffd/views/service.py
+++ b/uffd/views/service.py
@@ -6,12 +6,14 @@ from flask_babel import lazy_gettext
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
 from uffd.database import db
-from uffd.models import Service, get_services, Group, OAuth2Client, OAuth2LogoutURI, APIClient
+from uffd.models import Service, get_services, Group, OAuth2Client, OAuth2LogoutURI, APIClient, RemailerMode
 
 from .session import login_required
 
 bp = Blueprint('service', __name__, template_folder='templates')
 
+bp.add_app_template_global(RemailerMode, 'RemailerMode')
+
 def admin_acl():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
@@ -71,7 +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'
+	service.remailer_mode = RemailerMode[request.form['remailer-mode']]
 	service.enable_email_preferences = request.form.get('enable_email_preferences') == '1'
 	db.session.commit()
 	return redirect(url_for('service.show', id=service.id))
-- 
GitLab