Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I send an email using python logging's SMTPHandler and SSL

I'm developing a flask app in which I'd like to send error level logging to an email address. I tried setting up the typical error handler:

mail_handler = SMTPHandler(mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
                           fromaddr=app.config['MAIL_FROM_EMAIL'],
                           toaddrs=['me@my_address.com'],
                           subject='The server died. That sucks... :(',
                           credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']))

Note the configuration values are as set up using flask-mail, with MAIL_USE_SSL=True and MAIL_PORT=465.

However, upon invoking an error (on purpose during testing) I get socket timeout errors - other than the port, I can't see how to tell the handler to use SSL. There is a secure=() parameter that can be passed (see the SMTPHandler docs) but that specifies us of TLS, not SSL.

Any clues how to do this? Thanks!

like image 630
thclark Avatar asked Apr 29 '16 11:04

thclark


People also ask

How do I send an email using Python 3?

To send the mail you use smtpObj to connect to the SMTP server on the local machine. Then use the sendmail method along with the message, the from address, and the destination address as parameters (even though the from and to addresses are within the e-mail itself, these are not always used to route the mail).


1 Answers

EDIT - see bottom of post for more up-to-date, code

Figured it out with thanks to Ned Deily pointing out that smtplib (which sits under SMTPHandler) requires special treatment. I also found this post demonstrating how to do that, by overloading the SMTPHandler (in that case to fix a TLS problem).

Using smtplib.SMTP_SSL (see smtplib docs), rather than the straightforward smtplib.SMTP, I was able to get the whole system working. This is the utils/logs.py file I use to set up the handlers (which should be a nice example of file, as well as email, handlers):

from your.application.file import app

import smtplib
import logging
from logging.handlers import RotatingFileHandler, SMTPHandler


# Provide a class to allow SSL (Not TLS) connection for mail handlers by overloading the emit() method
class SSLSMTPHandler(SMTPHandler):
    def emit(self, record):
        """
        Emit a record.
        """
        try:
            port = self.mailport
            if not port:
                port = smtplib.SMTP_PORT
            smtp = smtplib.SMTP_SSL(self.mailhost, port)
            msg = self.format(record)
            if self.username:
                smtp.login(self.username, self.password)
            smtp.sendmail(self.fromaddr, self.toaddrs, msg)
            smtp.quit()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)


# Create file handler for error/warning/info/debug logs
file_handler = RotatingFileHandler('logs/app.log', maxBytes=1*1024*1024, backupCount=100)

# Apply format to the log messages
formatter = logging.Formatter("[%(asctime)s] |  %(levelname)s | {%(pathname)s:%(lineno)d} | %(message)s")
file_handler.setFormatter(formatter)

# Set the level according to whether we're debugging or not
if app.debug:
    file_handler.setLevel(logging.DEBUG)
else:
    file_handler.setLevel(logging.WARN)

# Create equivalent mail handler
mail_handler = SSLSMTPHandler(mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
                           fromaddr=app.config['MAIL_FROM_EMAIL'],
                           toaddrs='[email protected]',
                           subject='Your app died. Sad times...',
                           credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']))

# Set the email format
mail_handler.setFormatter(logging.Formatter('''
Message type:       %(levelname)s
Location:           %(pathname)s:%(lineno)d
Module:             %(module)s
Function:           %(funcName)s
Time:               %(asctime)s

Message:

%(message)s
'''))

# Only email errors, not warnings
mail_handler.setLevel(logging.ERROR)

This is registered in my application file with:

# Register the handlers against all the loggers we have in play
# This is done after app configuration and SQLAlchemy initialisation, 
# drop the sqlalchemy if not using - I thought a full example would be helpful.
import logging
from .utils.logs import mail_handler, file_handler
loggers = [app.logger, logging.getLogger('sqlalchemy'), logging.getLogger('werkzeug')]
for logger in loggers:
    logger.addHandler(file_handler)
    # Note - I added a boolean configuration parameter, MAIL_ON_ERROR, 
    # to allow direct control over whether to email on errors. 
    # You may wish to use 'if not app.debug' instead.
    if app.config['MAIL_ON_ERROR']:
        logger.addHandler(mail_handler)

EDIT:

Commenter @EduGord has had trouble emitting the record correctly. Digging deeper, the base SMTPHandler class is sending messages differently than it was 3+ years ago.

This updated emit() method should get the message to format correctly:

from email.message import EmailMessage
import email.utils
class SSLSMTPHandler(SMTPHandler):

    def emit(self, record):
        """
        Emit a record.
        """
        try:
            port = self.mailport
            if not port:
                port = smtplib.SMTP_PORT
            smtp = smtplib.SMTP_SSL(self.mailhost, port)
            msg = EmailMessage()
            msg['From'] = self.fromaddr
            msg['To'] = ','.join(self.toaddrs)
            msg['Subject'] = self.getSubject(record)
            msg['Date'] = email.utils.localtime()
            msg.set_content(self.format(record))
            if self.username:
                smtp.login(self.username, self.password)
            smtp.send_message(msg, self.fromaddr, self.toaddrs)
            smtp.quit()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

Hope this helps somebody!

like image 61
thclark Avatar answered Sep 29 '22 04:09

thclark