#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
A simple tool for generating notification emails to the OSG
"""
import os
import re
import sys
import getpass
import smtplib
import argparse
import email.message
import email.mime.text
import email.mime.multipart

import gnupg


if __name__ == "__main__" and __package__ is None:
    _parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.append(_parent + "/src")

import topology_utils
from topology_utils import get_topology_pool_manager
import net_name_addr_utils

# Parts of this implementation are from the following StackOverflow answer:
# https://stackoverflow.com/questions/10496902/pgp-signing-multipart-e-mails-with-python
# Licensed under CC-BY-SA


def messageFromSignature(signature):
    """
    Given a GnuPG signature, generate a corresponding MIME message.
    """
    message = email.message.Message()
    message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
    message['Content-Description'] = 'OpenPGP digital signature'
    message.set_payload(signature)
    return message


def generateFullMessage(subject, to, from_name, from_addr, message, sign=True, keyid=None):
    basemsg = email.mime.text.MIMEText(message)
    basetext = basemsg.as_string().replace("\n", "\r\n")

    to_addr = from_addr
    if from_name == 'Open Science Grid':
        to_name = from_name + ' Helpdesk'
    else:
        to_name = from_name

    if sign:
        gpg = gnupg.GPG()
        if not keyid:
            keys = gpg.list_keys(secret=True)
            if keys:
                name = "%s (key id %s)" % (keys[0]['uids'][0], keys[0]['keyid'])
                keyid = keys[0]['keyid']
            else:
                name = "(unknown)"
        else:
            name = str(keyid)
        passphrase = getpass.getpass("Please input passphrase for %s: " % name)
        signature = str(gpg.sign(basetext, passphrase=passphrase, detach=True, keyid=keyid))
        if not signature:
            raise RuntimeError("GnuPG signature of message failed")

        signmsg = messageFromSignature(signature)
        msg = email.mime.multipart.MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
                                                 protocol="application/pgp-signature")
        msg.attach(basemsg)
        msg.attach(signmsg)
        msg['Subject'] = subject
        msg['Bcc'] = ", ".join(to)
        msg['To'] = '"%s" <%s>' % (to_name, to_addr)
        msg['From'] = '"%s" <%s>' % (from_name, from_addr)
        return msg.as_string(unixfrom=True)
    else:
        basemsg['Subject'] = subject
        basemsg['Bcc'] = ", ".join(to)
        basemsg['To'] = '"%s" <%s>' % (to_name, to_addr)
        basemsg['From'] = '"%s" <%s>' % (from_name, from_addr)
        return basemsg.as_string(unixfrom=True)


def parseargs():
    oparser = argparse.ArgumentParser()
    oparser.add_argument("--host", dest="host", default="topology.opensciencegrid.org",
                         help="Remote topology host (default topology.opensciencegrid.org)")
    oparser.add_argument("--cert", dest="cert", help="Client certificate")
    oparser.add_argument("--key", dest="key", help="Client certificate private key")
    oparser.add_argument("--sign", dest="sign", default=True, action="store_true", help="Whether to sign with GPG")
    oparser.add_argument("--no-sign", dest="sign", action="store_false", help="Whether to sign with GPG")
    oparser.add_argument("--sign-id", dest="keyid", help="PGP signing key ID")
    oparser.add_argument("--type", dest="type", required=True, choices=["test", "production"],
                         help="Whether notification is test or production")
    oparser.add_argument("--recipients", dest="recipients", required=True,
                         help="Recipients of notification email")
    oparser.add_argument("--oim-recipients", dest="oim_recipients", action="append", choices=["resources", "vos"])
    oparser.add_argument("--message", dest="message", help="File containing message contents", required=True)
    oparser.add_argument("--subject", dest="subject", help="Contents of the subject line", required=True)
    oparser.add_argument("--from", dest="from_name", help="Human-friendly name for 'From' address",
                         choices=["default", "security"], default="default")
    oparser.add_argument("--dry-run", dest="dryrun", default=False, action="store_true",
                         help="Print out the email instead of sending it.")

    oparser.add_argument("--oim-name-filter", dest="name_filter", action="store", nargs="?",
                         help="Shell expression filter on the VO or resource name."
                         " Can't be specified along with --oim-fqdn-filter.")
    oparser.add_argument("--oim-fqdn-filter", dest="fqdn_filter", action="store", nargs="?",
                         help="Shell expression filter on the resource FQDN."
                         " Can't be specified along with --oim-name-filter.")
    oparser.add_argument("--oim-service-filter", dest="provides_service",
                         help="Filter on resources that provide given service(s)")
    oparser.add_argument("--oim-owner-filter", dest="owner_vo",
                         help="Filter on resources that list VO(s) as a partial owner")

    oparser.add_argument("--oim-contact-type", action="append", dest="contact_type",
                         choices=["all"] + topology_utils.CONTACT_TYPES,
                         help="Filter on contact type (default: all)")
    oparser.add_argument("--bypass-dns-check", action="store_true", dest="bypass_dns_check",
                         help="Bypass checking that one of the host's IP addresses matches with the hostanme resolution")
    oparser.add_argument("--allow-non-ascii", action="store_true", dest="allow_non_ascii",
                         help="Bypass the checking for non-ascii characters in the message")

    args = oparser.parse_args()

    if args.oim_recipients == 'vos' and args.owner_vo:
        oparser.error("--oim-owner-filter and --oim-recipients=vos options are conflicting")
    if args.name_filter and args.fqdn_filter:
        oparser.error("Can't specify both --oim-name-filter and --oim-fqdn-filter")

    if not args.contact_type or ("all" in args.contact_type):
        args.contact_type = ["all"]
    args.contact_type = set(args.contact_type)  # remove dupes

    if args.from_name == 'security':
        args.from_name = 'OSG Security Team'
        args.from_addr = 'security@osg-htc.org'
    else:
        args.from_name = 'OSG'
        args.from_addr = 'help@osg-htc.org'

    return args

def network_ok(bypass_dns_check):
    info = net_name_addr_utils.get_host_network_info()
    net_ok = net_name_addr_utils.hostnetinfo_good(info, bypass_dns_check)

    if net_ok:
        return True
    else:
        print("***")
        net_name_addr_utils.print_net_info(info)
        print("***")
        print("Refusing to send email without hostname/public DNS match.")
        print("For more info, see:")
        print("  https://osg-htc.org//operations/services/sending-announcements/")
        return False

def replace_smart_quotes_and_dashes(contents):
    # Replace smart quotes and em/en dashes
    regex_sub_map = [(r'[“”]', '"'),
                     (r'[‘’]', "'"),
                     (r'[—–]', '-')]
    replaced = contents
    for pattern, sub in regex_sub_map:
        replaced = re.sub(pattern, sub, replaced)
    return replaced

def has_non_printable_ascii_characters(contents):
    ret = False
    # For each character in the message
    for c in contents:
        # Chek if the numeric value of the character is outside the printable ascii range (32-126)
	# or is a tab(9) or and end of line(10)
        if ord(c) not in [9, 10] and (ord(c)< 32 or ord(c) > 126):
            ret = True
            break
    return ret

def main():
    args = parseargs()
    pm = get_topology_pool_manager(args)
    recipients = set(args.recipients.split())
    if args.oim_recipients and 'vos' in args.oim_recipients:
        attempts = 3
        while attempts > 0:
            try:
                results = pm.get_vo_contacts(args)
                break
            except topology_utils.InvalidPathError as exc:
                print(exc)
                exit(1)
            except topology_utils.IncorrectPasswordError as exc:
                attempts -= 1
                if attempts == 0:
                    print("Too many incorrect password attempts, exiting")
                    exit(1)
                else:
                    print(exc)
        results = topology_utils.filter_contacts(args, results)
        emails = set()
        for name in results.keys():
            for contact in results[name]:
                if 'Email' in contact:
                    emails.add(contact['Email'])
        recipients.update(emails)
    if args.oim_recipients and 'resources' in args.oim_recipients:
        attempts = 3
        while attempts > 0:
            try:
                if args.fqdn_filter:
                    results = pm.get_resource_contacts_by_fqdn(args)
                else:
                    results = pm.get_resource_contacts(args)
                break
            except topology_utils.InvalidPathError as exc:
                exit(str(exc))
            except topology_utils.IncorrectPasswordError as exc:
                attempts -= 1
                if attempts == 0:
                    exit("Too many incorrect password attempts, exiting")
                else:
                    print(exc)
        results = topology_utils.filter_contacts(args, results)
        emails = set()
        for name in results.keys():
            for contact in results[name]:
                if 'Email' in contact:
                    emails.add(contact['Email'])
        recipients.update(emails)

    with open(args.message, 'rb') as fp:
        contents = fp.read().decode('utf-8', errors='replace')

        # Replace smart quotes and em/en dashes
        contents = replace_smart_quotes_and_dashes(contents)

        # Check for non-ascii or non printable ascii characters
        if has_non_printable_ascii_characters(contents) and args.allow_non_ascii == False:
            print("ERROR: message contains non-ascii or non printable ascii characters.", file=sys.stderr)
            print("To force sending this message use the --allow-non-ascii option", file=sys.stderr)
            exit(1)

    if args.type != "production":
        if not args.dryrun and (len(recipients) > 5):
            raise Exception("Cowardly refusing to send a test email to more than 5 people")
        contents = """
===================================================
** This is a test of the osg-notify tool
** Please IGNORE the contents of this message
** If you received this message in error, please
** contact help@osg-htc.org
===================================================
""" + contents

    msg = generateFullMessage(subject=args.subject, to=recipients, from_name=args.from_name, from_addr=args.from_addr,
                              message=contents, sign=args.sign, keyid=args.keyid)

    if args.dryrun:
        print(msg)
    elif network_ok(args.bypass_dns_check):
        for _ in range(1, 4):
            try:
                verify_send = input("Really send mail to {0} recipients? (y/N)".format(len(recipients)))
            except EOFError:
                verify_send = 'n'

            if verify_send.lower() == 'y':
                session = smtplib.SMTP('localhost')
                session.sendmail(args.from_addr, recipients, msg)
                session.quit()
                break
            elif verify_send.lower() in ['n', '']:
                print("Not sending email...")
                print(msg)
                break
            else:
                print("Unrecognized answer: '{0}'".format(verify_send))


if __name__ == '__main__':
    main()
