diff --git a/constraints.txt b/constraints.txt
index 18e05ba613732671a30c101753f10d1c621c7fc0..f952fe3bd4861674c2bc508f9a5bf8b2b4119aa5 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -7,6 +7,7 @@ charset-normalizer==2.1.1
 click==8.1.3
 dateutils==0.6.12
 deprecation==2.1.0
+dnspython==2.2.1
 dominate==2.7.0
 Flask==2.2.2
 Flask-Bootstrap==3.3.7.1
diff --git a/doc/admin.md b/doc/admin.md
new file mode 100644
index 0000000000000000000000000000000000000000..9cc34d51dace0afb2d1b003dc286c3e3bd6bb278
--- /dev/null
+++ b/doc/admin.md
@@ -0,0 +1,11 @@
+Návod pro správce
+=================
+
+Zatím pouze útržkovité poznámky, které časem přestěhujeme na lepší místo
+
+- *Falešné e-maily:* Pokud je potřeba založit účet bez e-mailové adresy,
+  dá se použít adresa tvaru *někdo*`@nomail`. Na tu pak poštu neposíláme.
+  Při vytváření účtu je ale potřeba ověřit, že vymyšlená adresa je unikátní.
+
+- *Testovací e-maily:* Podobně v testovacích instancích používáme účty
+  *někdo*`@test`. Na ty se pošta také neposílá.
diff --git a/mo/email.py b/mo/email.py
index d2fa1572952b2192241d1d2cce6484506cda35ff..1ce77aad1a3a50da77b5658b58098eeb2b3fb4a9 100644
--- a/mo/email.py
+++ b/mo/email.py
@@ -12,6 +12,7 @@ import urllib.parse
 
 import mo.db as db
 import mo.config as config
+import mo.users
 from mo.util import logger, ExceptionInfo
 
 
@@ -45,6 +46,10 @@ def send_email(send_to: str, full_name: str, subject: str, body: str) -> bool:
     if mail_instead is not None:
         send_to = mail_instead
 
+    if mo.users.email_is_fake(send_to):
+        logger.info('Mail: Neposíláme, adresa je falešná')
+        return True
+
     sm = subprocess.Popen(
         [
             '/usr/sbin/sendmail',
diff --git a/mo/imports.py b/mo/imports.py
index 620d4971185c553c5f3bb551d9f694ce93f4b1d3..5833b3a4039a0bd5c94117f267f3322d0137bbb7 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -91,6 +91,8 @@ class Import:
             return self.error('Chybí e-mailová adresa')
 
         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)
         except mo.CheckError as e:
             return self.error(str(e))
diff --git a/mo/users.py b/mo/users.py
index b57c6e0557861c16c31f7ac9afb63089d2d94aeb..e765fe2f5366c20d0b778c54568b8aea8b9c44e1 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -3,6 +3,8 @@
 import bcrypt
 import datetime
 import dateutil.tz
+import dns.exception
+import dns.resolver
 import email.errors
 import email.headerregistry
 import re
@@ -246,7 +248,44 @@ def find_or_create_participation(user: db.User, contest: db.Contest, place: Opti
     return pion, is_new
 
 
-def normalize_email(addr: str) -> str:
+def email_is_fake(addr: str) -> bool:
+    return addr.endswith('@nomail') or addr.endswith('@test')
+
+
+bad_domains = {
+    'gmail.cz',
+}
+
+
+def email_check_domain(domain: str):
+    # Některé domény rovnou odmítáme
+    if domain in bad_domains:
+        logger.info(f'DNS: Doména <{domain}> na blacklistu')
+        raise mo.CheckError('Doména {domain} nepřijímá poštu')
+
+    for record in ['MX', 'A', 'AAAA']:
+        try:
+            dns.resolver.resolve(domain, record, lifetime=2, search=False)
+        except dns.exception.Timeout:
+            # Kontrola je konzervativní, při timeoutu adresu raději schválíme
+            logger.info(f'DNS: Timeout při kontrole domény <{domain}>')
+            return
+        except dns.resolver.NoAnswer:
+            logger.debug(f'DNS: Doména <{domain}> neobsahuje {record}')
+        except dns.resolver.NXDOMAIN:
+            logger.info(f'DNS: Doména <{domain}> neexistuje')
+            raise mo.CheckError('Adresa obsahuje neexistující doménu {domain}')
+        except dns.exception.DNSException as e:
+            logger.warn(f'DNS: Záznam <{domain}>/{record} nejde resolvovat: {e}')
+            return
+        logger.debug(f'DNS: Doména <{domain}> OK')
+        return
+
+    logger.info(f'DNS: Doména <{domain}> nepřijímá poštu')
+    raise mo.CheckError('Doména {domain} nepřijímá poštu')
+
+
+def normalize_email(addr: str, check_existence: bool = False) -> str:
     if not re.fullmatch(r'.+@.+', addr):
         raise mo.CheckError('V e-mailové adrese chybí zavináč')
 
@@ -263,10 +302,16 @@ def normalize_email(addr: str) -> str:
     try:
         # Tady úmyslně používáme knihovnu jen ke kontrole a ne k normalizaci,
         # protože nechceme riskovat, že se normalizovaný tvar časem změní.
-        email.headerregistry.Address(addr_spec=addr)
+        addr_obj = email.headerregistry.Address(addr_spec=addr)
     except (email.errors.HeaderParseError, ValueError):
         raise mo.CheckError('Chybná syntaxe mailové adresy')
 
+    if addr_obj.display_name != "":
+        raise mo.CheckError('Chybná syntaxe mailové adresy')
+
+    if check_existence and not email_is_fake(addr):
+        email_check_domain(addr_obj.domain)
+
     # XXX: Striktně vzato, tohle není korektní, protože některé domény mohou
     # mít case-sensitive levou stranu adresy. Ale i na nich se prakticky nevyskytují
     # levé strany s velkými písmeny, zatímco uživatelé při psaní adres běžně velká
diff --git a/mo/web/acct.py b/mo/web/acct.py
index 24f34bc2a38ac17937c03e88e57dd99a186e9365..d906e40943b0f4a79399e7154e88e09cef4bb8fa 100644
--- a/mo/web/acct.py
+++ b/mo/web/acct.py
@@ -156,7 +156,7 @@ def user_settings():
 
 
 class PersonalSettingsForm(FlaskForm):
-    email = mo_fields.Email(validators=[validators.DataRequired()])
+    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True)
     current_passwd = mo_fields.Password('Aktuální heslo', validators=[validators.DataRequired()])
     new_passwd = mo_fields.NewPassword(
         description=mo.users.password_help + ' Pokud nechcete heslo měnit, ponechte toto políčko prázdné.',
@@ -388,7 +388,7 @@ class Reg1:
 
 
 class Reg1Form(FlaskForm):
-    email = mo_fields.Email(validators=[validators.DataRequired()])
+    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True)
     token = wtforms.HiddenField()
     captcha = mo_fields.String('Kontrolní odpověď', validators=[validators.DataRequired()])
     submit = wtforms.SubmitField('Vytvořit účet')
diff --git a/mo/web/fields.py b/mo/web/fields.py
index 446b08023d4636801750c66056e06665977e1235..4c6ce5f57e226441d64a60a2fdca4c6f8437ef10 100644
--- a/mo/web/fields.py
+++ b/mo/web/fields.py
@@ -74,13 +74,16 @@ class Points(Decimal):
 
 
 class Email(Stripped, EmailField):
-    def __init__(self, label="E-mail", validators=None, **kwargs):
+    check_existence: bool
+
+    def __init__(self, label="E-mail", validators=None, check_existence: bool = False, **kwargs):
+        self.check_existence = check_existence
         super().__init__(label, validators, **kwargs)
 
     def pre_validate(field, form):
         if field.data:
             try:
-                field.data = mo.users.normalize_email(field.data)
+                field.data = mo.users.normalize_email(field.data, check_existence=field.check_existence)
             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 ed0eda1673c5859eddf9ca618c46418f44f466f0..5361d860187a4b6c89f45c04ddfb59fcabada283 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1810,7 +1810,7 @@ def check_contest_state(round: db.Round, contest: Optional[db.Contest], state: d
 
 
 class ParticipantAddForm(FlaskForm):
-    email = mo_fields.Email(validators=[validators.DataRequired()])
+    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True)
     first_name = mo_fields.FirstName(validators=[validators.Optional()])
     last_name = mo_fields.LastName(validators=[validators.Optional()])
     school = mo_fields.School(validators=[validators.Optional()])
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 237983f1387660b053ad377ded3fa6ef64585fa3..400d142ff41827bfefbc1b9d00edd635e3fedd89 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -413,7 +413,7 @@ 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()])
+    email = mo_fields.Email(validators=[DataRequired()], check_existence=True)
     note = wtforms.TextAreaField("Poznámka")
     is_test = wtforms.BooleanField("Testovací účet")
     allow_duplicate_name = wtforms.BooleanField("Přidat účet s duplicitním jménem")
diff --git a/setup.py b/setup.py
index 64f47dc1cc7c5cc15d2901f9b1fae1c673e58841..77e694f6268133fb1feb3b49e03a8dcbba227bd1 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@ setuptools.setup(
         'blinker',
         'click',
         'dateutils',
+        'dnspython',
         'flask_bootstrap',
         'flask_sqlalchemy',
         'markdown',