#!/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)