diff --git a/bin/create-user b/bin/create-user
index c6f86c6b7a46e8926b8cb233143170689e8e4195..7a8851b4a7b4d5eba08eb895387b72adddea61fb 100755
--- a/bin/create-user
+++ b/bin/create-user
@@ -4,6 +4,7 @@ import argparse
 import sys
 
 import mo.db as db
+import mo.email
 import mo.users
 import mo.util
 
@@ -49,4 +50,4 @@ if args.passwd is not None:
 session.commit()
 
 if args.passwd is None:
-    mo.util.send_new_account_email(user, token)
+    mo.email.send_new_account_email(user, token)
diff --git a/bin/reset-user b/bin/reset-user
index 88bb695387b4719eb526d01f81155af4f855a4a7..bea8fe06f8d75ac09566f699700a399d7b85e740 100755
--- a/bin/reset-user
+++ b/bin/reset-user
@@ -3,6 +3,7 @@
 import argparse
 
 import mo.config
+import mo.email
 import mo.db as db
 import mo.users
 import mo.util
@@ -26,4 +27,4 @@ session.commit()
 if args.mail_instead:
     mo.config.MAIL_INSTEAD = args.mail_instead
 
-mo.util.send_new_account_email(user, token)
+mo.email.send_new_account_email(user, token)
diff --git a/mo/email.py b/mo/email.py
new file mode 100644
index 0000000000000000000000000000000000000000..597ce0044b60ff58f7e4f3c2e53f76f256362b83
--- /dev/null
+++ b/mo/email.py
@@ -0,0 +1,122 @@
+# Rozesílání e-mailových notifikací všeho druhu
+
+import datetime
+import email.message
+import email.headerregistry
+import subprocess
+import textwrap
+import urllib.parse
+
+import mo.db as db
+import mo.config as config
+from mo.util import logger
+
+
+def send_user_email(user: db.User, subject: str, body: str) -> bool:
+    logger.info(f'Mail: "{subject}" -> {user.email}')
+
+    mail_from = getattr(config, 'MAIL_FROM', None)
+    if mail_from is None:
+        logger.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=mail_from,
+    )
+    msg['To'] = [
+        email.headerregistry.Address(
+            display_name=user.full_name(),
+            addr_spec=user.email,
+        )
+    ]
+    msg['Reply-To'] = email.headerregistry.Address(
+        display_name='Správce OSMO',
+        addr_spec=config.MAIL_CONTACT,
+    )
+    msg['Subject'] = 'OSMO – ' + subject
+    msg['Date'] = datetime.datetime.now()
+
+    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',
+            mail_from,
+            send_to,
+        ],
+        stdin=subprocess.PIPE,
+    )
+    sm.communicate(msg.as_bytes())
+
+    if sm.returncode != 0:
+        logger.error('Mail: Sendmail failed with return code {}'.format(sm.returncode))
+        return False
+
+    return True
+
+
+def activate_url(token: str) -> str:
+    return config.WEB_ROOT + 'auth/activate?' + urllib.parse.urlencode({'token': token}, safe=':')
+
+
+def confirm_url(type: str, token: str) -> str:
+    return config.WEB_ROOT + f'auth/confirm/{type}?' + 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 Odevzdávacím systému Matematické olympiády.
+        Nastavte si prosím heslo na následující stránce:
+
+                {}
+
+        Váš OSMO
+    '''.format(activate_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 Odevzdá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(confirm_url('p', token))))
+
+
+def send_confirm_create_email(user: db.User, token: str) -> bool:
+    return send_user_email(user, 'Založení účtu', textwrap.dedent('''\
+        Někdo (pravděpodobně Vy) požádal o založení účtu s touto e-mailovou adresou
+        v Odevzdávacím systému Matematické olympiády. Pokud účet chcete založit,
+        následujte tento odkaz:
+
+                {}
+
+        Váš OSMO
+    '''.format(confirm_url('r', token))))
+
+
+def send_confirm_change_email(user: db.User, token: str) -> bool:
+    return send_user_email(user, 'Změna e-mailové adresy', textwrap.dedent('''\
+        Někdo (pravděpodobně Vy) požádal o nastavení e-mailové adresy k účtu
+        v Odevzdávacím systému Matematické olympiády na tuto adresu.
+        Pokud změnu chcete provést, následujte tento odkaz:
+
+                {}
+
+        Váš OSMO
+    '''.format(confirm_url('e', token))))
diff --git a/mo/imports.py b/mo/imports.py
index 3645ea65502bbec806413bc7606b4c99e128c10c..bc09f3b0b99c64b3720788bba4e5a3f3dace7654 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -10,6 +10,7 @@ from typing import List, Optional, Any, Dict, Type, Union
 import mo.csv
 from mo.csv import FileFormat, MissingHeaderError
 import mo.db as db
+import mo.email
 import mo.rights
 import mo.users
 import mo.util
@@ -374,7 +375,7 @@ class Import:
             if u and not u.password_hash and not u.reset_at:
                 token = mo.users.make_activation_token(u)
                 sess.commit()
-                mo.util.send_new_account_email(u, token)
+                mo.email.send_new_account_email(u, token)
             else:
                 sess.rollback()
 
diff --git a/mo/util.py b/mo/util.py
index d22fadb49285b81bbf6da2e32481a9a06f7b52bc..56223feec89de5e9f8660f67b658dc0be0d5469a 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -4,18 +4,13 @@ from dataclasses import dataclass
 import datetime
 import decimal
 import dateutil.tz
-import email.message
-import email.headerregistry
 import locale
 import logging
 import os
 import re
 import secrets
-import subprocess
 import sys
 from typing import Any, Optional, NoReturn, Tuple, List
-import textwrap
-import urllib.parse
 
 import mo
 import mo.db as db
@@ -60,124 +55,6 @@ def log(type: db.LogType, what: int, details: Any):
     db.get_session().add(entry)
 
 
-def send_user_email(user: db.User, subject: str, body: str) -> bool:
-    logger.info(f'Mail: "{subject}" -> {user.email}')
-
-    mail_from = getattr(config, 'MAIL_FROM', None)
-    if mail_from is None:
-        logger.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=mail_from,
-    )
-    msg['To'] = [
-        email.headerregistry.Address(
-            display_name=user.full_name(),
-            addr_spec=user.email,
-        )
-    ]
-    msg['Reply-To'] = email.headerregistry.Address(
-        display_name='Správce OSMO',
-        addr_spec=config.MAIL_CONTACT,
-    )
-    msg['Subject'] = 'OSMO – ' + subject
-    msg['Date'] = datetime.datetime.now()
-
-    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',
-            mail_from,
-            send_to,
-        ],
-        stdin=subprocess.PIPE,
-    )
-    sm.communicate(msg.as_bytes())
-
-    if sm.returncode != 0:
-        logger.error('Mail: Sendmail failed with return code {}'.format(sm.returncode))
-        return False
-
-    return True
-
-
-def activate_url(token: str) -> str:
-    return config.WEB_ROOT + 'auth/activate?' + urllib.parse.urlencode({'token': token}, safe=':')
-
-
-def password_reset_url(token: str) -> str:
-    return config.WEB_ROOT + 'auth/confirm/p?' + urllib.parse.urlencode({'token': token}, safe=':')
-
-
-def confirm_create_url(token: str) -> str:
-    return config.WEB_ROOT + 'auth/confirm/r?' + urllib.parse.urlencode({'token': token}, safe=':')
-
-
-def confirm_email_url(token: str) -> str:
-    return config.WEB_ROOT + 'auth/confirm/e?' + 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 Odevzdávacím systému Matematické olympiády.
-        Nastavte si prosím heslo na následující stránce:
-
-                {}
-
-        Váš OSMO
-    '''.format(activate_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 Odevzdá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 send_confirm_create_email(user: db.User, token: str) -> bool:
-    return send_user_email(user, 'Založení účtu', textwrap.dedent('''\
-        Někdo (pravděpodobně Vy) požádal o založení účtu s touto e-mailovou adresou
-        v Odevzdávacím systému Matematické olympiády. Pokud účet chcete založit,
-        následujte tento odkaz:
-
-                {}
-
-        Váš OSMO
-    '''.format(confirm_create_url(token))))
-
-
-def send_confirm_change_email(user: db.User, token: str) -> bool:
-    return send_user_email(user, 'Změna e-mailové adresy', textwrap.dedent('''\
-        Někdo (pravděpodobně Vy) požádal o nastavení e-mailové adresy k účtu
-        v Odevzdávacím systému Matematické olympiády na tuto adresu.
-        Pokud změnu chcete provést, následujte tento odkaz:
-
-                {}
-
-        Váš OSMO
-    '''.format(confirm_email_url(token))))
-
-
 def die(msg: str) -> NoReturn:
     print(msg, file=sys.stderr)
     sys.exit(1)
diff --git a/mo/web/auth.py b/mo/web/auth.py
index 7b547e59fb0858b653717e45f94e69167d5768f4..e7b79448100f0442325497899849ca6d0278c167 100644
--- a/mo/web/auth.py
+++ b/mo/web/auth.py
@@ -65,7 +65,7 @@ def login():
         rr = mo.users.request_reset_password(user, request.remote_addr)
         if rr:
             db.get_session().commit()
-            mo.util.send_password_reset_email(user, rr.email_token)
+            mo.email.send_password_reset_email(user, rr.email_token)
             flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla.', 'success')
         else:
             flash('Příliš časté požadavky na obnovu hesla.', 'danger')
@@ -169,7 +169,7 @@ def user_settings_change():
                 sess.commit()
                 app.logger.info(f'Settings: Požadavek na změnu e-mailu uživatele #{user.user_id}')
                 flash('Odeslán e-mail s odkazem na potvrzení nové adresy.', 'success')
-                mo.util.send_confirm_change_email(user, rr.email_token)
+                mo.email.send_confirm_change_email(user, rr.email_token)
             else:
                 app.logger.info('Settings: Rate limit')
                 flash('Příliš mnoho požadavků na změny e-mailu. Počkejte prosím chvíli a zkuste to znovu.', 'danger')
@@ -194,6 +194,7 @@ class ResetForm(FlaskForm):
     submit = wtforms.SubmitField('Nastavit heslo')
 
 
+# URL je explicitně uvedeno v mo.email.activate_url
 @app.route('/auth/activate', methods=('GET', 'POST'))
 def activate():
     token = request.args.get('token')
@@ -366,7 +367,7 @@ def create_acct():
         app.logger.debug(f'Reg1: E-mailový token {reg1.email_token}')
         flash('Odeslán e-mail s odkazem na založení účtu.', 'success')
         user = db.User(email=form.email.data, first_name='Nový', last_name='Uživatel')
-        mo.util.send_confirm_create_email(user, reg1.email_token)
+        mo.email.send_confirm_create_email(user, reg1.email_token)
         return redirect(url_for('confirm_reg'))
 
     form.captcha.description = reg1.captcha_task()
@@ -541,6 +542,7 @@ class Reg2Form(FlaskForm):
     submit = wtforms.SubmitField('Vytvořit účet')
 
 
+# URL je explicitně uvedeno v mo.email.activate_url
 @app.route('/auth/confirm/r', methods=('GET', 'POST'))
 def confirm_reg():
     token = request.args.get('token')
@@ -572,6 +574,7 @@ class ConfirmEmailForm(FlaskForm):
     cancel = wtforms.SubmitField('Zrušit požadavek')
 
 
+# URL je explicitně uvedeno v mo.email.activate_url
 @app.route('/auth/confirm/e', methods=('GET', 'POST'))
 def confirm_email():
     reg2 = Reg2(request.args.get('token'), db.RegReqType.change)
@@ -599,6 +602,7 @@ class CancelResetForm(FlaskForm):
     cancel = wtforms.SubmitField('Zrušit obnovu hesla')
 
 
+# URL je explicitně uvedeno v mo.email.activate_url
 @app.route('/auth/confirm/p', methods=('GET', 'POST'))
 def confirm_reset():
     reg2 = Reg2(request.args.get('token'), db.RegReqType.reset)
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 981d8c0b4b424259542a7b473bc03e5b7e192fa2..2d00c0106e5da3f59976b7947c0c97e9f985ccd7 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -14,6 +14,7 @@ from wtforms.validators import Required
 
 import mo
 import mo.db as db
+import mo.email
 from mo.rights import Right
 import mo.util
 import mo.users
@@ -319,7 +320,7 @@ class ResendInviteForm(FlaskForm):
         if user.last_login_at is None:
             token = mo.users.make_activation_token(user)
             db.get_session().commit()
-            if mo.util.send_new_account_email(user, token):
+            if mo.email.send_new_account_email(user, token):
                 flash('Uvítací e-mail s odkazem na aktivaci účtu odeslán na {}.'.format(user.email), 'success')
             else:
                 flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')
@@ -572,7 +573,7 @@ def org_user_new():
             sess.commit()
             flash('Nový uživatel vytvořen', 'success')
 
-            if mo.util.send_new_account_email(new_user, token):
+            if mo.email.send_new_account_email(new_user, token):
                 flash('E-mail s odkazem na aktivaci účtu odeslán na {}.'.format(new_user.email), 'success')
             else:
                 flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')