diff --git a/compendium_v2/__init__.py b/compendium_v2/__init__.py index 9d1b68d063ca54b1cce4e0b32e44e324a2cf2452..187920aca1d14c2aed83879055f3c13458588344 100644 --- a/compendium_v2/__init__.py +++ b/compendium_v2/__init__.py @@ -67,6 +67,17 @@ def _create_app_with_db(app_config) -> Flask: # for the publishers app.config['SQLALCHEMY_BINDS'] = app_config['SQLALCHEMY_BINDS'] + if 'mail' in app_config: + mail_config = app_config['mail'] + app.config['MAIL_ENABLE'] = True + app.config['MAIL_SERVER'] = mail_config['MAIL_SERVER'] + app.config['MAIL_PORT'] = mail_config['MAIL_PORT'] + app.config['MAIL_SENDER_EMAIL'] = mail_config['MAIL_SENDER_EMAIL'] # email address to send emails from + excluded_admins = mail_config.get('MAIL_EXCLUDED_ADMINS', []) + app.config['MAIL_EXCLUDED_ADMINS'] = excluded_admins # list of admin emails not to send emails to + else: + app.config['MAIL_ENABLE'] = False + db.init_app(app) return app diff --git a/compendium_v2/auth/session_management.py b/compendium_v2/auth/session_management.py index 37be02d952405c8005ddb21cedd9d7f63f8dba4b..4c713edee2e8ad21e6222c7a9226e117b0296519 100644 --- a/compendium_v2/auth/session_management.py +++ b/compendium_v2/auth/session_management.py @@ -5,6 +5,7 @@ from datetime import datetime from flask_login import LoginManager, current_user # type: ignore from compendium_v2.db import session_scope from compendium_v2.db.auth_model import User, ROLES +from compendium_v2.email import send_mail def admin_required(func): @@ -45,6 +46,7 @@ def create_user(email: str, fullname: str, oidc_sub: str): with session_scope() as session: user = User(email=email, fullname=fullname, oidc_sub=oidc_sub) session.add(user) + send_mail(f'{fullname} has just signed up with the email {email} and provider ID {oidc_sub}') return user diff --git a/compendium_v2/config.py b/compendium_v2/config.py index 490f399a55af8bff0a2d0b82e45ef6ae2fbbd6fa..4c67ad9842da0bd6b930f934d52898efb4377cc8 100644 --- a/compendium_v2/config.py +++ b/compendium_v2/config.py @@ -17,6 +17,20 @@ CONFIG_SCHEMA = { 'required': ['client_id', 'client_secret', 'server_metadata_url'], 'additionalProperties': False }, + 'mail': { + 'type': 'object', + 'properties': { + 'MAIL_SERVER': {'type': 'string'}, + 'MAIL_PORT': {'type': 'integer'}, + 'MAIL_SENDER_EMAIL': {'type': 'string', 'format': 'email'}, + 'MAIL_EXCLUDED_ADMINS': { + 'type': 'array', + 'items': {'type': 'string', 'format': 'email'} + } + }, + 'required': ['MAIL_SERVER', 'MAIL_PORT', 'MAIL_SENDER_EMAIL'], + 'additionalProperties': False + }, 'SECRET_KEY': {'type': 'string'}, }, 'required': ['SQLALCHEMY_DATABASE_URI', 'SURVEY_DATABASE_URI', 'SECRET_KEY'], diff --git a/compendium_v2/email/__init__.py b/compendium_v2/email/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..85008856d3ed82198cc90d888846ec746295bf89 --- /dev/null +++ b/compendium_v2/email/__init__.py @@ -0,0 +1,54 @@ +import smtplib +import threading +import logging +from typing import Sequence, Union +from sqlalchemy import select +from flask import current_app +from compendium_v2.db import db +from compendium_v2.db.auth_model import User, ROLES + +logger = logging.getLogger(__name__) + + +def _send_mail(smtp_server, port, sender_email, recipients, message): + + try: + with smtplib.SMTP(smtp_server, port) as server: + server.sendmail(from_addr=sender_email, to_addrs=recipients, msg=message) + logger.debug('Successfully sent email') + except Exception: + logger.exception('Unable to send email:') + + +def send_mail( + contents: str, + subject: str = 'New user signed up for Compendium', + recipients: Union[str, Sequence[str]] = '' +): + if not current_app.config['MAIL_ENABLE']: + logger.warning('No mail configuration, cannot send email.') + return + + if not contents or not isinstance(contents, str): + raise ValueError('Contents must be a non-empty string.') + + excluded_admins = set(email.lower() for email in current_app.config['MAIL_EXCLUDED_ADMINS']) + + admins = db.session.scalars(select(User).where(User.roles == ROLES.admin)).unique().all() + + admin_emails = [admin.email for admin in admins if admin.email.lower() not in excluded_admins] + + if not recipients: + recipients = admin_emails + + subject = subject.replace('\n', ' ') + message = f"""Subject: {subject}\n\n{contents}""" + + smtp_server = current_app.config['MAIL_SERVER'] + port = current_app.config['MAIL_PORT'] + sender_email = current_app.config['MAIL_SENDER_EMAIL'] + + # spin off a thread since this can take some time.. + logger.debug('Sending email') + thread = threading.Thread(target=_send_mail, args=(smtp_server, port, sender_email, recipients, message)) + thread.start() diff --git a/config-example.json b/config-example.json index a04ee059f8b528a5821205dc507765c0e6751aa8..f712e8b4cb8f8c88cbf809350526a41a330afdec 100644 --- a/config-example.json +++ b/config-example.json @@ -6,5 +6,13 @@ "client_secret": "<secret>", "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration" }, - "SECRET_KEY": "changeme" + "SECRET_KEY": "changeme", + "mail": { + "MAIL_SERVER": "mail.geant.net", + "MAIL_PORT": 25, + "MAIL_SENDER_EMAIL": "compendium@geant.org", + "MAIL_EXCLUDED_ADMINS": [ + "bjarke@nordu.net" + ] + } } diff --git a/test/conftest.py b/test/conftest.py index 001aee506856395d69d023cb33d57a2aef8bb204..154265bc3a3fedbd494443132232e80868177957 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -26,7 +26,7 @@ def dummy_config(): @pytest.fixture -def mocked_admin_user(app, mocker): +def mocked_admin_user(app, test_survey_data, mocker): with app.app_context(): user = User(email='testemail123@email.local', fullname='testfullname', oidc_sub='fakesub', roles=ROLES.admin) @@ -42,7 +42,7 @@ def mocked_admin_user(app, mocker): @pytest.fixture -def mocked_user(app, mocker): +def mocked_user(app, test_survey_data, mocker): with app.app_context(): user = User(email='testemail123@email.local', fullname='testfullname', oidc_sub='fakesub') diff --git a/test/test_send_mail.py b/test/test_send_mail.py new file mode 100644 index 0000000000000000000000000000000000000000..a1725367f4f4230b769f6338760c10c5d48c8d1f --- /dev/null +++ b/test/test_send_mail.py @@ -0,0 +1,16 @@ +from compendium_v2.email import send_mail + + +def test_email(app, mocked_admin_user, mocker): + + def _send_mail(*args, **kwargs): + pass + + mocker.patch('compendium_v2.email._send_mail', _send_mail) + with app.app_context(): + app.config['MAIL_ENABLE'] = True + app.config['MAIL_SERVER'] = 'localhost' + app.config['MAIL_PORT'] = 54655 + app.config['MAIL_SENDER_EMAIL'] = 'fakesender123@test.local' + app.config['MAIL_EXCLUDED_ADMINS'] = [] + send_mail('testmail321')