From acee0440ad716a08e30e9e8ed44b370c9ad69186 Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Sat, 18 Mar 2023 19:50:54 +0100
Subject: [PATCH] =?UTF-8?q?"@nomail"=20vyrob=C3=AD=20novou=20pravd=C4=9Bpo?=
 =?UTF-8?q?dobn=C4=9B=20unik=C3=A1tn=C3=AD=20nomailovou=20adresu?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Toto funguje všude, kde organizátor zakládá nového uživatele:
v editaci uživatelů, v přídávání do soutěže, v importech.
---
 mo/imports.py         |  2 +-
 mo/users.py           | 14 ++++++++++++--
 mo/web/fields.py      |  8 +++++---
 mo/web/org_contest.py |  5 +++--
 mo/web/org_users.py   |  3 ++-
 5 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/mo/imports.py b/mo/imports.py
index 5833b3a4..46b1e18e 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -93,7 +93,7 @@ class Import:
         try:
             # XXX: Zde si nemůžeme dovolit kontrolovat existenci domén,
             # protože import by byl příliš pomalý. Možná z něj jednou uděláme job...
-            return mo.users.normalize_email(email)
+            return mo.users.normalize_email(email, make_unique_nomail=True)
         except mo.CheckError as e:
             return self.error(str(e))
 
diff --git a/mo/users.py b/mo/users.py
index 7c9f887e..d3de7e54 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -1,5 +1,6 @@
 # Správa uživatelů
 
+import base64
 import bcrypt
 import datetime
 import dateutil.tz
@@ -292,10 +293,19 @@ def email_check_domain(domain: str):
     raise mo.CheckError(f'Doména {domain} nepřijímá poštu')
 
 
-def normalize_email(addr: str, check_existence: bool = False) -> str:
-    if not re.fullmatch(r'.+@.+', addr):
+def normalize_email(addr: str, check_existence: bool = False, make_unique_nomail: bool = False) -> str:
+    if make_unique_nomail and addr.endswith('@nomail'):
+        if addr == '@nomail':
+            addr = base64.b32encode(secrets.token_bytes(10)).decode('US-ASCII').lower() + '@nomail'
+        else:
+            raise mo.CheckError('Adresa @nomail nesmí obsahovat jméno uživatele před zavináčem')
+
+    if '@' not in addr:
         raise mo.CheckError('V e-mailové adrese chybí zavináč')
 
+    if not re.fullmatch(r'.*@.+', addr):
+        raise mo.CheckError('E-mailová adresa nesmí ani začínat, ani končit zavináčem')
+
     if re.search(r'[ \t]', addr):
         raise mo.CheckError('E-mailová adresa obsahuje mezeru')
 
diff --git a/mo/web/fields.py b/mo/web/fields.py
index 4c6ce5f5..2f939534 100644
--- a/mo/web/fields.py
+++ b/mo/web/fields.py
@@ -73,17 +73,19 @@ class Points(Decimal):
         super().__init__(label, validators, **kwargs)
 
 
-class Email(Stripped, EmailField):
+class Email(String):
     check_existence: bool
+    make_unique_nomail: bool
 
-    def __init__(self, label="E-mail", validators=None, check_existence: bool = False, **kwargs):
+    def __init__(self, label="E-mail", validators=None, check_existence: bool = False, make_unique_nomail: bool = False, **kwargs):
         self.check_existence = check_existence
+        self.make_unique_nomail = make_unique_nomail
         super().__init__(label, validators, **kwargs)
 
     def pre_validate(field, form):
         if field.data:
             try:
-                field.data = mo.users.normalize_email(field.data, check_existence=field.check_existence)
+                field.data = mo.users.normalize_email(field.data, check_existence=field.check_existence, make_unique_nomail=field.make_unique_nomail)
             except mo.CheckError as e:
                 raise wtforms.ValidationError(str(e))
 
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 67396174..a49d464b 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1809,7 +1809,7 @@ def check_contest_state(round: db.Round, contest: Optional[db.Contest], state: d
 
 
 class ParticipantAddForm(FlaskForm):
-    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True)
+    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True, make_unique_nomail=True)
     first_name = mo_fields.FirstName(validators=[validators.Optional()])
     last_name = mo_fields.LastName(validators=[validators.Optional()])
     school = mo_fields.School(validators=[validators.Optional()])
@@ -1819,7 +1819,8 @@ class ParticipantAddForm(FlaskForm):
     save = wtforms.SubmitField("Přidat")
 
     def set_descriptions(self, contest: db.Contest, place_desc: bool):
-        self.email.description = "Nepoužívejte prosím e-mailové adresy ve školních doménách, na které nejde posílat pošta z veřejné sítě."
+        self.email.description = ("Nepoužívejte prosím e-mailové adresy ve školních doménách, na které nejde posílat pošta z veřejné sítě. "
+                                  + "Pokud zatím neznáte e-mail, zadejte @nomail, ale pak adresu doplňte.")
         self.school.description = f'Kód školy najdete v <a href="{url_for("org_place", id=contest.place.place_id)}">katalogu míst</a>.'
         if place_desc:
             self.participation_place.description = f'Pokud účastník soutěží někde jinde než {contest.place.name_locative()}, vyplňte <a href="{url_for("org_place", id=contest.place.place_id)}">kód místa</a>. Dozor na tomto místě pak může za účastníka odevzdávat řešení.'
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 4fabde7c..d09b600e 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -412,7 +412,8 @@ def org_user(id: int):
 class UserEditForm(FlaskForm):
     first_name = mo_fields.FirstName(validators=[DataRequired()], render_kw={'autofocus': True})
     last_name = mo_fields.LastName(validators=[DataRequired()])
-    email = mo_fields.Email(validators=[DataRequired()], check_existence=True)
+    email = mo_fields.Email(validators=[DataRequired()], check_existence=True, make_unique_nomail=True,
+                            description="Pokud zadáte @nomail, vznikne účet bez e-mailové adresy, ke kterému se ale nepůjde přihlásit.")
     note = wtforms.TextAreaField("Poznámka")
     is_test = wtforms.BooleanField("Testovací účet")
     email_notify = wtforms.BooleanField("Mailové notifikace")
-- 
GitLab