<?PHP
#
#   FILE:  MLMailman.php
#
#   NOTE: View the README file for more information.
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2012-2014 Internet Scout Project
#   http://scout.wisc.edu/cwis/
#

class MLMailman extends Plugin
{
    /**
    * Register information about this plugin.
    */
    public function Register()
    {
        $this->Name = "Mailman";
        $this->Version = "1.0.0";
        $this->Description = "MailingList backend plugin to support Mailman.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
            "CWISCore" => "2.2.3",
            "MailingList" => "1.0.0"
            );
        $this->EnabledByDefault = FALSE;

        $this->InitializeBefore = array("MailingList");

        $this->CfgSetup["MailmanPrefix"] = array(
            "Type" => "Text",
            "Label" => "Mailman Prefix",
            "Help" => "Installation prefix of the Mailman software "
              ." (i.e. the directory where mailman is installed). "
              ." This will be /usr/lib/mailman on most distributions.");

        $this->CfgSetup["EnableUnsubscribeLink"] = array(
            "Type" => "Flag",
            "Label" => "Enable One-Click Unsubscription Link",
            "Help" => "Should a link (to be used in email footers) for single-click "
              ."unsubscription be enabled?  While these links can be convenient for users, "
              ."they are somewhat risky; anyone who has the link can unsubscribe the "
              ."corresponding user, even if they got the link because someone forwarded them "
              ."a message.",
            "OnLabel" => "Yes",
            "OffLabel" => "No");
    }

    /**
    * Make sure the Mailman configuration values are valid
    * @return null|string NULL if there are no errors or an error message
    */
    public function Initialize()
    {
        $MailmanPrefix = $this->ConfigSetting("MailmanPrefix");

        # if the find_member executable is not found, return an error
        if (!file_exists($MailmanPrefix . "/bin/find_member"))
        {
            return "The \"Mailman Prefix\" setting is invalid.";
        }

        MailingList::RegisterBackend( $this->Name, "Mailman Support", $this );
    }

    /**
    * Get all available mailman mailing lists. This method can cause mailman to
    * generate many DNS requests; use with caution. Lists are fetched once per
    * object.
    * @return array all available mailing lists
    */
    public function GetAllLists()
    {
        if (!isset($this->Lists))
        {
            $Result = $this->RunMailmanCommand(
                "list_lists", NULL, "--bare");
            $this->Lists = $Result["Output"];
        }

        return $this->Lists;
    }

    /**
    * Unsubscribe the given address from the given Mailman list.
    * @param string $Address a target email address
    * @param string $List a target mailing list
    * Because of the lack of security associated with this method
    * (anybody can unsubscribe anybody else without any
    * authentication required), it is disabled by default.
    * @return error string if link unsubs are disabled, TRUE on
    *   success, or FALSE if link unsubs are enabled but the user wasn't
    *   subscribed.
    */
    public function UnsubscribeViaLink($Address, $List)
    {
        if ($this->ConfigSetting("EnableUnsubscribeLink"))
        {
            return $this->Unsubscribe($Address, $List);
        }
        else
        {
            return "One-click unsubscription is disabled.";
        }
    }

    /**
    * Get the mailing list subscriptions for the given e-mail address via a
    * shell.
    * @param string $Email e-mail address
    * @return array mailing lists to which the user is subscribed
    */
    public function GetUserSubscriptions($Email)
    {
        $Result = $this->RunMailmanCommand(
            "find_member", $Email, "--owners" );

        $Lists = array();
        foreach ($Result["Output"] as $Line)
        {
            # skip over ". . . found in:" lines
            if (preg_match('/^     (.+?)$/', $Line, $Matches))
            {
                $Lists[] = $Matches[1];
            }
        }

        # remove redundant list names
        $Lists = array_unique($Lists);

        return $Lists;
    }

    /**
    * Subscribe the given display name and e-mail address or just an e-mail
    * address to the given list via a shell.
    * @param string $Subscription display name (optional) and e-mail address
    * @param string $List mailing list name
    * @return TRUE on success, FALSE on failure
    */
    public function Subscribe($Subscription, $List)
    {
        $Result = $this->RunMailmanCommand(
            "add_members",
            $List,
            "--regular-members-file=- --welcome-msg=n --admin-notify=n",
            $Subscription);

        # on success, add_members tells us who was subscribed
        if ($Result["ReturnCode"] === 0 &&
            current($Result["Output"]) == "Subscribed: ".$Subscription)
        {
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }

    /**
    * Unsubscribe the given e-mail address from the given list via a shell.
    * @param string $Email e-mail address
    * @param string $List mailing list name
    * @return TRUE on success, FALSE on failure
    */
    public function Unsubscribe($Email, $List)
    {
        $Result = $this->RunMailmanCommand(
            "remove_members",
            $List,
            "--file=- --nouserack --noadminack",
            $Email );

        # on success, remove_members returns zero and says nothing
        if ($Result["ReturnCode"] === 0 &&
            count($Result["Output"]) == 0)
        {
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }

    /**
    * Update the user's mailing list subscriptions when his or her e-mail
    * address has changed.
    * @param string $OldEmail previous e-mail address for the user
    * @param string $NewEmail new e-mail address for the user
    */
    public function UpdateUserEmail($OldEmail, $NewEmail)
    {
        $ListSubscriptions = $this->GetUserSubscriptions($OldEmail);

        foreach ($ListSubscriptions as $List)
        {
            # unsubscribe the user from the list with his or her old e-mail
            $this->Unsubscribe($OldEmail, $List);

            # subscribe the user to the list with his or her new e-mail
            $this->Subscribe($NewEmail, $List);
        }
    }

    /**
    * Get the list of subscribers to the given list via a shell.
    * @param string $List mailing list name
    * @return array list of subscribers to the list
    */
    public function GetSubscribers($List)
    {
        $Result = $this->RunMailmanCommand(
            "list_members",
            $List);

        if ($Result["ReturnCode"] === 0 &&
            count($Result["Output"]) )
        {
            return $Result["Output"];
        }
        else
        {
            return array();
        }
    }

    /**
    * Execute a mailman command via sudo
    * @param string $Command to run
    * @param string|NULL $List to operate on
    * @param string $Options flags to use (optional, default "")
    * @param string $Input to give on stdin (optional, default NULL)
    * @return array() with keys ReturnCode and Output.  ReturnCode is
    *   the int return code Output is an array of strings that were
    *   received on stdout.
    */
    private function RunMailmanCommand($Command, $List, $Options="", $Input=NULL)
    {

        $Binary = escapeshellarg(
            $this->ConfigSetting("MailmanPrefix")."/bin/".$Command );

        $Command = "sudo -u mailman ".$Binary." ".$Options;

        # if a list was specified, add at the end of the command
        if ($List !== NULL)
        {
            $Command .= " ".escapeshellarg($List);
        }

        # if input was specified, pipe it in with echo
        if ($Input !== NULL)
        {
            $Command = "echo ".escapeshellarg($Input)." | ".$Command ;
        }

        # run the command, snagging the output and return code
        exec($Command, $Output, $ReturnCode);

        # pass both back to caller
        return array("Output" => $Output,
                     "ReturnCode" => $ReturnCode );
    }

    /**
    * @var array $Lists mailing list cache
    */
    private $Lists;
}
