1#!/usr/bin/env python3
  2
  3"""Event email functionality.
  4
  5Contains handler to test whether emails should be for each ingested
  6event.
  7
  8The is pre-Roles code from before the User/Role classes were added."""
  9
 10import logging
 11from datetime import datetime
 12
 13from chart.project import settings
 14from chart.db.connection import db_connect
 15from chart.common import sendmail
 16from chart.common.prettyprint import show_time_ms
 17from chart.common.prettyprint import show_time
 18from chart.common.util import nvl
 19from chart.common.xml import datetime_to_xml
 20from chart.common.xml import xml_to_datetime
 21from chart.common.util import ensure_dir_exists
 22
 23db_conn = db_connect()  # TBD: used to auth_user and users
 24
 25logger = logging.getLogger()
 26
 27
 28def get_notify_dir():
 29    """Return directory for notification statefiles,
 30    creating it if needed."""
 31
 32    res = settings.STATEFILE_DIR.joinpath('notifications')
 33    ensure_dir_exists(res)
 34    return res
 35
 36
 37def event_notify(event, sendmails=False, cutoff_duration=settings.EMAIL_CUTOFF):
 38    """Tell any subscribers about a new event.
 39    This is called on ingestion and sends an email to every user who is subscribed to this event.
 40    If the event is older than settings.EMAIL_CUTOFF don't send it, unless
 41    `force_sendmail` is set.
 42    """
 43    if cutoff_duration is None:
 44        cutoff_duration = settings.EMAIL_CUTOFF
 45
 46    if cutoff_duration is not None and (
 47            (not sendmails) or (event.start_time < (datetime.utcnow() - cutoff_duration))):
 48        return
 49
 50    # if the event is configured to use the EMAIL_NOTIFICATIONS table, block it
 51    # if there is already a notification present
 52    if event.event_class.email_notification:
 53        # build a filename for a notification file in the statefiles directory
 54        state_filename = get_notify_dir().joinpath('EVENT_{event}_{sid}'.format(
 55                event=event.event_classname,
 56                sid=event.sid.name))
 57
 58        if state_filename.exists():
 59            # state is already set
 60            logger.info('Not sending emails as a notification exists in {name}'.format(
 61                    name=state_filename))
 62            return
 63
 64        # write an empty state file
 65        with open(str(state_filename), 'w') as handle:
 66            handle.write(datetime_to_xml(event.start_time))
 67
 68        logger.info('Created an email event notification statefile {name}'.format(
 69                name=state_filename))
 70
 71    # logger.debug('Event emailer received {e}'.format(e=event))
 72    for first_name, last_name, email in db_conn.query(
 73        'SELECT first_name, last_name, email '
 74        'FROM auth_user usr, event_subscriptions sub '
 75        'WHERE usr.id=sub.django_user_id '
 76        'AND sub.event_classname=:event_classname '
 77            'AND {sid}'.format(sid=event.sid.sql_where(table_name='EVENT_SUBSCRIPTIONS',
 78                                                       match_on_none=True,
 79                                                       prefix='sub.')),
 80        event_classname=event.event_classname):
 81
 82        # if sim_sendmail:
 83            # logger.info('Would send email to {first} {last} <{email}>'.format(
 84                # first=first_name, last=last_name, email=email))
 85        email_event(first_name, last_name, email, event)
 86
 87
 88def email_event(first_name=None, last_name=None, email=None, event=None):
 89    """Send an event notification email to a single person."""
 90    if first_name is not None:
 91        name = '{first} {last}'.format(first=nvl(first_name, ''), last=nvl(last_name, ''))
 92        logger.debug('Emailing {name} {email}'.format(name=name, email=email))
 93        to_address = (name, email)
 94
 95    else:
 96        # documentation claims "to" address can either be email or (name, email)
 97        # but it requires a tuple
 98        to_address = (email, email)
 99
100    times = 'Start time: {time}'.format(time=show_time_ms(event.start_time))
101    if event.stop_time is not None:
102        times += '\nStop time: {time}'.format(time=show_time_ms(event.stop_time))
103
104    # if not None, this function will return a message body to replace the default one
105    email_function = event.event_class.email_function
106
107    if email_function is not None:
108        message = email_function(event)
109
110    else:
111        message = """{APPNAME} has raised an event of class {classname}
112
113Source: {sid}
114{times}
115
116{desc}
117
118This event was produced by the script {gen_method} at {gen_time} and the event id is {event_id}.
119Do not reply to this email.
120{suffix}""".format(
121    APPNAME=settings.APPNAME,
122    classname=event.event_classname,
123    sid=event.sid,
124    times=times,
125    desc=event.description('email').encode('utf-8'),
126    gen_method=event.gen_method,
127    gen_time=event.gen_time,
128    event_id=-1 if event.event_id is None else event.event_id,
129    suffix=settings.EMAIL_MESSAGE_SUFFIX)
130
131    sendmail.sendmail(
132        from_address=(settings.EMAIL_NAME.format('system'),
133                      settings.EMAIL_ADDRESS.format('system')),
134        to_addresses=(to_address,),
135        subject='{prefix}{classname} event generated for {sid}'.format(
136            prefix=settings.EMAIL_SUBJECT_PREFIX,
137            classname=event.event_classname,
138            sid=event.sid.name),
139        message=message)
140
141
142def event_denotify(event_classname,
143                   sid=None,
144                   stop_time=None,
145                   cutoff_duration=settings.EMAIL_CUTOFF):
146    """An event-raising algorithm can call this function to indicate a certain event
147    did not occur. This will result in an email notification to subscribers.
148    """
149    if stop_time < (datetime.utcnow() - cutoff_duration):
150        return
151
152    # build a filename for a notification file in the statefiles directory
153    state_filename = get_notify_dir().joinpath('EVENT_{event}_{sid}'.format(
154        event=event_classname,
155        sid=sid.name))
156
157    if state_filename.exists():
158        # state is already set
159        with open(str(state_filename), 'r') as handle:
160            start_time = xml_to_datetime(handle.read().strip())
161
162        logger.debug('Deleting existing event notification {name}'.format(
163                name=state_filename))
164        # there is an activate notification for this event/scid combination
165        state_filename.unlink()
166
167        for first_name, last_name, email in db_conn.query(
168            'SELECT first_name, last_name, email '
169            'FROM auth_user usr, event_subscriptions sub '
170            'WHERE usr.id=sub.django_user_id '
171            'AND sub.event_classname=:event_classname '
172            'AND (sub.scid is null or sub.scid=:scid)',
173            event_classname=event_classname,
174            scid=sid.sid.name):
175
176            logger.debug('Emailing {first} {last}'.format(first=first_name, last=last_name))
177
178            message = """The following condition has now ended:
179
180Event name: {classname}
181Source: {sid}
182Start time: {start}
183Stop time: {stop}
184
185Do not reply to this email.
186{suffix}""".format(
187    classname=event_classname,
188    sid=sid,
189    start=show_time(start_time) if start_time is not None else 'unknown',
190    stop=show_time(stop_time) if stop_time is not None else 'unknown',
191    suffix=settings.EMAIL_MESSAGE_SUFFIX)
192
193            sendmail.sendmail(from_address=(settings.EMAIL_NAME.format('system'),
194                                            settings.EMAIL_ADDRESS.format('system')),
195                              to_addresses=((first_name + ' ' + last_name, email),),
196                              subject='CHART ' + event_classname + ' denotification ' + str(sid),
197                              message=message)
198
199    logger.debug('All done')