How can I send an email using python logging's SMTPHandler and SSL
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 appimport smtplibimport loggingfrom logging.handlers import RotatingFileHandler, SMTPHandler# Provide a class to allow SSL (Not TLS) connection for mail handlers by overloading the emit() methodclass 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 logsfile_handler = RotatingFileHandler('logs/app.log', maxBytes=1*1024*1024, backupCount=100)# Apply format to the log messagesformatter = 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 notif app.debug: file_handler.setLevel(logging.DEBUG)else: file_handler.setLevel(logging.WARN)# Create equivalent mail handlermail_handler = SSLSMTPHandler(mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), fromaddr=app.config['MAIL_FROM_EMAIL'], toaddrs='my@emailaddress.com', subject='Your app died. Sad times...', credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']))# Set the email formatmail_handler.setFormatter(logging.Formatter('''Message type: %(levelname)sLocation: %(pathname)s:%(lineno)dModule: %(module)sFunction: %(funcName)sTime: %(asctime)sMessage:%(message)s'''))# Only email errors, not warningsmail_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 loggingfrom .utils.logs import mail_handler, file_handlerloggers = [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 EmailMessageimport email.utilsclass 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!