Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New endpoint: Send MOTD as email #1283

Merged
merged 17 commits into from
Sep 19, 2022
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,7 @@ Please add a _short_ line describing the PR you make, if the PR implements a spe
- Change docker image to alpine ([#1272](https://github.com/ScilifelabDataCentre/dds_web/pull/1272))
- Added trivy when publishing to dockerhub ([#1276](https://github.com/ScilifelabDataCentre/dds_web/pull/1276))
- Bug fix: Cost value displayed by the --usage flag fixed ([#1274](https://github.com/ScilifelabDataCentre/dds_web/pull/1274))

## Sprint (2022-09-16 - 2022-09-30)

- New endpoint: SendMOTD - send important information to users ([#1283](https://github.com/ScilifelabDataCentre/dds_web/pull/1283))
i-oden marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions dds_web/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def output_json(data, code, headers=None):

api.add_resource(superadmin_only.AllUnits, "/unit/info/all", endpoint="all_units")
api.add_resource(superadmin_only.MOTD, "/motd", endpoint="motd")
api.add_resource(superadmin_only.SendMOTD, "/motd/send", endpoint="send_motd")
api.add_resource(superadmin_only.FindUser, "/user/find", endpoint="find_user")
api.add_resource(
superadmin_only.ResetTwoFactor, "/user/totp/deactivate", endpoint="reset_user_hotp"
Expand Down
63 changes: 62 additions & 1 deletion dds_web/api/superadmin_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
####################################################################################################

# Standard library
import os

# Installed
import flask_restful
import flask
import structlog
import flask_mail

# Own modules
from dds_web import auth, db
from dds_web import auth, db, mail
from dds_web.database import models
from dds_web.api.dds_decorators import json_required, logging_bind_request, handle_db_error
from dds_web import utils
import dds_web.errors as ddserr
from dds_web.api.user import AddUser


# initiate bound logger
Expand Down Expand Up @@ -136,6 +139,64 @@ def put(self):
return {"message": "The MOTD was successfully deactivated in the database."}


class SendMOTD(flask_restful.Resource):
"""Send a MOTD to all users in database."""

@auth.login_required(role=["Super Admin"])
@logging_bind_request
@json_required
@handle_db_error
def post(self):
"""Send MOTD as email to users."""
# Get MOTD ID
motd_id: int = flask.request.json.get("motd_id")
if not motd_id or not isinstance(motd_id, int): # The id starts at 1 - ok to not accept 0
raise ddserr.DDSArgumentError(
message="Please specify the ID of the MOTD you want to send."
)

# Get MOTD object
motd_obj: models.MOTD = models.MOTD.query.get(motd_id)
if not motd_obj or not motd_obj.active:
raise ddserr.DDSArgumentError(message=f"There is no active MOTD with ID '{motd_id}'.")

# Create email content
# put motd_obj.message etc in there etc
subject: str = "DDS Important Information"
body: str = flask.render_template(f"mail/motd.txt", motd=motd_obj.message)
html = flask.render_template(f"mail/motd.html", motd=motd_obj.message)

# Setup email connection
with mail.connect() as conn:
# Email users
for user in utils.page_query(db.session.query(models.User)):
primary_email = user.primary_email
if not primary_email:
flask.current_app.logger.warning(
f"No primary email found for user '{user.username}'."
)
continue
msg = flask_mail.Message(
subject=subject, recipients=[primary_email], body=body, html=html
)
msg.attach(
"scilifelab_logo.png",
"image/png",
open(
os.path.join(flask.current_app.static_folder, "img/scilifelab_logo.png"),
"rb",
).read(),
"inline",
headers=[
["Content-ID", "<Logo>"],
],
)
# Send email
utils.send_email_with_retry(msg=msg, obj=conn)

return {"message": f"MOTD '{motd_id}' has been sent to the users."}


class FindUser(flask_restful.Resource):
"""Get all users or check if there a specific user in the database."""

Expand Down
20 changes: 3 additions & 17 deletions dds_web/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,6 @@ def invite_user(email, new_user_role, project=None, unit=None):
"errors": projects_not_shared,
}

@staticmethod
def send_email_with_retry(msg, times_retried=0):
"""Send email with retry on exception"""

try:
mail.send(msg)
except smtplib.SMTPException as err:
# Wait a little bit
time.sleep(10)
# Retry twice
if times_retried < 2:
retry = times_retried + 1
AddUser.send_email_with_retry(msg, retry)

@staticmethod
@logging_bind_request
def add_to_project(whom, project, role, send_email=True):
Expand Down Expand Up @@ -493,7 +479,7 @@ def compose_and_send_email_to_user(userobj, mail_type, link=None, project=None):
deadline=deadline,
)

AddUser.send_email_with_retry(msg)
dds_web.utils.send_email_with_retry(msg)


class RetrieveUserInfo(flask_restful.Resource):
Expand Down Expand Up @@ -1040,7 +1026,7 @@ def post(self):
link=link,
)

AddUser.send_email_with_retry(msg)
dds_web.utils.send_email_with_retry(msg)
return {
"message": "Please check your email and follow the attached link to activate two-factor with authenticator app."
}
Expand Down Expand Up @@ -1106,7 +1092,7 @@ def post(self):
link=link,
)

AddUser.send_email_with_retry(msg)
dds_web.utils.send_email_with_retry(msg)
return {
"message": "Please check your email and follow the attached link to activate two-factor with email."
}
Expand Down
27 changes: 27 additions & 0 deletions dds_web/templates/mail/motd.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends 'mail/mail_base.html' %}
{% block body %}
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td>
<p style="text-align: center;">
<img src="cid:Logo" alt="Logo" border="0" style="border:0; outline:none; text-decoration:none; margin-bottom: 15px; max-width: 400px; max-height: 87px;">
</p>
</td>
</tr>
<tr>
<td style=" font-family: arial, sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: arial, sans-serif; font-size: 14px; font-weight: bold; margin: 0; margin-bottom: 5px;">
Important information to all DDS users:
</p>
</br>
<p style="font-family: arial, sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-top: 0px;">
{{ motd }}
</p>
</td>
</tr>
</table>
</br></br>
<p style="font-family: arial, sans-serif; font-size: 12px; font-weight: normal; margin: 0; Margin-top: 0px;">
This email cannot be replied to. If you need to get in contact with us regarding the DDS, please send an email to [email protected].
</p>
{% endblock %}
5 changes: 5 additions & 0 deletions dds_web/templates/mail/motd.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Important information to all DDS users:

{{ motd }}

This email cannot be replied to. If you need to get in contact with us regarding the DDS, please send an email to [email protected].
17 changes: 17 additions & 0 deletions dds_web/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import re
import typing
import urllib.parse
import time
import smtplib

# Installed
from contextlib import contextmanager
Expand Down Expand Up @@ -448,6 +450,21 @@ def page_query(q):
break


def send_email_with_retry(msg, times_retried=0, obj=None):
"""Send email with retry on exception"""
if obj is None:
obj = mail
try:
obj.send(msg)
except smtplib.SMTPException as err:
# Wait a little bit
time.sleep(10)
# Retry twice
if times_retried < 2:
retry = times_retried + 1
send_email_with_retry(msg, times_retried=retry, obj=obj)


def create_one_time_password_email(user, hotp_value):
"""Create HOTP email."""
msg = flask_mail.Message(
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class DDSEndpoint:
# Superadmins only
LIST_UNITS_ALL = BASE_ENDPOINT + "/unit/info/all"
MOTD = BASE_ENDPOINT + "/motd"
MOTD_SEND = BASE_ENDPOINT + "/motd/send"
USER_FIND = BASE_ENDPOINT + "/user/find"
TOTP_DEACTIVATE = BASE_ENDPOINT + "/user/totp/deactivate"

Expand Down
Loading