From 7c563b3e28e9a6ff1f63d156a33996fecc1991c8 Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Mon, 26 Jul 2021 18:02:15 +0200 Subject: [PATCH] =?UTF-8?q?Odes=C3=ADl=C3=A1n=C3=AD=20e-mail=C5=AF=20p?= =?UTF-8?q?=C5=99esunuto=20do=20samostatn=C3=A9ho=20modulu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/create-user | 3 +- bin/reset-user | 3 +- mo/email.py | 122 +++++++++++++++++++++++++++++++++++++++++++ mo/imports.py | 3 +- mo/util.py | 123 -------------------------------------------- mo/web/auth.py | 10 ++-- mo/web/org_users.py | 5 +- 7 files changed, 138 insertions(+), 131 deletions(-) create mode 100644 mo/email.py diff --git a/bin/create-user b/bin/create-user index c6f86c6b..7a8851b4 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 88bb6953..bea8fe06 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 00000000..597ce004 --- /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 3645ea65..bc09f3b0 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 d22fadb4..56223fee 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 7b547e59..e7b79448 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 981d8c0b..2d00c010 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') -- GitLab