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

class Mailman extends Plugin
{

    /**
     * The threshold for refusing to get additional subscriber list information.
     */
    const SUBSCRIBER_LIST_THRESHOLD = 750;

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Mailman";
        $this->Version = "1.0.6";
        $this->Description = "
            Links a CWIS site with a Mailman installation
            to provide user-friendly mailing list subscription
            and transparent updates to mailing list subscriptions when a user's
            e-mail address is changed.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.2.3");
        $this->EnabledByDefault = FALSE;

        $this->CfgSetup["MailmanPrefix"] = array(
            "Type" => "Text",
            "Label" => "Mailman Prefix",
            "Help" => "Installation prefix of the Mailman software");

        $this->CfgSetup["EnableMailingList"] = array(
          "Type" => "Flag",
          "Label" => "Enable Mailing List",
          "Help" => "Enable the default mailing list.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");

        $this->CfgSetup["MailingList"] = array(
          "Type" => "Text",
          "Label" => "Mailing List Name",
          "Help" => "The name of the Mailman mailing list to manage.");

        $this->CfgSetup["MailingListLabel"] = array(
          "Type" => "Text",
          "Label" => "Mailing List Label",
          "Help" => trim(preg_replace('/\s+/', " ", "
              The label to use for the mailing list if the mailing list name
              contains more than two words or is otherwise undesirable to
              display unmodified.")));

        $this->CfgSetup["SubscribeByDefault"] = array(
          "Type" => "Flag",
          "Label" => "Subscribe New Users By Default",
          "Help" => "Check the subscription check box for new users by default.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");

        $this->CfgSetup["UnsubscribeWhenDeleted"] = array(
          "Type" => "Flag",
          "Label" => "Unsubscribe Users When Deleted",
          "Help" => "Unsubscribe users from the mailing list when deleted from CWIS.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");

        $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?",
            "OnLabel" => "Yes",
            "OffLabel" => "No");
    }

    /**
     * Install necessary SQL tables.
     * @return null|string NULL on success or error message on error
     */
    public function Install()
    {
        $Database = new Database();

        # table for pending subscriptions
        if (FALSE === $Database->Query("
            CREATE TABLE IF NOT EXISTS Mailman_PendingSubscriptions (
                UserId         INT,
                Created        DATETIME,
                PRIMARY KEY    (UserId)
            );"))
        { return "Could not create the pending subscriptions table."; }

        # set default configuration
        $this->ConfigSetting("MailmanPrefix", NULL);
        $this->ConfigSetting("EnableMailingList", FALSE);
        $this->ConfigSetting("LastMailingList", NULL);
        $this->ConfigSetting("MailingList", NULL);
        $this->ConfigSetting("MailingListLabel", NULL);
        $this->ConfigSetting("SubscribeByDefault", FALSE);
        $this->ConfigSetting("UnsubscribeWhenDeleted", FALSE);
        $this->ConfigSetting("EnableUnsubscribeLink", FALSE);

        return NULL;
    }

    /**
     * Upgrade from a previous version.
     * @param string $PreviousVersion previous version
     * @return null|string NULL on success or error message on error
     */
    public function Upgrade($PreviousVersion)
    {
        $Database = new Database();

        # remove old lists table, if it's still around
        if (FALSE === $Database->Query("DROP TABLE IF EXISTS MailmanLists"))
        { return "Could not delete the mailing lists table."; }

        # remove old config table, if it's still around
        if (FALSE === $Database->Query("DROP TABLE IF EXISTS MailmanConfig"))
        { return "Could not delete the configuration table."; }

        # ugprade from versions < 1.0.3 to 1.0.3
        if (version_compare($PreviousVersion, "1.0.3", "<"))
        {
            $Database = new Database();

            # table for pending subscriptions
            if (FALSE === $Database->Query("
                CREATE TABLE IF NOT EXISTS Mailman_PendingSubscriptions (
                    UserId         INT,
                    Created        DATETIME,
                    PRIMARY KEY    (UserId)
                );"))
            { return "Could not create the pending subscriptions table."; }
        }

        return NULL;
    }

    /**
     * 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.";
        }

        # should only check if the list is enabled and has change
        if ($this->DefaultListEnabled() && $this->ListHasChanged())
        {
            $AllLists = $this->GetAllLists();
            $MailingList = $this->ConfigSetting("MailingList");
            $MailingList = $this->NormalizeListName($MailingList);

            # make sure the mailing list is valid
            if (!in_array($MailingList, $AllLists))
            {
                return "The \"Mailman Mailing List\" setting is invalid.";
            }

            $this->SyncListChanges();
        }
    }

    /**
     * Declare the events this plugin provides to the application framework.
     * @return array the events this plugin provides
     */
    public function DeclareEvents()
    {
        $Events = array(
            "MAILMAN_GET_MAILMAN_VERSION"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_GET_ALL_LISTS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_GET_USER_SUBSCRIPTIONS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_IS_SUBSCRIBED"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_SUBSCRIBE"
                => ApplicationFramework::EVENTTYPE_DEFAULT,
            "MAILMAN_UNSUBSCRIBE"
                => ApplicationFramework::EVENTTYPE_DEFAULT,
            "MAILMAN_GET_STATISTICS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            # DEPRECATED EVENTS
            "MAILMAN_EVENT_GET_SUBS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_EVENT_CHANGE_SUBS"
                => ApplicationFramework::EVENTTYPE_DEFAULT);

        # these events should only be enabled if the default list is enabled
        if ($this->DefaultListEnabled())
        {
            $Events = array_merge($Events, array(
                "MAILMAN_IS_SUBSCRIBED_TO_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_FIRST,
                "MAILMAN_SUBSCRIBE_TO_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_DEFAULT,
                "MAILMAN_UNSUBSCRIBE_FROM_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_DEFAULT,
                "MAILMAN_GET_DEFAULT_LIST_DISPLAY_NAME"
                    => ApplicationFramework::EVENTTYPE_FIRST,
                "MAILMAN_GET_DEFAULT_LIST_STATISTICS"
                    => ApplicationFramework::EVENTTYPE_FIRST));
        }

        return $Events;
    }

    /**
     * Hook the events into the application framework.
     * @return array events to be hooked into the application framework
     */
    function HookEvents()
    {
        $Events = array(
            "MAILMAN_GET_MAILMAN_VERSION" => "GetMailmanVersion",
            "MAILMAN_GET_ALL_LISTS" => "GetAllLists",
            "MAILMAN_GET_USER_SUBSCRIPTIONS" => "GetUserSubscriptions",
            "MAILMAN_IS_SUBSCRIBED" => "IsSubscribed",
            "MAILMAN_SUBSCRIBE" => "Subscribe",
            "MAILMAN_UNSUBSCRIBE" => "Unsubscribe",
            "MAILMAN_GET_STATISTICS" => "GetStatistics",
            # DEPRECATED EVENTS
            "MAILMAN_EVENT_GET_SUBS"    => "DEPRECATED_GetUserSubscriptions",
            "MAILMAN_EVENT_CHANGE_SUBS" => "DEPRECATED_ChangeSubscription");

        # these events should only be hooked if the default list is enabled
        if ($this->DefaultListEnabled())
        {
            $Events = array_merge($Events, array(
                "EVENT_MONTHLY" => "RunMonthly",
                "EVENT_PAGE_LOAD" => "PageLoaded",
                "EVENT_USER_ADDED" => "UserAdded",
                "EVENT_USER_VERIFIED" => "UserVerified",
                "EVENT_USER_DELETED" => "UserDeleted",
                "EVENT_USER_REAL_NAME_CHANGED"  => "UserRealNameChanged",
                "EVENT_USER_EMAIL_CHANGED"  => "UserEmailChanged",
                "EVENT_APPEND_HTML_TO_FORM" => "AppendHtmlToForm",
                "EVENT_USER_ADMINISTRATION_MENU" => "DeclareUserPages",
                "MAILMAN_IS_SUBSCRIBED_TO_DEFAULT_LIST" => "IsSubscribedToDefaultList",
                "MAILMAN_SUBSCRIBE_TO_DEFAULT_LIST" => "SubscribeToDefaultList",
                "MAILMAN_UNSUBSCRIBE_FROM_DEFAULT_LIST" => "UnsubscribeFromDefaultList",
                "MAILMAN_GET_DEFAULT_LIST_DISPLAY_NAME" => "GetDefaultListDisplayName",
                "MAILMAN_GET_DEFAULT_LIST_STATISTICS" => "GetDefaultListStatistics"));
        }

        return $Events;
    }

    /**
     * Delete pending subscriptions that are older than a month.
     * @param string $LastRunAt the time this method was last run
     */
    public function RunMonthly($LastRunAt)
    {
        $Database = new Database();

        $Date = date("Y-m-d H:i:s", strtotime("-1 month"));

        $Database->Query("
            DELETE FROM Mailman_PendingSubscriptions
            WHERE Created < ".addslashes($Date));
    }

    /**
     * Manage subscription updates and subscription settings when there are
     * errors in the new account form values.
     * @param string $PageName page name
     */
    public function PageLoaded($PageName)
    {
        global $User;

        # catch the form value from the previous page load
        if (isset($_SESSION["P_Mailman_Subscribe"]))
        {
            $_GET["P_Mailman_Subscribe"] = $_SESSION["P_Mailman_Subscribe"];
            unset($_SESSION["P_Mailman_Subscribe"]);
        }

        if ($PageName == "PreferencesComplete")
        {
            $IsSubscribed = $this->IsSubscribedToDefaultList($User);
            $ShouldSubscribe = GetArrayValue($_POST, "P_Mailman_Subscribe");
            $Action = $ShouldSubscribe ?
                "SubscribeToDefaultList" : "UnsubscribeFromDefaultList";

            $this->$Action($User);
        }

        else if ($PageName == "RequestAccountComplete")
        {
            # pass the form value onto the next page load
            $_SESSION["P_Mailman_Subscribe"] =
                GetArrayValue($_POST, "P_Mailman_Subscribe");
        }

        return array("PageName" => $PageName);
    }

    /**
     * Add a newly-added user to the pending subscriptions list if he or she
     * requested a subscription to the default mailing list.
     * @param int|string $UserId user ID
     * @param string $Password user password
     */
    public function UserAdded($UserId, $Password)
    {
        $Database = new Database();

        if (GetArrayValue($_POST, "P_Mailman_Subscribe") == "on")
        {
            $Database->Query("
                INSERT INTO Mailman_PendingSubscriptions
                SET UserId = '".addslashes($UserId)."',
                Created = NOW()");
        }
    }

    /**
     * Add a recently-verified user to the default mailing list from the
     * pending subscriptions list if he or she requested a subscription.
     * @param int|string $UserId user ID
     */
    public function UserVerified($UserId)
    {
        $Database = new Database();

        $Database->Query("
            SELECT * FROM Mailman_PendingSubscriptions
            WHERE UserId = '".addslashes($UserId)."'");

        if ($Database->NumRowsSelected() > 0)
        {
            $User = new User($Database, $UserId);
            $this->SubscribeToDefaultList($User);

            $Database->Query("
                DELETE FROM Mailman_PendingSubscriptions
                WHERE UserId = '".addslashes($UserId)."'");
        }
    }

    /**
     * Remove a recently-deleted user from the default mailing list if the
     * plugin is configured to do so.
     * @param int|string $UserId user ID
     */
    public function UserDeleted($UserId)
    {
        $Database = new Database();

        if ($this->ConfigSetting("UnsubscribeWhenDeleted"))
        {
            $User = new User($Database, $UserId);
            $this->UnsubscribeFromDefaultList($User);

            $Database->Query("
                DELETE FROM Mailman_PendingSubscriptions
                WHERE UserId = '".addslashes($UserId)."'");
        }
    }

    /**
     * Update the user's mailing list subscriptions when his or her real name
     * value has changed
     * @param int|string $UserId user ID
     * @param string $OldRealName previous e-mail address for the user
     * @param string $NewRealName new e-mail address for the user
     */
    public function UserRealNameChanged($UserId, $OldRealName, $NewRealName)
    {
        $User = new User(new Database(), $UserId);
        $Email = $User->Get("EMail");
        $ListSubscriptions = $this->MailmanGetUserSubscriptions($Email);

        foreach ($ListSubscriptions as $List)
        {
            # unsubscribe the user from the list
            $this->Unsubscribe($User, $List);

            # re-subscribe the user to the list
            $this->Subscribe($User, $List);
        }
    }

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

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

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

    /**
     * Add a "subscribe" option to the new account form to allow users to
     * subscribe to the default Mailman mailing list.
     * @param string $Page page name
     * @param string $Form form name
     * @param array $Labels form labels
     * @param array $Inputs form input elements
     * @param array $Notes notes for the labels and inputs
     */
    public function AppendHtmlToForm($Page, $Form, $Labels, $Inputs, $Notes)
    {
        global $User;

        # allow new users to subscribe to the mailing list
        if ($Page == "RequestAccount" && $Form == "NewAccountForm")
        {
            $DisplayName = $this->GetDefaultListDisplayName();
            $ShouldSubscribe = GetArrayValue($_GET, "P_Mailman_Subscribe");
            $ShouldSubscribe = $ShouldSubscribe ||
                (!isset($_GET["FTAddErrCodes"]) &&
                $this->ConfigSetting("SubscribeByDefault"));
            $Checked = $ShouldSubscribe ? ' checked="checked"' : NULL;

            $Labels[] = NULL;
            $Notes[] = NULL;
            $Inputs[] = '
                <input type="checkbox" id="P_Mailman_Subscribe" name="P_Mailman_Subscribe"'.$Checked.' />
                <label for="P_Mailman_Subscribe">Subscribe to <i>' . $DisplayName . '</i></label>';
        }

        # allow existing users to modify their subscription
        else if ($Page == "Preferences" && $Form == "UserPreferences")
        {
            $DisplayName = defaulthtmlentities($this->GetDefaultListDisplayName());
            $IsSubscribed = $this->IsSubscribedToDefaultList($User);
            $YesChecked = $IsSubscribed ? ' checked="checked"' : NULL;
            $NoChecked = $IsSubscribed ? NULL : ' checked="checked"';

            $Notes[] = NULL;
            $Labels[] = '<label for="P_Mailman_Subscribe">Subscribe to <i>' . $DisplayName . '</i></label>';
            $Inputs[] = '
                <input type="radio" id="P_Mailman_Subscribe_Yes" name="P_Mailman_Subscribe" value="1"'.$YesChecked.' />
                <label for="P_Mailman_Subscribe_Yes">Yes</label>

                <input type="radio" id="P_Mailman_Subscribe_No" name="P_Mailman_Subscribe" value="0"'.$NoChecked.' />
                <label for="P_Mailman_Subscribe_No">No</label>';
        }

        return array(
            "PageName" => $Page,
            "FormName" => $Form,
            "Labels" => $Labels,
            "InputElements" => $Inputs,
            "Notes" => $Notes);
    }

    /**
     * Declare the user administration pages this plugin provides.
     * @return array page URLs and labels
     */
    public function DeclareUserPages()
    {
        return array("Information" => "Mailing List Information");
    }

    /**
     * Get the version number of the Mailman program.
     * @return string version number of the Mailman program
     */
    public function GetMailmanVersion()
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/version";
        $SafeBinary = escapeshellarg($Binary);

        # construct the shell command
        $Command = "sudo -u mailman " . $SafeBinary;

        exec($Command, $Lines, $ReturnVar);

        # parse out the version number if possible
        if ($ReturnVar === 0 && count($Lines))
        {
            preg_match('/^Using Mailman version: (.*?)$/', $Lines[0], $Matches);

            if (count($Matches))
            {
                return $Matches[1];
            }
        }

        return "unknown";
    }

    /**
     * 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))
        {
            $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/list_lists";
            $SafeBinary = escapeshellarg($Binary);

            $Mailman = "sudo -u mailman " . $SafeBinary;
            $Options = "--bare";
            $Command = $Mailman . " " . $Options;

            exec($Command, $this->Lists);
        }

        return $this->Lists;
    }

    /**
     * Get the Mailman mailing lists to which the given user is subscribed.
     * @param User $User user
     * @return array mailing lists to which the user is subscribed
     */
    public function GetUserSubscriptions(User $User)
    {
        $Email = $User->Get("EMail");

        return $this->MailmanGetUserSubscriptions($Email);
    }

    /**
     * Determine if the given user is subscribed to the given Mailman mailing
     * list.
     * @param User $User user
     * @param string $List mailing list name
     * @return bool TRUE if the user is subscribed to the mailing list
     */
    public function IsSubscribed(User $User, $List)
    {
        $ListSubscriptions = $this->GetUserSubscriptions($User);
        $List = $this->NormalizeListName($List);

        $IsSubscribed = in_array($List, $ListSubscriptions);

        return $IsSubscribed;
    }

    /**
     * Subscribe the given user to the given Mailman mailing list.
     * @param User $User user
     * @param string $List mailing list
     */
    public function Subscribe(User $User, $List)
    {
        $AllLists = $this->GetAllLists();
        $List = $this->NormalizeListName($List);

        # don't try subscribing the user to a non-existent list
        if (!in_array($List, $AllLists))
        {
            return;
        }

        $RealName = $User->Get("RealName");
        $Email = $User->Get("EMail");

        # tack on the real name of the user if available
        if (strlen($RealName))
        {
            $DisplayName = $this->EscapeEmailDisplayName($RealName);
            $Subscription = '"' . $DisplayName . '"' . " <" . $Email . ">";
        }

        # otherwise, just use the e-mail address
        else
        {
            $Subscription = $Email;
        }

        $this->MailmanSubscribe($Subscription, $List);
    }

    /**
     * Unsubscribe the given user from the given Mailman mailing list.
     * @param User $User user
     * @param string $List mailing list
     */
    public function Unsubscribe(User $User, $List)
    {
        $AllLists = $this->GetAllLists();
        $List = $this->NormalizeListName($List);

        # don't try unsubscribing the user to a non-existent list
        if (!in_array($List, $AllLists))
        {
            return;
        }

        $this->MailmanUnsubscribe($User->Get("EMail"), $List);
    }

    /**
     * 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.
     */
    public function UnsubscribeViaLink($Address, $List)
    {
        if ($this->ConfigSetting("EnableUnsubscribeLink"))
        {
            return $this->MailmanUnsubscribe($Address, $List);
        }
        else
        {
            return array("One-click unsubscription is disabled.");
        }
    }

    /**
     * Get the statistics for the given mailing list.
     * @param string $List mailing list name
     * @return array statistics for the mailing list
     */
    public function GetStatistics($List)
    {
        $Subscribers = $this->MailmanGetSubscribers($List);
        $NumSubscribers = count($Subscribers);
        $ShowSubscribers = $NumSubscribers < self::SUBSCRIBER_LIST_THRESHOLD;
        $SubscriberList = array();
        $NumUsersInCwis = 0;

        # only attempt getting additional information for the subscribers if
        # the number of subscribers is below the threshold to avoid performance
        # issues
        if ($ShowSubscribers)
        {
            $Information = $this->GetInformationForSubscriberList($Subscribers);
            $SubscriberList = $Information["Subscribers"];
            $NumUsersInCwis = $Information["NumUsersInCwis"];
        }

        return array(
            "MailingList" => $this->ConfigSetting("MailingList"),
            "MailingListLabel" => $this->ConfigSetting("MailingListLabel"),
            "Subscribers" => $SubscriberList,
            "NumSubscribers" => $NumSubscribers,
            "NumUsersInCwis" => $NumUsersInCwis,
            "ShowSubscribers" => $ShowSubscribers,
            "PluginVersion" => $this->Version,
            "MailmanVersion" => $this->GetMailmanVersion());
    }

    /**
     * Determine if the given user is subscribed to the default Mailman mailing
     * list.
     * @param User $User user
     * @return bool TRUE if the user is subscribed to the default mailing list
     */
    public function IsSubscribedToDefaultList(User $User)
    {
        $ListSubscriptions = $this->GetUserSubscriptions($User);
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $IsSubscribed = in_array($DefaultList, $ListSubscriptions);

        return $IsSubscribed;
    }

    /**
     * Subscribe the given user to the default Mailman mailing list.
     * @param User $User user
     */
    public function SubscribeToDefaultList(User $User)
    {
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $this->Subscribe($User, $DefaultList);
    }

    /**
     * Unsubscribe the given user from the default Mailman mailing list.
     * @param User $User user
     */
    public function UnsubscribeFromDefaultList(User $User)
    {
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $this->Unsubscribe($User, $DefaultList);
    }

    /**
     * Get the display name for the default Mailman mailing list. It will return
     * the mailing list label or the mailing list name if the label is not set.
     * @return string the display name for the default Mailman mailing list
     */
    public function GetDefaultListDisplayName()
    {
        $DisplayName = $this->ConfigSetting("MailingListLabel");

        # use the mailing list name if the label is unavailable
        if (is_null($DisplayName) || strlen(trim($DisplayName)) < 1)
        {
            $DisplayName = $this->ConfigSetting("MailingList");
        }

        return $DisplayName;
    }

    /**
     * Get the statistics for the default mailing list.
     * @return array statistics for the default mailing list
     */
    public function GetDefaultListStatistics()
    {
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        return $this->GetStatistics($DefaultList);
    }

    /**
     * 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
     */
    protected function MailmanGetUserSubscriptions($Email)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/find_member";
        $SafeBinary = escapeshellarg($Binary);
        $SafeEmail = escapeshellarg("^" . $Email . "$");

        # construct the shell command
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--owners";
        $Command = $Mailman . " " . $Options . " " . $SafeEmail;

        exec($Command, $Lines);

        $Lists = array();

        foreach ($Lines 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
     */
    protected function MailmanSubscribe($Subscription, $List)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/add_members";
        $SafeBinary = escapeshellarg($Binary);
        $SafeSubscription = escapeshellarg($Subscription);
        $SafeList = escapeshellarg($List);

        # construct the shell command
        $Echo = "echo " . $SafeSubscription;
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--regular-members-file=- --welcome-msg=n --admin-notify=n";
        $Command = $Echo . " | " . $Mailman . " " . $Options . " " . $SafeList;

        exec($Command);
    }

    /**
     * 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
     */
    protected function MailmanUnsubscribe($Email, $List)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/remove_members";
        $SafeBinary = escapeshellarg($Binary);
        $SafeEmail = escapeshellarg($Email);
        $SafeList = escapeshellarg($List);

        # construct the shell command
        $Echo = "echo " . $SafeEmail;
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--file=- --nouserack --noadminack";
        $Command = $Echo . " | " . $Mailman . " " . $Options . " " . $SafeList . " 2>&1";

        exec($Command, $Output);

        return $Output;
    }

    /**
     * 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
     */
    protected function MailmanGetSubscribers($List)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/list_members";
        $SafeBinary = escapeshellarg($Binary);
        $SafeList = escapeshellarg($List);

        # construct the shell command
        $Command = "sudo -u mailman " . $SafeBinary . " " . $SafeList;

        exec($Command, $Subscribers, $ReturnValue);

        if ($ReturnValue === 0 && count($Subscribers))
        {
            return $Subscribers;
        }

        return array();
    }

    /**
     * Get additional information for the given subscriber list.
     * @param array $Subscribers subscriber list
     * @return array additional information for each subscriber
     */
    protected function GetInformationForSubscriberList(array $Subscribers)
    {
        $Database = new Database();
        $ExtraInformation = array(
            "Subscribers" => array(),
            "NumUsersInCwis" => 0);

        foreach ($Subscribers as $Subscriber)
        {
            # so that each subscriber has these indices regardless
            $ExtraInformation["Subscribers"][$Subscriber] = array(
                "InCwis" => FALSE,
                "UserId" => NULL,
                "UserName" => NULL,
                "RealName" => NULL);

            $Database->Query("
                SELECT * FROM APUsers
                WHERE EMail = '".addslashes($Subscriber)."'");

            # if the user is in CWIS
            if ($Database->NumRowsSelected())
            {
                $Row = $Database->FetchRow();

                $ExtraInformation["NumUsersInCwis"] += 1;
                $ExtraInformation["Subscribers"][$Subscriber] = array(
                    "InCwis" => TRUE,
                    "UserId" => $Row["UserId"],
                    "UserName" => $Row["UserName"],
                    "RealName" => $Row["RealName"]);
            }
        }

        return $ExtraInformation;
    }

    /**
     * Normalize the given mailing list name.
     * @param string $ListName mailing list name
     * @return string normalized mailing list name
     */
    protected function NormalizeListName($ListName)
    {
        return strtolower(trim($ListName));
    }

    /**
     * Transform the given display name to a string that is a valid display-name
     * token from the spec.
     * @param string $DisplayName e-mail display name
     * @return string valid display-name token
     * @see http://tools.ietf.org/html/rfc5322#section-3.4
     */
    protected function EscapeEmailDisplayName($DisplayName)
    {
        return preg_replace('/[^\x20\x23-\x5B\x5D-\x7E]/', "", $DisplayName);
    }

    /**
     * Determine whether the default mailing list is enabled in CWIS.
     * @return bool TRUE if the default list is enabled or FALSE otherwise
     */
    protected function DefaultListEnabled()
    {
        return $this->ConfigSetting("EnableMailingList");
    }

    /**
     * Determine if the mailing list value has changed.
     * @return bool TRUE if the mailing list value changed or FALSE otherwise
     */
    protected function ListHasChanged()
    {
        $LastMailingList = $this->ConfigSetting("LastMailingList");
        $MailingList = $this->ConfigSetting("MailingList");

        return $LastMailingList != $MailingList;
    }

    /**
     * Synchronize the list changes, i.e., set the last mailing list value to
     * the current mailing list value.
     */
    protected function SyncListChanges()
    {
        $MailingList = $this->ConfigSetting("MailingList");
        $this->ConfigSetting("LastMailingList", $MailingList);
    }

    /**
     * @var array $Lists mailing list cache
     */
    protected $Lists;

    # ----- DEPRECATED --------------------------------------------------------

    /**
     * DEPRECATED: Get the mailing list subscriptions of the current user.
     * @return array mailing lists to which the user is subscribed
     */
    public function DEPRECATED_GetUserSubscriptions()
    {
        global $User;

        return $this->GetUserSubscriptions($User);
    }

    /**
     * DEPRECATED: Change a mailing list subscription of the current user.
     * @param string $List mailing list name
     * @param bool $Subscribe TRUE to subscribe and FALSE to unsubscribe
     */
    public function DEPRECATED_ChangeSubscription($List, $Subscribe)
    {
        global $User;

        $Action = $Subscribe ? "Subscribe" : "Unsubscribe";

        $this->$Action($User, $List);
    }

}
