Skip to content
Snippets Groups Projects
Commit 7af0a4b9 authored by Martin Mareš's avatar Martin Mareš
Browse files

Reforma rozesílání mailů

- Máme obecnou funkci na odeslání mailu účastníkovi. V configu se
  dá nastavit, že všechny maily se přesměrovávají na zadanou adresu.

- Všechny maily logujeme.

- Posílání mailů negeneruje výjimky, nýbrž chyby zapisuje do logu.

- Rozlišujeme mail o založení účtu od mailu o resetu hesla. Je v nich
  stejný odkaz, ale liší se popis situace.

- Odkaz na reset hesla se vytváří v odesílacích funkcích. Proto
  z configu potřebujeme znát URL webu.
parent 7b9960ac
No related branches found
No related tags found
No related merge requests found
......@@ -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/'
......@@ -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:
......
......@@ -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))
......
......@@ -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:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment