# -*- test-case-name: twisted.mail.test.test_mailmail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Tests for L{twisted.mail.scripts.mailmail}, the implementation of the
command line program I{mailmail}.
"""

import os
import sys
from io import StringIO
from unittest import skipIf

from twisted.copyright import version
from twisted.internet.defer import Deferred
from twisted.mail import smtp
from twisted.mail.scripts import mailmail
from twisted.mail.scripts.mailmail import parseOptions
from twisted.python.failure import Failure
from twisted.python.runtime import platformType
from twisted.test.proto_helpers import MemoryReactor
from twisted.trial.unittest import TestCase


class OptionsTests(TestCase):
    """
    Tests for L{parseOptions} which parses command line arguments and reads
    message text from stdin to produce an L{Options} instance which can be
    used to send a message.
    """

    memoryReactor = MemoryReactor()

    def setUp(self):
        """
        Override some things in mailmail, so that we capture C{stdout},
        and do not call L{reactor.stop}.
        """
        self.out = StringIO()
        # Override the mailmail logger, so we capture stderr output
        from twisted.logger import textFileLogObserver, Logger

        logObserver = textFileLogObserver(self.out)
        self.patch(mailmail, "_log", Logger(observer=logObserver))
        self.host = None
        self.options = None
        self.ident = None

        # Override mailmail.sendmail, so we don't call reactor.stop()
        def sendmail(host, options, ident):
            self.host = host
            self.options = options
            self.ident = ident
            return smtp.sendmail(
                host,
                options.sender,
                options.to,
                options.body,
                reactor=self.memoryReactor,
            )

        self.patch(mailmail, "sendmail", sendmail)

    def test_unspecifiedRecipients(self):
        """
        If no recipients are given in the argument list and there is no
        recipient header in the message text, L{parseOptions} raises
        L{SystemExit} with a string describing the problem.
        """
        self.patch(sys, "stdin", StringIO("Subject: foo\n" "\n" "Hello, goodbye.\n"))
        exc = self.assertRaises(SystemExit, parseOptions, [])
        self.assertEqual(exc.args, ("No recipients specified.",))

    def test_listQueueInformation(self):
        """
        The I{-bp} option for listing queue information is unsupported and
        if it is passed to L{parseOptions}, L{SystemExit} is raised.
        """
        exc = self.assertRaises(SystemExit, parseOptions, ["-bp"])
        self.assertEqual(exc.args, ("Unsupported option.",))

    def test_stdioTransport(self):
        """
        The I{-bs} option for using stdin and stdout as the SMTP transport
        is unsupported and if it is passed to L{parseOptions}, L{SystemExit}
        is raised.
        """
        exc = self.assertRaises(SystemExit, parseOptions, ["-bs"])
        self.assertEqual(exc.args, ("Unsupported option.",))

    def test_ignoreFullStop(self):
        """
        The I{-i} and I{-oi} options for ignoring C{"."} by itself on a line
        are unsupported and if either is passed to L{parseOptions},
        L{SystemExit} is raised.
        """
        exc = self.assertRaises(SystemExit, parseOptions, ["-i"])
        self.assertEqual(exc.args, ("Unsupported option.",))
        exc = self.assertRaises(SystemExit, parseOptions, ["-oi"])
        self.assertEqual(exc.args, ("Unsupported option.",))

    def test_copyAliasedSender(self):
        """
        The I{-om} option for copying the sender if they appear in an alias
        expansion is unsupported and if it is passed to L{parseOptions},
        L{SystemExit} is raised.
        """
        exc = self.assertRaises(SystemExit, parseOptions, ["-om"])
        self.assertEqual(exc.args, ("Unsupported option.",))

    def test_version(self):
        """
        The I{--version} option displays the version and raises
        L{SystemExit} with L{None} as the exit code.
        """
        out = StringIO()
        self.patch(sys, "stdout", out)
        systemExitCode = self.assertRaises(SystemExit, parseOptions, "--version")
        # SystemExit.code is None on success
        self.assertEqual(systemExitCode.code, None)
        data = out.getvalue()
        self.assertEqual(data, f"mailmail version: {version}\n")

    def test_backgroundDelivery(self):
        """
        The I{-odb} flag specifies background delivery.
        """
        stdin = StringIO("\n")
        self.patch(sys, "stdin", stdin)
        o = parseOptions("-odb")
        self.assertTrue(o.background)

    def test_foregroundDelivery(self):
        """
        The I{-odf} flags specifies foreground delivery.
        """
        stdin = StringIO("\n")
        self.patch(sys, "stdin", stdin)
        o = parseOptions("-odf")
        self.assertFalse(o.background)

    def test_recipientsFromHeaders(self):
        """
        The I{-t} flags specifies that recipients should be obtained
        from headers.
        """
        stdin = StringIO(
            "To: Curly <invaliduser2@example.com>\n"
            "Cc: Larry <invaliduser1@example.com>\n"
            "Bcc: Moe <invaliduser3@example.com>\n"
            "\n"
            "Oh, a wise guy?\n"
        )
        self.patch(sys, "stdin", stdin)
        o = parseOptions("-t")
        self.assertEqual(len(o.to), 3)

    def test_setFrom(self):
        """
        When a message has no I{From:} header, a I{From:} value can be
        specified with the I{-F} flag.
        """
        stdin = StringIO("To: invaliduser2@example.com\n" "Subject: A wise guy?\n\n")
        self.patch(sys, "stdin", stdin)
        o = parseOptions(["-F", "Larry <invaliduser1@example.com>", "-t"])
        self.assertEqual(o.sender, "Larry <invaliduser1@example.com>")

    def test_overrideFromFlagByFromHeader(self):
        """
        The I{-F} flag specifies the From: value.  However, I{-F} flag is
        overriden by the value of From: in the e-mail header.
        """
        stdin = StringIO(
            "To: Curly <invaliduser4@example.com>\n"
            "From: Shemp <invaliduser4@example.com>\n"
        )
        self.patch(sys, "stdin", stdin)
        o = parseOptions(["-F", "Groucho <invaliduser5@example.com>", "-t"])
        self.assertEqual(o.sender, "invaliduser4@example.com")

    @skipIf(
        platformType == "win32",
        "mailmail.run() does not work on win32 due to lack of support for" " getuid()",
    )
    def test_runErrorsToStderr(self):
        """
        Call L{mailmail.run}, and specify I{-oep} to print errors
        to stderr.  The sender, to, and printErrors options should be
        set and there should be no failure.
        """
        argv = ("test_mailmail.py", "invaliduser2@example.com", "-oep")
        stdin = StringIO("\n")
        self.patch(sys, "argv", argv)
        self.patch(sys, "stdin", stdin)
        mailmail.run()
        self.assertEqual(self.options.sender, mailmail.getlogin())
        self.assertEqual(self.options.to, ["invaliduser2@example.com"])
        # We should have printErrors set because we specified "-oep"
        self.assertTrue(self.options.printErrors)
        # We should not have any failures.
        self.assertIsNone(mailmail.failed)

    @skipIf(
        platformType == "win32",
        "mailmail.run() does not work on win32 due to lack of support for" " getuid()",
    )
    def test_readInvalidConfig(self):
        """
        Error messages for illegal UID value, illegal GID value, and illegal
        identity entry will be sent to stderr.
        """
        stdin = StringIO("\n")
        self.patch(sys, "stdin", stdin)

        filename = self.mktemp()
        myUid = os.getuid()
        myGid = os.getgid()

        with open(filename, "w") as f:
            # Create a config file with some invalid values
            f.write(
                "[useraccess]\n"
                "allow=invaliduser2,invaliduser1\n"
                "deny=invaliduser3,invaliduser4,{}\n"
                "order=allow,deny\n"
                "[groupaccess]\n"
                "allow=invalidgid1,invalidgid2\n"
                "deny=invalidgid1,invalidgid2,{}\n"
                "order=deny,allow\n"
                "[identity]\n"
                "localhost=funny\n"
                "[addresses]\n"
                "smarthost=localhost\n"
                "default_domain=example.com\n".format(myUid, myGid)
            )

        # The mailmail script looks in
        # the twisted.mail.scripts.GLOBAL_CFG variable
        # and then the twisted.mail.scripts.LOCAL_CFG
        # variable for the path to it's  config file.
        #
        # Override twisted.mail.scripts.LOCAL_CFG with the file we just
        # created.
        self.patch(mailmail, "LOCAL_CFG", filename)

        argv = ("test_mailmail.py", "invaliduser2@example.com", "-oep")
        self.patch(sys, "argv", argv)
        mailmail.run()
        self.assertRegex(
            self.out.getvalue(),
            "Illegal UID in \\[useraccess\\] section: " "invaliduser1",
        )
        self.assertRegex(
            self.out.getvalue(),
            "Illegal GID in \\[groupaccess\\] section: " "invalidgid1",
        )
        self.assertRegex(
            self.out.getvalue(), "Illegal entry in \\[identity\\] section: funny"
        )

    def getConfigFromFile(self, config):
        """
        Read a mailmail configuration file.

        The mailmail script checks the twisted.mail.scripts.mailmail.GLOBAL_CFG
        variable and then the twisted.mail.scripts.mailmail.LOCAL_CFG
        variable for the path to its  config file.

        @param config: path to config file
        @type config: L{str}

        @return: A parsed config.
        @rtype: L{twisted.mail.scripts.mailmail.Configuration}
        """

        from twisted.mail.scripts.mailmail import loadConfig

        filename = self.mktemp()

        with open(filename, "w") as f:
            f.write(config)

        return loadConfig(filename)

    def test_loadConfig(self):
        """
        L{twisted.mail.scripts.mailmail.loadConfig}
        parses the config file for mailmail.
        """
        config = self.getConfigFromFile(
            """
[addresses]
smarthost=localhost"""
        )
        self.assertEqual(config.smarthost, "localhost")

        config = self.getConfigFromFile(
            """
[addresses]
default_domain=example.com"""
        )
        self.assertEqual(config.domain, "example.com")

        config = self.getConfigFromFile(
            """
[addresses]
smarthost=localhost
default_domain=example.com"""
        )
        self.assertEqual(config.smarthost, "localhost")
        self.assertEqual(config.domain, "example.com")

        config = self.getConfigFromFile(
            """
[identity]
host1=invalid
host2=username:password"""
        )
        self.assertNotIn("host1", config.identities)
        self.assertEqual(config.identities["host2"], ["username", "password"])

        config = self.getConfigFromFile(
            """
[useraccess]
allow=invalid1,35
order=allow"""
        )
        self.assertEqual(config.allowUIDs, [35])

        config = self.getConfigFromFile(
            """
[useraccess]
deny=35,36
order=deny"""
        )
        self.assertEqual(config.denyUIDs, [35, 36])

        config = self.getConfigFromFile(
            """
[useraccess]
allow=35,36
deny=37,38
order=deny"""
        )
        self.assertEqual(config.allowUIDs, [35, 36])
        self.assertEqual(config.denyUIDs, [37, 38])

        config = self.getConfigFromFile(
            """
[groupaccess]
allow=gid1,41
order=allow"""
        )
        self.assertEqual(config.allowGIDs, [41])

        config = self.getConfigFromFile(
            """
[groupaccess]
deny=41
order=deny"""
        )
        self.assertEqual(config.denyGIDs, [41])

        config = self.getConfigFromFile(
            """
[groupaccess]
allow=41,42
deny=43,44
order=allow,deny"""
        )
        self.assertEqual(config.allowGIDs, [41, 42])
        self.assertEqual(config.denyGIDs, [43, 44])

    def test_senderror(self):
        """
        L{twisted.mail.scripts.mailmail.senderror} sends mail back to the
        sender if an error occurs while sending mail to the recipient.
        """

        def sendmail(host, sender, recipient, body):
            self.assertRegex(sender, "postmaster@")
            self.assertEqual(recipient, ["testsender"])
            self.assertRegex(body.getvalue(), "ValueError")
            return Deferred()

        self.patch(smtp, "sendmail", sendmail)
        opts = mailmail.Options()
        opts.sender = "testsender"
        fail = Failure(ValueError())
        mailmail.senderror(fail, opts)
