Source code for mentat.daemon.component.mailer

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# This file is part of Mentat system (https://mentat.cesnet.cz/).
#
# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
# Use of this source is governed by the MIT license, see LICENSE file.
#-------------------------------------------------------------------------------


"""
Daemon component providing configurable mailing services to daemons.

The implementation is based on :py:class:`pyzenkit.zendaemon.ZenDaemonComponent`.
"""


__author__ = "Jan Mach <jan.mach@cesnet.cz>"
__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"


import json
from subprocess             import Popen, PIPE
from email.mime.multipart   import MIMEMultipart
#from email.mime.application import MIMEApplication
from email.mime.text        import MIMEText
from jinja2                 import Environment, PackageLoader

#
# Custom libraries.
#
import pyzenkit.zendaemon


[docs]class MailerDaemonComponent(pyzenkit.zendaemon.ZenDaemonComponent): """ Implementation of ZenDaemonComponent for mailing services. """ EVENT_EMAIL_SEND_IDEA = 'email_send_idea' EVENT_LOG_STATISTICS = 'log_statistics' STATS_CNT_MAILED = 'cnt_mailed' STATS_CNT_ERRORS = 'cnt_errors' STATS_COUNTERS = 'counters' def __init__(self, **kwargs): """ Perform component initializations. """ super().__init__(**kwargs) # Unique component identifier self.cid = kwargs.get('cid', 'mailer') # Initialize Jinja2 template environment self.templ_env = Environment(loader=PackageLoader('mentat', 'templates_email')) # Permit changing of default event mapping self.event_map = kwargs.get('event_map', { self.EVENT_EMAIL_SEND_IDEA: self.EVENT_EMAIL_SEND_IDEA, self.EVENT_LOG_STATISTICS: self.EVENT_LOG_STATISTICS }) self.statistics_cur[self.STATS_COUNTERS] = {}
[docs] def get_events(self): """ Get the list of event names and their appropriate callback handlers. """ return [ { 'event': self.event_map[self.EVENT_EMAIL_SEND_IDEA], 'callback': self.cbk_event_email_send_idea, 'prepend': False }, { 'event': self.event_map[self.EVENT_LOG_STATISTICS], 'callback': self.cbk_event_log_statistics, 'prepend': False } ]
@staticmethod def _sendmail(email): """ Send email directly through local sendmail binary. """ with Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) as proc: proc.communicate(bytes(email.as_string(), 'UTF-8')) #---------------------------------------------------------------------------
[docs] def cbk_event_email_send_idea(self, daemon, args): """ Send email containing various number of idea messages. """ template_name = args.get('template', 'idea') template_txt = self.templ_env.get_template('{}.txt.j2'.format(template_name)) template_html = self.templ_env.get_template('{}.html.j2'.format(template_name)) # Create message container - the correct MIME type is multipart/alternative. msg = MIMEMultipart('mixed') msg['Subject'] = args['subject'] msg['From'] = args['from'] msg['To'] = args['to'] if 'reply_to' in args: msg['Reply-To'] = args['reply_to'] msg['X-Cesnet-Report-Type'] = args['report_type'] # Create the body of the message (a plain-text and an HTML version). text_plain = template_txt.render(**args) text_html = template_html.render(**args) # Record the MIME types of both parts - text/plain and text/html. msg_text = MIMEMultipart('alternative') msg_text_part1 = MIMEText(text_plain, 'plain') msg_text_part2 = MIMEText(text_html, 'html') # Attach parts into message container. # According to RFC 2046, the last part of a multipart message, in this case # the HTML message, is best and preferred. msg_text.attach(msg_text_part1) msg_text.attach(msg_text_part2) # Attach the text message msg.attach(msg_text) # Attach IDEA message #attach_idea = MIMEApplication(str(args['idea']), 'json') #attach_idea = MIMEApplication(json.dumps(args['idea'], default=args['idea'].json_default, sort_keys=True, indent=4), 'json') attach_idea = MIMEText(json.dumps(args['idea'], default=args['idea'].json_default, sort_keys=True, indent=4), 'plain') attach_idea.add_header('Content-Disposition', 'attachment', filename='{}.idea.json.txt'.format(args['id'])) msg.attach(attach_idea) daemon.logger.info("Reporting message '{}':'{}' via email to '{}'".format(args['id'], args['idea_id'], args['to'])) self.statistics_cur[self.STATS_COUNTERS][args['to']] = self.statistics_cur[self.STATS_COUNTERS].get(args['to'], 0) + 1 self.inc_statistic(self.STATS_CNT_MAILED) self._sendmail(msg) return (daemon.FLAG_CONTINUE, None)
[docs] def cbk_event_log_statistics(self, daemon, args): """ Periodical processing statistics logging. """ stats = self.get_statistics() stats_str = '' for k in [self.STATS_CNT_MAILED, self.STATS_CNT_ERRORS]: if k in stats: stats_str = self.pattern_stats.format(stats_str, k, stats[k]['cnt'], stats[k]['inc'], stats[k]['spd']) else: stats_str = self.pattern_stats.format(stats_str, k, 0, 0, 0) stats_str = "{}\n\t--- Destination stats ---".format(stats_str) for k in stats[self.STATS_COUNTERS]: stats_str = "{}\n\t{:40s} {:12,d} (+{:8,d}, {:8,.2f} #/s)".format(stats_str, k, stats[self.STATS_COUNTERS][k]['cnt'], stats[self.STATS_COUNTERS][k]['inc'], stats[self.STATS_COUNTERS][k]['spd']) daemon.logger.info("Component '{}': *** Processing statistics ***{}".format(self.cid, stats_str)) return (pyzenkit.zendaemon.ZenDaemon.FLAG_CONTINUE, args)