diff --git a/etc/config.py.example b/etc/config.py.example
index 9644174652047277b2cf09437cdfe6d491adda30..5f2d3ba6f50c5a51d4a99016aad6ce647d562002 100644
--- a/etc/config.py.example
+++ b/etc/config.py.example
@@ -10,4 +10,11 @@ SECRET_KEY = "FIXME"
 SESSION_COOKIE_PATH = '/'
 SESSION_COOKIE_NAME = 'mo_session'
 
-MAIL_FROM = "osmo-auto@mo.mff.cuni.cz"
+# Odesilatel generovaných mailů (není-li definován, neposílají se)
+# MAIL_FROM = "osmo-auto@mo.mff.cuni.cz"
+
+# Pro testování je možné všechny odesílané maily přesměrovat na jinou adresu
+# MAIL_INSTEAD = "mares@kam.mff.cuni.cz"
+
+# URL, na kterém aplikace běží
+WEB_ROOT = 'https://mo.mff.cuni.cz/osmo-test/'
diff --git a/mo/util.py b/mo/util.py
index 7b361c1604c50fe75e4bcbfa6401ddd8069f662e..e3a6a582b37d0c6d59ce1d3182d30e7e8cb117a1 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -4,11 +4,13 @@ from dataclasses import dataclass
 import datetime
 import email.message
 import email.headerregistry
+import logging
 import re
 import subprocess
 import sys
 from typing import Any, Optional, NoReturn
 import textwrap
+import urllib.parse
 
 import mo.db as db
 import mo.config as config
@@ -17,6 +19,8 @@ current_log_user: Optional[db.User] = None
 
 
 def log(type: db.LogType, what: int, details: Any):
+    """Zapíše záznam do databázového logu."""
+
     entry = db.Log(
         changed_by=current_log_user.user_id if current_log_user else None,
         type=type,
@@ -26,13 +30,18 @@ def log(type: db.LogType, what: int, details: Any):
     db.get_session().add(entry)
 
 
-def send_password_reset_email(user: db.User, link: str):
-    if not hasattr(config, 'MAIL_FROM'):
-        raise RuntimeError('V configu chybí pole MAIL_FROM.')
+def send_user_email(user: db.User, subject: str, body: str) -> bool:
+    logging.info(f'Mail: "{subject}" -> {user.email}')
+
+    mail_from = getattr(config, 'MAIL_FROM', None)
+    if mail_from is None:
+        logging.error('Mail: V configu chybí nastavení MAIL_FROM')
+        return False
 
     msg = email.message.EmailMessage()
     msg['From'] = email.headerregistry.Address(
-        display_name='Odevzdávací Systém MO', addr_spec=config.MAIL_FROM
+        display_name='Odevzdávací Systém MO',
+        addr_spec=mail_from,
     )
     msg['To'] = [
         email.headerregistry.Address(
@@ -40,31 +49,63 @@ def send_password_reset_email(user: db.User, link: str):
             addr_spec=user.email,
         )
     ]
-    msg['Subject'] = 'OSMO – obnova hesla'
+    msg['Subject'] = 'OSMO – ' + subject
     msg['Date'] = datetime.datetime.now()
 
-    body = textwrap.dedent('''
-        Pro obnovení hesla pro váš účet v Odevzávacím Systému MO klikněte sem: {}
-
-        Váš OSMO
-    '''.format(link))
-
     msg.set_content(body, cte='quoted-printable')
 
+    mail_instead = getattr(config, 'MAIL_INSTEAD', None)
+    if mail_instead is None:
+        send_to = user.email
+    else:
+        send_to = mail_instead
+
     sm = subprocess.Popen(
         [
             '/usr/sbin/sendmail',
             '-oi',
             '-f',
-            config.MAIL_FROM,
-            user.email,
+            mail_from,
+            send_to,
         ],
         stdin=subprocess.PIPE,
     )
     sm.communicate(msg.as_bytes())
 
     if sm.returncode != 0:
-        raise RuntimeError('Sendmail failed with return code {}'.format(sm.returncode))
+        logging.error('Mail: Sendmail failed with return code {}'.format(sm.returncode))
+        return False
+
+    return True
+
+
+def password_reset_url(token: str) -> str:
+    return config.WEB_ROOT + 'auth/reset?' + urllib.parse.urlencode({'token': token}, safe=':')
+
+
+def send_new_account_email(user: db.User, token: str) -> bool:
+    return send_user_email(user, 'Založen nový účet', textwrap.dedent('''\
+        Vítejte!
+
+        Právě Vám byl založen účet v Odevzávacím systému Matematické olympiády.
+        Nastavte si prosím heslo na následující stránce:
+
+                {}
+
+        Váš OSMO
+    '''.format(password_reset_url(token))))
+
+
+def send_password_reset_email(user: db.User, token: str) -> bool:
+    return send_user_email(user, 'Obnova hesla', textwrap.dedent('''\
+        Někdo (pravděpodobně Vy) požádal o obnovení hesla k Vašemu účtu v Odevzávacím
+        systému Matematické olympiády. Heslo si můžete nastavit, případně požadavek
+        zrušit, na následující stránce:
+
+                {}
+
+        Váš OSMO
+    '''.format(password_reset_url(token))))
 
 
 def die(msg: str) -> NoReturn:
diff --git a/mo/web/main.py b/mo/web/main.py
index f90e1df6b1c58c7bd22e36e51887f1972527cbfe..a82aea9f7e275fb2d5deaf00fb97a62634253051 100644
--- a/mo/web/main.py
+++ b/mo/web/main.py
@@ -35,7 +35,7 @@ def login():
         app.logger.error('Login: Neznámý uživatel <%s>', email)
         error = 'Neznámý uživatel.'
     elif form.reset.data:
-        app.logger.info('Login: Požadavek na změnu hesla pro <%s>', email)
+        app.logger.info('Login: Požadavek na reset hesla pro <%s>', email)
 
         min_time_between_resets = datetime.timedelta(minutes=1)
         now = datetime.datetime.now().astimezone()
@@ -44,14 +44,9 @@ def login():
             error = 'Poslední požadavek na obnovení hesla byl odeslán příliš nedávno.'
         else:
             token = mo.users.ask_reset_password(user)
-            link = url_for('reset', token=token, _external=True)
             db.get_session().commit()
 
-            try:
-                mo.util.send_password_reset_email(user, link)
-            except RuntimeError as e:
-                app.logger.error('Login: problém při posílání emailu: {}'.format(e))
-
+            mo.util.send_password_reset_email(user, token)
             return render_template('reset.html')
 
     elif not form.passwd.data or not mo.users.check_password(user, form.passwd.data):
@@ -116,6 +111,7 @@ def reset():
 
     if form.cancel.data:
         mo.users.cancel_reset_password(user)
+        app.logger.info('Login: Zrušen reset hesla pro uživatele <%s>', user.email)
         db.get_session().commit()
         return render_template('reset.html', okay='Obnova hesla zrušena.')
     elif len(form.passwd.data) < 8:
@@ -127,6 +123,12 @@ def reset():
     else:
         mo.users.do_reset_password(user)
         mo.users.set_password(user, form.passwd.data)
+        app.logger.info('Login: Reset hesla pro uživatele <%s>', user.email)
+        mo.util.log(
+            type=db.LogType.user,
+            what=user.user_id,
+            details={'action': 'reset-passwd'},
+        )
         db.get_session().commit()
         return redirect(url_for('reset', done=1))
 
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 536eb295e193a2de44eeb6cce606297f7f55be26..09b301f4463f4a56307ef6a8db33d4691b8a2f77 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -378,14 +378,11 @@ def org_user_new():
 
             # Send password (re)set link
             token = mo.users.ask_reset_password(new_user)
-            link = url_for('reset', token=token, _external=True)
             db.get_session().commit()
 
-            try:
-                mo.util.send_password_reset_email(new_user, link)
+            if mo.util.send_new_account_email(new_user, token):
                 flash('E-mail s odkazem pro nastavení hesla odeslán na {}'.format(new_user.email), 'success')
-            except RuntimeError as e:
-                app.logger.error('Login: problém při posílání e-mailu: {}'.format(e))
+            else:
                 flash('Problém při odesílání e-mailu s odkazem pro nastavení hesla', 'danger')
 
             if is_org: