From 9dd3570a52027ecb8e84dacb87d66fc511812c3c Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Fri, 6 Jul 2018 22:52:19 +0100 Subject: [PATCH] Add email on Flask error --- app/__init__.py | 4 ++ app/maillogger.py | 109 ++++++++++++++++++++++++++++++++++++++++++++ app/tasks/emails.py | 18 ++++++-- config.example.cfg | 1 + 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 app/maillogger.py diff --git a/app/__init__.py b/app/__init__.py index 8b7dd6a4..37c6cc8a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -37,5 +37,9 @@ csrf = CsrfProtect(app) mail = Mail(app) pages = FlatPages(app) +if not app.debug: + from .maillogger import register_mail_error_handler + register_mail_error_handler(app, mail) + from . import models, tasks from .views import * diff --git a/app/maillogger.py b/app/maillogger.py new file mode 100644 index 00000000..585cb295 --- /dev/null +++ b/app/maillogger.py @@ -0,0 +1,109 @@ +import logging +from enum import Enum +from app.tasks.emails import sendEmailRaw + +def _has_newline(line): + """Used by has_bad_header to check for \\r or \\n""" + if line and ("\r" in line or "\n" in line): + return True + return False + +def _is_bad_subject(subject): + """Copied from: flask_mail.py class Message def has_bad_headers""" + if _has_newline(subject): + for linenum, line in enumerate(subject.split("\r\n")): + if not line: + return True + if linenum > 0 and line[0] not in "\t ": + return True + if _has_newline(line): + return True + if len(line.strip()) == 0: + return True + return False + + +class FlaskMailSubjectFormatter(logging.Formatter): + def format(self, record): + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + s = self.formatMessage(record) + return s + +class FlaskMailTextFormatter(logging.Formatter): + pass + +# TODO: hier nog niet tevreden over (vooral logger.error(..., exc_info, stack_info)) +class FlaskMailHTMLFormatter(logging.Formatter): + pre_template = "

%s

%s
" + def formatException(self, exc_info): + formatted_exception = logging.Handler.formatException(self, exc_info) + return FlaskMailHTMLFormatter.pre_template % ("Exception information", formatted_exception) + def formatStack(self, stack_info): + return FlaskMailHTMLFormatter.pre_template % ("

Stack information

%s
", stack_info) + + +# see: https://github.com/python/cpython/blob/3.6/Lib/logging/__init__.py (class Handler) + +class FlaskMailHandler(logging.Handler): + def __init__(self, mailer, subject_template, level=logging.NOTSET): + logging.Handler.__init__(self, level) + self.mailer = mailer + self.send_to = mailer.app.config["MAIL_UTILS_ERROR_SEND_TO"] + self.subject_template = subject_template + self.html_formatter = None + + def setFormatter(self, text_fmt, html_fmt=None): + """ + Set the formatters for this handler. Provide at least one formatter. + When no text_fmt is provided, no text-part is created for the email body. + """ + assert (text_fmt, html_fmt) != (None, None), "At least one formatter should be provided" + if type(text_fmt)==str: + text_fmt = FlaskMailTextFormatter(text_fmt) + self.formatter = text_fmt + if type(html_fmt)==str: + html_fmt = FlaskMailHTMLFormatter(html_fmt) + self.html_formatter = html_fmt + + def getSubject(self, record): + fmt = FlaskMailSubjectFormatter(self.subject_template) + subject = fmt.format(record) + #Since templates can cause header problems, and we rather have a incomplete email then an error, we fix this + if _is_bad_subject(subject): + subject="FlaskMailHandler log-entry from %s [original subject is replaced, because it would result in a bad header]" % self.mailer.app.name + return subject + + def emit(self, record): + text = self.format(record) if self.formatter else None + html = self.html_formatter.format(record) if self.html_formatter else None + sendEmailRaw.delay(self.send_to, self.getSubject(record), text, html) + + +def register_mail_error_handler(app, mailer): + subject_template = "ContentDB crashed (%(module)s > %(funcName)s)" + text_template = """ +Message type: %(levelname)s +Location: %(pathname)s:%(lineno)d +Module: %(module)s +Function: %(funcName)s +Time: %(asctime)s +Message: +%(message)s""" + html_template = """ + + + + + + +
Message type:%(levelname)s
Location:%(pathname)s:%(lineno)d
Module:%(module)s
Function:%(funcName)s
Time:%(asctime)s
+

Message

+
%(message)s
""" + + import logging + mail_handler = FlaskMailHandler(mailer, subject_template) + mail_handler.setLevel(logging.ERROR) + mail_handler.setFormatter(text_template, html_template) + app.logger.addHandler(mail_handler) diff --git a/app/tasks/emails.py b/app/tasks/emails.py index fbefbc76..5af57698 100644 --- a/app/tasks/emails.py +++ b/app/tasks/emails.py @@ -22,7 +22,17 @@ from app.tasks import celery @celery.task() def sendVerifyEmail(newEmail, token): - msg = Message("Verify email address", recipients=[newEmail]) - msg.body = "This is a verification email!" - msg.html = render_template("emails/verify.html", token=token) - mail.send(msg) + msg = Message("Verify email address", recipients=[newEmail]) + msg.body = "This is a verification email!" + msg.html = render_template("emails/verify.html", token=token) + mail.send(msg) + +@celery.task() +def sendEmailRaw(to, subject, text, html): + from flask_mail import Message + msg = Message(subject, recipients=to) + if text: + msg.body = text + if html: + msg.html = html + mail.send(msg) diff --git a/config.example.cfg b/config.example.cfg index 41d685c2..ae78f29b 100644 --- a/config.example.cfg +++ b/config.example.cfg @@ -22,3 +22,4 @@ MAIL_DEFAULT_SENDER="" MAIL_SERVER="" MAIL_PORT=587 MAIL_USE_TLS=True +MAIL_UTILS_ERROR_SEND_TO=[""]