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.
This commit is contained in:
Baptiste Jonglez 2020-04-26 22:08:50 +02:00
parent 046c5757ff
commit 38aae77d33
2 changed files with 62 additions and 18 deletions

View file

@ -4,11 +4,14 @@ import re
import os import os
import ast import ast
import operator import operator
import smtplib
import socket
from io import BytesIO, StringIO from io import BytesIO, StringIO
import jinja2 import jinja2
from json import dumps, JSONEncoder 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 flask_babel import get_locale
from babel import Locale from babel import Locale
from werkzeug.routing import HTTPException, RoutingException from werkzeug.routing import HTTPException, RoutingException
@ -33,6 +36,28 @@ def slugify(value):
return re.sub(r'[-\s]+', '-', 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): class Redirect303(HTTPException, RoutingException):
"""Raise if the map requests a redirect. This is for example the case if """Raise if the map requests a redirect. This is for example the case if

View file

@ -27,7 +27,7 @@ from ihatemoney.forms import (
AdminAuthenticationForm, AuthenticationForm, EditProjectForm, AdminAuthenticationForm, AuthenticationForm, EditProjectForm,
InviteForm, MemberForm, PasswordReminder, ResetPasswordForm, ProjectForm, get_billform_for 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__) main = Blueprint("main", __name__)
@ -247,10 +247,17 @@ def create_project():
msg = Message(message_title, msg = Message(message_title,
body=message_body, body=message_body,
recipients=[project.contact_email]) recipients=[project.contact_email])
try: success = send_email(msg, _("A reminder email has just been sent to you"))
current_app.mail.send(msg) if not success:
except SMTPRecipientsRefused: # Display the error as a simple "info" alert, because it's
flash(_("Error while sending reminder email"), category="danger") # 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) # redirect the user to the next step (invite)
flash(_("The project identifier is %(project)s", project=project.id)) flash(_("The project identifier is %(project)s", project=project.id))
@ -267,16 +274,21 @@ def remind_password():
# get the project # get the project
project = Project.query.get(form.id.data) project = Project.query.get(form.id.data)
# send a link to reset the password # send a link to reset the password
current_app.mail.send( remind_message = Message(
Message( "password recovery",
"password recovery", body=render_localized_template("password_reminder", project=project),
body=render_localized_template( recipients=[project.contact_email],
"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) return render_template("password_reminder.html", form=form)
@ -408,9 +420,16 @@ def invite():
body=message_body, body=message_body,
recipients=[email.strip() recipients=[email.strip()
for email in form.emails.data.split(",")]) for email in form.emails.data.split(",")])
current_app.mail.send(msg) success = send_email(
flash(_("Your invitations have been sent")) msg,
return redirect(url_for(".list_bills")) _("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) return render_template("send_invites.html", form=form)