From 38aae77d33ac4df187e4b9a425787b648077eb89 Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Sun, 26 Apr 2020 22:08:50 +0200 Subject: [PATCH] Improve error handling when sending emails In one case, we were not catching socket-related exceptions , and in the two other cases there was no error handling at all. Sending emails can easily fail if no email server is configured, so it is really necessary to handle these errors instead of crashing with a HTTP 500 error. Refactor email sending code and add proper error handling. Show alert messages that tell the user if an email was sent or if there was an error. When sending a password reminder email or inviting people by email, don't proceed to the next step in case of error, because sending emails is the whole point of these actions. --- ihatemoney/utils.py | 27 ++++++++++++++++++++++- ihatemoney/web.py | 53 ++++++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py index d8ee112b..f6fab922 100644 --- a/ihatemoney/utils.py +++ b/ihatemoney/utils.py @@ -4,11 +4,14 @@ import re import os import ast import operator +import smtplib +import socket from io import BytesIO, StringIO + import jinja2 from json import dumps, JSONEncoder -from flask import redirect, current_app, render_template +from flask import flash, redirect, current_app, render_template from flask_babel import get_locale from babel import Locale from werkzeug.routing import HTTPException, RoutingException @@ -33,6 +36,28 @@ def slugify(value): return re.sub(r'[-\s]+', '-', value) +def send_email(mail_message, flash_success="", flash_error=""): + """Send an email using Flask-Mail, returning False if there was an error. + + Optionally display a "flash alert" message to the user. The flash + message is different depending on whether we could successfully send + the email or not. + """ + # Since Python 3.4, SMTPException and socket.error are actually + # identical, but this was not the case before. Also, it is more clear + # to check for both. + try: + current_app.mail.send(mail_message) + except (smtplib.SMTPException, socket.error): + if flash_error: + flash(flash_error, category="danger") + return False + # Email was sent successfully + if flash_success: + flash(flash_success, category="success") + return True + + class Redirect303(HTTPException, RoutingException): """Raise if the map requests a redirect. This is for example the case if diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 45da8726..53dfc9aa 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -27,7 +27,7 @@ from ihatemoney.forms import ( AdminAuthenticationForm, AuthenticationForm, EditProjectForm, InviteForm, MemberForm, PasswordReminder, ResetPasswordForm, ProjectForm, get_billform_for ) -from ihatemoney.utils import Redirect303, list_of_dicts2json, list_of_dicts2csv, LoginThrottler, render_localized_template +from ihatemoney.utils import Redirect303, list_of_dicts2json, list_of_dicts2csv, LoginThrottler, render_localized_template, send_email main = Blueprint("main", __name__) @@ -247,10 +247,17 @@ def create_project(): msg = Message(message_title, body=message_body, recipients=[project.contact_email]) - try: - current_app.mail.send(msg) - except SMTPRecipientsRefused: - flash(_("Error while sending reminder email"), category="danger") + success = send_email(msg, _("A reminder email has just been sent to you")) + if not success: + # Display the error as a simple "info" alert, because it's + # not critical and doesn't prevent using the project. + flash( + _( + "We tried to send you an reminder email, but there was an error. " + "You can still use the project normally." + ), + category="info", + ) # redirect the user to the next step (invite) flash(_("The project identifier is %(project)s", project=project.id)) @@ -267,16 +274,21 @@ def remind_password(): # get the project project = Project.query.get(form.id.data) # send a link to reset the password - current_app.mail.send( - Message( - "password recovery", - body=render_localized_template( - "password_reminder", project=project - ), - recipients=[project.contact_email], - ) + remind_message = Message( + "password recovery", + body=render_localized_template("password_reminder", project=project), + recipients=[project.contact_email], ) - return redirect(url_for(".password_reminder_sent")) + success = send_email( + remind_message, + _("A password reminder email has just been sent to you"), + _( + "Sorry, there was an error while sending you an email with password reset instructions. " + "Please check the email configuration of the server or contact the administrator." + ), + ) + if success: + return redirect(url_for(".password_reminder_sent")) return render_template("password_reminder.html", form=form) @@ -408,9 +420,16 @@ def invite(): body=message_body, recipients=[email.strip() for email in form.emails.data.split(",")]) - current_app.mail.send(msg) - flash(_("Your invitations have been sent")) - return redirect(url_for(".list_bills")) + success = send_email( + msg, + _("Your invitations have been sent"), + _( + "Sorry, there was an error while trying to send the invitation emails. " + "Please check the email configuration of the server or contact the administrator." + ), + ) + if success: + return redirect(url_for(".list_bills")) return render_template("send_invites.html", form=form)