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

"""
A simple tool for generating notification emails to the OSG
"""
import os
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
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", default="all", dest="contact_type",
                         choices=["all", "administrative", "miscellaneous", "security", "submitter", "site"],
                         help="Filter on contact type e.g. administrative, miscellaneous, security, submitter or site"
                         "(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 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
    replaced = contents.replace('“','"').replace('”','"').replace("–","-").replace("—","-")
    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()

    recipients = set(args.recipients.split())
    if args.oim_recipients and 'vos' in args.oim_recipients:
        attempts = 3
        while attempts > 0:
            try:
                results = topology_utils.get_vo_contacts(args)
            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 = topology_utils.get_resource_contacts_by_fqdn(args)
                else:
                    results = topology_utils.get_resource_contacts(args)
            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()
