Esempio n. 1
0
    def send_notification_target_reports(self):
        # First key is e-mail address of recipient, second is UpdateRequestStatus.SAVED
        # or UpdateRequestStatus.ERROR_AUTH
        reports_per_recipient: Dict[str, Dict[UpdateRequestStatus, OrderedSet]] = defaultdict(dict)
        sources: OrderedSet[str] = OrderedSet()

        for result in self.results:
            for target in result.notification_targets():
                if result.status in [UpdateRequestStatus.SAVED, UpdateRequestStatus.ERROR_AUTH]:
                    if result.status not in reports_per_recipient[target]:
                        reports_per_recipient[target][result.status] = OrderedSet()
                    reports_per_recipient[target][result.status].add(result.notification_target_report())
                    sources.add(result.rpsl_obj_new.source())

        sources_str = '/'.join(sources)
        subject = f'Notification of {sources_str} database changes'
        header = textwrap.dedent(f"""
            This is to notify you of changes in the {sources_str} database
            or object authorisation failures.
            
            You may receive this message because you are listed in
            the notify attribute on the changed object(s), or because
            you are listed in the mnt-nfy or upd-to attribute on a maintainer
            of the object(s).
            
            This message is auto-generated.
            The request was made by email, with the following details:
        """)
        header_saved = textwrap.dedent("""
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            Some objects in which you are referenced have been created,
            deleted or changed.
            
        """)

        header_failed = textwrap.dedent("""
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            Some objects in which you are referenced were requested
            to be created, deleted or changed, but *failed* the 
            proper authorisation for any of the referenced maintainers.
            
        """)

        for recipient, reports_per_status in reports_per_recipient.items():
            user_report = header + self._request_meta_str()
            if UpdateRequestStatus.ERROR_AUTH in reports_per_status:
                user_report += header_failed
                for report in reports_per_status[UpdateRequestStatus.ERROR_AUTH]:
                    user_report += f"---\n{report}\n"
                user_report += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n'
            if UpdateRequestStatus.SAVED in reports_per_status:
                user_report += header_saved
                for report in reports_per_status[UpdateRequestStatus.SAVED]:
                    user_report += f"---\n{report}\n"
                user_report += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n'

            email.send_email(recipient, subject, user_report)
Esempio n. 2
0
    def send_notification_target_reports(self):
        # First key is e-mail address of recipient, second is UpdateRequestStatus.SAVED
        # or UpdateRequestStatus.ERROR_AUTH
        reports_per_recipient: Dict[str, Dict[UpdateRequestStatus,
                                              OrderedSet]] = defaultdict(dict)
        sources: OrderedSet[str] = OrderedSet()

        for result in self.results:
            for target in result.notification_targets():
                if result.status in [
                        UpdateRequestStatus.SAVED,
                        UpdateRequestStatus.ERROR_AUTH
                ]:
                    if result.status not in reports_per_recipient[target]:
                        reports_per_recipient[target][
                            result.status] = OrderedSet()
                    reports_per_recipient[target][result.status].add(
                        result.notification_target_report())
                    sources.add(result.rpsl_obj_new.source())

        sources_str = '/'.join(sources)
        subject = f'Notification of {sources_str} database changes'
        header = get_setting('email.notification_header',
                             '').format(sources_str=sources_str)
        header += '\nThis message is auto-generated.\n'
        header += 'The request was made with the following details:\n'
        header_saved = textwrap.dedent("""
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            Some objects in which you are referenced have been created,
            deleted or changed.
            
        """)

        header_failed = textwrap.dedent("""
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            Some objects in which you are referenced were requested
            to be created, deleted or changed, but *failed* the 
            proper authorisation for any of the referenced maintainers.
            
        """)

        for recipient, reports_per_status in reports_per_recipient.items():
            user_report = header + self._request_meta_str()
            if UpdateRequestStatus.ERROR_AUTH in reports_per_status:
                user_report += header_failed
                for report in reports_per_status[
                        UpdateRequestStatus.ERROR_AUTH]:
                    user_report += f'---\n{report}\n'
                user_report += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n'
            if UpdateRequestStatus.SAVED in reports_per_status:
                user_report += header_saved
                for report in reports_per_status[UpdateRequestStatus.SAVED]:
                    user_report += f'---\n{report}\n'
                user_report += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n'

            email.send_email(recipient, subject, user_report)
Esempio n. 3
0
def handle_email_submission(email_txt: str) -> Optional[ChangeSubmissionHandler]:
    handler = None
    try:
        msg = email.EmailParser(email_txt)
        request_meta = {
            'Message-ID': msg.message_id,
            'From': msg.message_from,
            'Date': msg.message_date,
            'Subject': msg.message_subject,
        }
    except Exception as exc:
        logger.critical(f'An exception occurred while attempting to parse the following update e-mail: {email_txt}\n'
                        f'--- traceback for {exc} follows:', exc_info=exc)
        return None

    if not msg.message_from:
        logger.critical(f'No from address was found while attempting to parse the following update e-mail - '
                        f'update not processed: {email_txt}\n')
        return None

    try:
        if not msg.body:
            logger.warning(f'Unable to extract message body from e-mail {msg.message_id} from {msg.message_from}')
            subject = f'FAILED: {msg.message_subject}'
            reply_content = textwrap.dedent(f"""
            Unfortunately, your message with ID {msg.message_id}
            could not be processed, as no text/plain part could be found.
            
            Please try to resend your message as plain text email.
            """)
        else:
            handler = ChangeSubmissionHandler(msg.body, msg.pgp_fingerprint, request_meta)
            logger.info(f'Processed e-mail {msg.message_id} from {msg.message_from}: {handler.status()}')
            logger.debug(f'Report for e-mail {msg.message_id} from {msg.message_from}: {handler.submitter_report()}')

            subject = f'{handler.status()}: {msg.message_subject}'
            reply_content = handler.submitter_report()

    except Exception as exc:
        logger.critical(f'An exception occurred while attempting to process the following update: {email_txt}\n'
                        f'--- traceback for {exc} follows:', exc_info=exc)
        subject = f'ERROR: {msg.message_subject}'
        reply_content = textwrap.dedent(f"""
        Unfortunately, your message with ID {msg.message_id}
        could not be processed, due to an internal error.
        """)

    try:
        email.send_email(msg.message_from, subject, reply_content)
        if handler:
            handler.send_notification_target_reports()
    except Exception as exc:
        logger.critical(f'An exception occurred while attempting to send a reply to an update: '
                        f'{subject}\n{reply_content}\n --- traceback for {exc} follows:', exc_info=exc)

    return handler
Esempio n. 4
0
def notify_rpki_invalid_owners(
        database_handler: DatabaseHandler,
        rpsl_dicts_now_invalid: List[Dict[str, str]]) -> int:
    """
    Notify the owners/contacts of newly RPKI invalid objects.

    Expects a list of objects, each a dict with their properties.
    Contacts are resolved as any mnt-nfy, or any email address on any
    tech-c or admin-c, of any maintainer of the object.
    One email is sent per email address.
    """
    if not get_setting('rpki.notify_invalid_enabled'):
        return 0

    rpsl_objs = []
    for obj in rpsl_dicts_now_invalid:
        source = obj['source']
        authoritative = get_setting(f'sources.{source}.authoritative')
        if authoritative and obj['rpki_status'] == RPKIStatus.invalid:
            rpsl_objs.append(rpsl_object_from_text(obj['object_text']))

    if not rpsl_objs:
        return 0

    sources = set([obj.parsed_data['source'] for obj in rpsl_objs])
    mntner_emails_by_source = {}
    for source in sources:
        # For each source, a multi-step process is run to fill this
        # dict with the contact emails for each mntner.
        mntner_emails = defaultdict(set)

        # Step 1: retrieve all relevant maintainers from the DB
        mntner_pks = set(
            itertools.chain(*[
                obj.parsed_data.get('mnt-by', []) for obj in rpsl_objs
                if obj.parsed_data['source'] == source
            ]))
        query = RPSLDatabaseQuery(['rpsl_pk', 'parsed_data']).sources(
            [source]).rpsl_pks(mntner_pks).object_classes(['mntner'])
        mntners = list(database_handler.execute_query(query))

        # Step 2: any mnt-nfy on these maintainers is a contact address
        for mntner in mntners:
            mntner_emails[mntner['rpsl_pk']].update(mntner['parsed_data'].get(
                'mnt-nfy', []))

        # Step 3: extract the contact handles for each maintainer
        mntner_contacts = {
            m['rpsl_pk']: m['parsed_data'].get('tech-c', []) +
            m['parsed_data'].get('admin-c', [])
            for m in mntners
        }

        # Step 4: retrieve all these contacts from the DB in bulk,
        # and extract their e-mail addresses
        contact_pks = set(itertools.chain(*mntner_contacts.values()))
        query = RPSLDatabaseQuery(['rpsl_pk', 'parsed_data']).sources(
            [source]).rpsl_pks(contact_pks).object_classes(['role', 'person'])
        contacts = {
            r['rpsl_pk']: r['parsed_data'].get('e-mail', [])
            for r in database_handler.execute_query(query)
        }

        # Step 5: use the contacts per maintainer, and emails per contact
        # to create a flattened list of emails per maintainer
        for mntner_pk, mntner_contacts in mntner_contacts.items():
            for contact_pk in mntner_contacts:
                try:
                    mntner_emails[mntner_pk].update(contacts[contact_pk])
                except KeyError:
                    pass

        mntner_emails_by_source[source] = mntner_emails

    # With mntners_emails_by_source filled with per source, per maintainer,
    # all relevant emails, categorise the RPSL objects on which email
    # addresses they need to be sent to.
    objs_per_email: Dict[str, Set[RPSLObject]] = defaultdict(set)
    for rpsl_obj in rpsl_objs:
        mntners = rpsl_obj.parsed_data.get('mnt-by', [])
        source = rpsl_obj.parsed_data['source']
        for mntner_pk in mntners:
            try:
                for email in mntner_emails_by_source[source][mntner_pk]:
                    objs_per_email[email].add(rpsl_obj)
            except KeyError:  # pragma: no cover
                pass

    header_template = get_setting('rpki.notify_invalid_header', '')
    subject_template = get_setting('rpki.notify_invalid_subject',
                                   '').replace('\n', ' ')
    for email, objs in objs_per_email.items():
        sources_str = ', '.join(
            set([obj.parsed_data['source'] for obj in objs]))
        subject = subject_template.format(sources_str=sources_str,
                                          object_count=len(objs))
        body = header_template.format(sources_str=sources_str,
                                      object_count=len(objs))
        body += '\nThe following objects are affected:\n'
        body += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n'
        for rpsl_obj in objs:
            body += rpsl_obj.render_rpsl_text() + '\n'
        body += '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
        try:
            send_email(email, subject, body)
        except Exception as e:  # pragma: no cover
            logger.warning(
                f'Unable to send RPKI invalid notification to {email}: {e}')

    return len(objs_per_email.keys())