#!/usr/bin/env python
#
#                        Buildbot CVS Mail
#
# This script was derrived from syncmail,
# Copyright (c) 2002-2006 Barry Warsaw, Fred Drake, and contributors
#
# http://cvs-syncmail.cvs.sourceforge.net
#
# The script was re-written with the sole pupose of providing updates to
# Buildbot master by Andy Howell
#
# Options handling done right by djmitche


import optparse
import os
import re
import smtplib
import socket
import sys
import textwrap
import time
from email.utils import formataddr
from io import StringIO

"""
    -t
    --testing
      Construct message and send to stdout for testing

The rest of the command line arguments are:

    %%{sVv}
        CVS %%{sVv} loginfo expansion.  When invoked by CVS, this will be a single
        string containing the files that are changing.

"""
__version__ = '$Revision: 1.3 $'


try:
    import pwd
except ImportError:
    # pwd is not available on Windows..
    pwd = None

COMMASPACE = ', '

PROGRAM = sys.argv[0]


class SmtplibMock:
    """I stand in for smtplib for testing purposes."""

    class SMTP:
        """I stand in for smtplib.SMTP connection for testing purposes.
        I copy the message to stdout.
        """

        def close(self):
            pass

        def connect(self, mailhost, mailport):
            pass

        def sendmail(self, address, email, msg):
            sys.stdout.write(msg)


rfc822_specials_re = re.compile(r'[\(\)<>@,;:\\\"\.\[\]]')


def quotename(name):
    if name and rfc822_specials_re.search(name):
        return '"{}"'.format(name.replace('"', '\\"'))
    else:
        return name


def send_mail(options):
    # Create the smtp connection to the localhost
    conn = options.smtplib.SMTP()
    conn.connect(options.mailhost, options.mailport)
    if pwd:
        pwinfo = pwd.getpwuid(os.getuid())
        user = pwinfo[0]
        name = pwinfo[4]
    else:
        user = 'cvs'
        name = 'CVS'

    domain = options.fromhost
    if not domain:
        # getfqdn is not good for use in unit tests
        if options.amTesting:
            domain = 'testing.com'
        else:
            domain = socket.getfqdn()
    address = f'{user}@{domain}'
    s = StringIO()
    datestamp = time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime(time.time()))
    fileList = ' '.join(map(str, options.files))

    vars = {
        'author': formataddr((name, address)),
        'email': options.email,
        'subject': f'cvs update for project {options.project}',
        'version': __version__,
        'date': datestamp,
    }
    print(
        '''\
From: {author}
To: {email}'''.format(**vars),
        file=s,
    )
    if options.replyto:
        print(f'Reply-To: {options.replyto}', file=s)
    print(
        '''\
Subject: {subject}
Date: {date}
X-Mailer: Python buildbot-cvs-mail {version}
'''.format(**vars),
        file=s,
    )
    print(f'Cvsmode: {options.cvsmode}', file=s)
    print(f'Category: {options.category}', file=s)
    print(f'CVSROOT: {options.cvsroot}', file=s)
    print(f'Files: {fileList}', file=s)
    if options.path:
        print(f'Path: {options.path}', file=s)
    print(f'Project: {options.project}', file=s)
    cvs_input = sys.stdin.read()

    # On Python 2, sys.stdin.read() returns bytes, but
    # on Python 3, it returns unicode str.
    if isinstance(cvs_input, bytes):
        cvs_input = cvs_input.decode("utf-8")

    s.write(cvs_input)
    print('', file=s)
    conn.sendmail(address, options.email, s.getvalue())
    conn.close()


def fork_and_send_mail(options):
    # cannot wait for child process or that will cause parent to retain cvs
    # lock for too long.  Urg!
    if not os.fork():
        # in the child
        # give up the lock you cvs thang!
        time.sleep(2)
        send_mail(options)
        os._exit(0)


description = """
This script is used to provide email notifications of changes to the CVS
repository to a buildbot master.  It is invoked via a CVS loginfo file (see
$CVSROOT/CVSROOT/loginfo).  See the Buildbot manual for more information.
"""
usage = "%prog [options] %{sVv}"
parser = optparse.OptionParser(
    description=description, usage=usage, add_help_option=True, version=__version__
)

parser.add_option(
    "-C",
    "--category",
    dest='category',
    metavar="CAT",
    help=textwrap.dedent("""\
            Category for change. This becomes the Change.category attribute, which
            can be used within the buildmaster to filter changes.
            """),
)
parser.add_option(
    "-c",
    "--cvsroot",
    dest='cvsroot',
    metavar="PATH",
    help=textwrap.dedent("""\
            CVSROOT for use by buildbot workers to checkout code.
            This becomes the Change.repository attribute.
            Exmaple: :ext:myhost:/cvsroot
            """),
)
parser.add_option(
    "-e",
    "--email",
    dest='email',
    metavar="EMAIL",
    help=textwrap.dedent("""\
            Email address of the buildbot.
            """),
)
parser.add_option(
    "-f",
    "--fromhost",
    dest='fromhost',
    metavar="HOST",
    help=textwrap.dedent("""\
            The hostname that email messages appear to be coming from.  The From:
            header of the outgoing message will look like user@hostname.  By
            default, hostname is the machine's fully qualified domain name.
            """),
)
parser.add_option(
    "-m",
    "--mailhost",
    dest='mailhost',
    metavar="HOST",
    default="localhost",
    help=textwrap.dedent("""\
            The hostname of an available SMTP server.  The default is
            'localhost'.
            """),
)
parser.add_option(
    "--mailport",
    dest='mailport',
    metavar="PORT",
    default=25,
    type="int",
    help=textwrap.dedent("""\
            The port number of SMTP server.  The default is '25'.
            """),
)
parser.add_option(
    "-q",
    "--quiet",
    dest='verbose',
    action="store_false",
    default=True,
    help=textwrap.dedent("""\
            Don't print as much status to stdout.
            """),
)
parser.add_option(
    "-p",
    "--path",
    dest='path',
    metavar="PATH",
    help=textwrap.dedent("""\
            The path for the files in this update. This comes from the %p parameter
            in loginfo for CVS version 1.12.x. Do not use this for CVS version 1.11.x
            """),
)
parser.add_option(
    "-P",
    "--project",
    dest='project',
    metavar="PROJ",
    help=textwrap.dedent("""\
            The project for the source. Often set to the CVS module being modified. This becomes
            the Change.project attribute.
            """),
)
parser.add_option(
    "-R",
    "--reply-to",
    dest='replyto',
    metavar="ADDR",
    help=textwrap.dedent("""\
            Add a "Reply-To: ADDR" header to the email message.
            """),
)
parser.add_option("-t", "--testing", action="store_true", dest="amTesting", default=False)
parser.set_defaults(smtplib=smtplib)


def get_options():
    options, args = parser.parse_args()

    # rest of command line are the files.
    options.files = args
    if options.path is None:
        options.cvsmode = '1.11'
    else:
        options.cvsmode = '1.12'

    if options.cvsroot is None:
        parser.error('--cvsroot is required')
    if options.email is None:
        parser.error('--email is required')

    # set up for unit tests
    if options.amTesting:
        options.verbose = 0
        options.smtplib = SmtplibMock

    return options


# scan args for options


def main():
    options = get_options()

    if options.verbose:
        print(f'Mailing {options.email}...')
        print('Generating notification message...')
    if options.amTesting:
        send_mail(options)
    else:
        fork_and_send_mail(options)

    if options.verbose:
        print('Generating notification message... done.')
    return 0


if __name__ == '__main__':
    ret = main()
    sys.exit(ret)
