1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    use Opcenter\Filesystem;
    use Opcenter\Mail\Services\Majordomo;
    use Opcenter\Mail\Services\Postfix;

    /**
     * Majordomo mailing list functions
     *
     * @package core
     */
    class Majordomo_Module extends Module_Skeleton implements \Opcenter\Contracts\Hookable
    {
        const DEPENDENCY_MAP = [
            'mail'
        ];
        const  MAJORDOMO_SETUID = 'nobody';

        /**
         * {{{ void __construct(void)
         *
         * @ignore
         */
        public function __construct()
        {
            parent::__construct();
            if (!($this->permission_level & PRIVILEGE_SITE) || !$this->enabled()) {
                // permission level is null in backend enumeration
                $this->exportedFunctions = [
                    '*' => PRIVILEGE_NONE,
                    'enabled' => PRIVILEGE_SITE
                ];
                return;
            }

            $this->exportedFunctions = array(
                '*'                                 => PRIVILEGE_SITE,
                'list_mailing_lists_backend'        => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
                'create_mailing_list_backend'       => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
                'delete_mailing_list_backend'       => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
                'get_mailing_list_users_backend'    => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
                'get_domain_from_list_name_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC
            );
            include_once(INCLUDE_PATH . '/lib/configuration_driver.php');
            include(INCLUDE_PATH . '/lib/modules/majordomo/config_skeleton.php');


            $this->majordomo_skeleton = $__majordomo_skeleton;
            $this->majordomo_preamble = $__majordomo_preamble;
        }

        /**
         * Service enabled
         *
         * @return bool
         */
        public function enabled(): bool {
            if (!platform_is('7.5')) {
                return true;
            }
            return $this->email_get_provider() === 'builtin' &&
                $this->getConfig('mlist', 'enabled') &&
                $this->getConfig('mlist', 'provider', 'majordomo') === 'majordomo';
        }

        public function get_mailing_list_users($list)
        {
            if (!IS_CLI) {
                return $this->query('majordomo_get_mailing_list_users', $list);
            }

            if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
                return error('Invalid list ' . $list);
            }

            if (!file_exists($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
                return error('Invalid list name ' . $list);
            }

            return file_get_contents($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list);
        }

        public function create_mailing_list($list, $password, $email = null, $domain = null)
        {
            $max = $this->getServiceValue('mlist', 'max');
            if ($max !== null) {
                $count = \count($this->list_mailing_lists());
                if ($count >= $max) {
                    return error("Mailing list limit `%d' reached", $count);
                }
            }
            $list = strtolower(trim($list));

            if (!$domain) {
                $domain = $this->getServiceValue('siteinfo', 'domain');
            }
            if (!$email) {
                $email = trim($this->getServiceValue('siteinfo', 'email'));
            }

            $email = strtolower($email);

            if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
                return error('Invalid list name');
            }
            if ($this->mailing_list_exists($list)) {
                return error('Mailing list already exists');
            }
            if (!$password) {
                return error('Invalid argument: missing password');
            }
            if (!in_array($domain, $this->email_list_virtual_transports())) {
                return error('Domain not configured to handle mail');
            }
            if (!preg_match(Regex::EMAIL, $email)) {
                return error('Invalid owner e-mail address');
            }

            $status = $this->query('majordomo_create_mailing_list_backend',
                $list,
                $password,
                $email,
                $domain
            );

            if ($status instanceof Exception) {
                return $status;
            }

            $this->email_add_alias($list, $domain, $list . '+' . $domain);
            $this->email_add_alias($list . '-approval', $domain, $email);
            $this->email_add_alias($list . '-owner', $domain, $email);
            $this->email_add_alias('owner-' . $list, $domain, $list . '-owner');
            $this->email_add_alias($list . '-request', $domain, $list . '-request+' . $domain);

            if (!$this->email_address_exists('majordomo-owner', $domain)) {
                $this->email_add_alias('majordomo-owner', $domain, $email);
            }
            if (!$this->email_address_exists('majordomo', $domain)) {
                $this->email_add_alias('majordomo', $domain, 'majordomo+' . $domain);
            }

            return true;
        }

        public function set_mailing_list_users($list, $members)
        {
            if (!IS_CLI) {
                if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
                    return error("`%s': invalid list name", $list);
                }

                if (is_array($members)) {
                    $members = join("\n", $members);
                }

                return $this->query('majordomo_set_mailing_list_users', $list, $members);
            }

            if (!file_exists($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
                return error("list `%s' does not exist", $list);
            }

            // ensure list ends in newline
            $members = trim($members) . "\n";
            $listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
            file_put_contents("${listHome}/lists/${list}", $members);
            return Filesystem::chogp("${listHome}/lists/${list}", self::MAJORDOMO_SETUID, $this->group_id, 0644);
        }

        public function create_mailing_list_backend($list, $password, $email, $domain)
        {
            $prefix = $this->domain_fs_path();

            if (file_exists($prefix . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
                return error("list `%s' already exists", $list);
            }

            /** check that we have the very basic majordomo mapping */
            $ret = \Util_Process_Safe::exec('/usr/sbin/postalias -q majordomo+%s %s', $domain, Postfix::getAliasesPath(), [0, 1]);

            if ($ret['return'] === 1) {
                // alias not found, add it
                $aliasPath = Postfix::getAliasesPath();
                file_put_contents(
                    $aliasPath,
                    trim(file_get_contents($aliasPath)) . "\n" .
                    'majordomo+' . $domain . ': "| env HOME=/usr/lib/majordomo MAJORDOMO_CF=' .
                    $prefix . '/etc/majordomo-' . $domain . '.cf  /usr/lib/majordomo/majordomo"'
                );
                // delete in case it was replicated by an alias addition
                $this->email_remove_alias('majordomo', $domain);
                $this->email_add_alias('majordomo', $domain, 'majordomo+' . $domain);
                $proc = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext());
                // let this run independently
                if (version_compare(platform_version(), '7.5', '>=')) {
                    $svc = 'mlist';
                } else {
                    $svc = 'majordomo';
                }
                $proc->setConfig($svc, 'enabled', 1);
                $proc->edit();
            }
            if (!file_exists($prefix . '/etc/majordomo.cf')) {
                // @TODO move to templates/
                (new \Opcenter\Provisioning\ConfigurationWriter('majordomo.majordomo-cf', \Opcenter\SiteConfiguration::shallow($this->getAuthContext())))->
                    write($prefix . '/etc/majordomo.cf');
                Filesystem::chogp($prefix . '/etc/majordomo.cf', 0, 0);
            }
            file_put_contents($prefix . '/etc/majordomo-' . $domain . '.cf',
                preg_replace('/^\s*\$whereami.+$/m', '$whereami = "' . $domain . '";',
                    file_get_contents($prefix . '/etc/majordomo.cf')));
            Filesystem::chogp($prefix . "/etc/majordomo-${domain}.cf", 0, 0);
            if (!file_exists($listHome = Majordomo::bindTo($prefix)->getListHome())) {
                Filesystem::mkdir($listHome, self::MAJORDOMO_SETUID, $this->group_id);
                \Util_Process_Safe::exec('setfacl -d -m user:%d:7 %s', $this->user_id, $listHome);
            }

            foreach (array('archives', 'digest', 'lists', 'OLDLOGS', 'tmp') as $dir) {
                if (!file_exists("${listHome}/${dir}")) {
                    mkdir("${listHome}/${dir}");
                }
                Filesystem::chogp("${listHome}/${dir}", self::MAJORDOMO_SETUID, $this->group_id, 02771) &&
                    \Util_Process_Safe::exec('setfacl -m user:postfix:7 -d -m user:%s:7 %s/%s', self::MAJORDOMO_SETUID, $listHome, $dir);
            }
            $aliasPath = Postfix::getAliasesPath();
            file_put_contents(
                $aliasPath,
                trim(file_get_contents($aliasPath)) . "\n" .
                $list . '+' . $domain . ': "|  env HOME=/usr/lib/majordomo /usr/lib/majordomo/wrapper resend -C ' . $prefix . '/etc/majordomo-' . $domain . '.cf -l ' . $list . ' -h ' . $domain . ' ' . $list . '-outgoing+' . $domain . '"' . "\n" .
                $list . '-outgoing+' . $domain . ': :include:' . $listHome . '/lists/' . $list . "\n" .
                $list . '-request+' . $domain . ': "| env HOME=/usr/lib/majordomo MAJORDOMO_CF=' . $prefix . '/etc/majordomo-' . $domain . '.cf  /usr/lib/majordomo/request-answer ' . $list . ' -h ' . $domain . '"' . "\n"
            );

            // add aliases
            Util_Process::exec('/usr/sbin/postalias -w %s', $aliasPath);
            foreach (array($list, $list . '.config', $list . '.intro', $list . '.info') as $file) {
                Filesystem::touch("${listHome}/lists/${file}", self::MAJORDOMO_SETUID, $this->group_id);
            }
            file_put_contents("${listHome}/lists/${list}", $email . "\n");
            file_put_contents("${listHome}/lists/${list}.config",
                $this->change_configuration_options(array(
                    'admin_passwd'  => $password,
                    'resend_host'   => $domain,
                    'restrict_post' => $list,
                    'sender'        => 'owner-' . $list
                )));

            chmod($listHome, 0755);
            chmod("${listHome}/lists", 02751);
            chmod("${listHome}/lists/${list}", 0644);
            Util_Process_Safe::exec('setfacl -d -m user:%s:7 -m user:postfix:7 %s/*',
                self::MAJORDOMO_SETUID,
                $listHome
            );
            Util_Process_Safe::exec('setfacl -m user:%d:7 %s/lists/%s*',
                $this->user_id,
                $listHome,
                $list
            );
            Util_Process_Safe::exec('setfacl -R -m user:%s:7 -m user:postfix:7 %s',
                self::MAJORDOMO_SETUID,
                $listHome
            );

            return true;
        }

        public function change_configuration_options(array $options)
        {
            $configuration = $this->majordomo_skeleton;
            foreach ($options as $option => $value) {
                if (!isset($configuration[$option])) {
                    continue;
                }
                if ($configuration[$option]['type'] == enum) {
                    $configuration[$option]['value'] = in_array($value, $configuration[$option]['values']) ?
                        $value :
                        (isset($configuration[$option]['default']) ? $configuration[$option]['default'] : '');
                } else {
                    $configuration[$option]['value'] = $value;
                }
            }

            return $this->generate_configuration($configuration);
        }

        public function generate_configuration(array $config)
        {
            $configuration = $this->majordomo_preamble;
            foreach ($config as $opt_name => $opt_params) {

                $configuration .= wordwrap('# ' . $opt_params['help'], 72, "\n# ") . "\n" . $opt_name;
                if ($opt_params['type'] == text) {
                    $configuration .= " << END \n" . (isset($opt_params['value']) ? $opt_params['value'] : (isset($opt_params['default']) ? $opt_params['default'] : '')) . "\n" . 'END';
                } else {
                    $configuration .= ' = ';
                    if ($opt_params['type'] == bool) {
                        $configuration .= (isset($opt_params['value']) ? ($opt_params['value'] ? 'yes' : 'no') : ((isset($opt_params['default']) && $opt_params['default']) ? 'yes' : 'no'));
                    } else {
                        $configuration .= (isset($opt_params['value']) ? $opt_params['value'] : (isset($opt_params['default']) ? $opt_params['default'] : ''));
                    }
                }
                $configuration .= "\n\n";

            }

            return $configuration;

        }

        public function load_configuration_options($list)
        {
            return $this->_parse_configuration($this->file_get_file_contents(Majordomo::MAILING_LIST_HOME . '/lists/' . $list . '.config'));
        }

        private function _parse_configuration($text)
        {
            if (!preg_match_all(Regex::MAJORDOMO_CONFIG_ENTRY, $text, $matches, PREG_SET_ORDER)) {
                return false;
            }
            $base = $this->majordomo_skeleton;
            foreach ($matches as $match => $value) {
                if (isset($base[$value[1]])) {
                    $base[$value[1]]['value'] = trim(($base[$value[1]]['type'] == text) ? str_replace('END', '',
                        $value[2]) : $value[2]);
                }
            }

            return array_merge($base, array_intersect_key($base, $base));
        }

        public function save_configuration_options($list, $data)
        {
            // FIXME: we lose the 0 otherwise, which is significant
            return $this->file_put_file_contents(Majordomo::MAILING_LIST_HOME . "/lists/${list}.config", $data, true) &&
                $this->file_chmod(Majordomo::MAILING_LIST_HOME . "/lists/${list}.config", 644);
        }

        public function _delete()
        {
            if (!$this->enabled()) {
                return true;
            }
            foreach ($this->list_mailing_lists() as $list) {
                $this->delete_mailing_list($list);
            }
        }

        public function list_mailing_lists()
        {
            if (!IS_CLI) {
                return $this->query('majordomo_list_mailing_lists');
            }

            $entries = array();
            $listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
            if (!file_exists("${listHome}/lists")) {
                return $entries;
            }
            $dh = dir("${listHome}/lists");
            while (false !== ($entry = $dh->read())) {
                /* should check .config/.info/.intro/.auto */
                if (false !== strpos($entry, '.')) {
                    continue;
                }
                $entries[] = $entry;
            }
            $dh->close();

            return $entries;
        }

        public function delete_mailing_list($list)
        {
            if (!IS_CLI) {
                return $this->query('majordomo_delete_mailing_list', $list);
            }

            $list = trim(strtolower($list));
            if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
                return error('Invalid list name');
            } else {
                if (!$this->mailing_list_exists($list)) {
                    return error("mailing list `%s' does not exist", $list);
                }
            }

            $domain = $this->get_domain_from_list_name($list);
            $listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome() . '/lists';
            foreach (array($list, $list . '.config', $list . '.intro', $list . '.info') as $file) {
                $path = "${listHome}/${file}";
                if (file_exists($path)) {
                    unlink($path);
                }
            }

            // that was the last mailing list
            $moreLists = $this->mailing_lists_exist();

            if (!$moreLists) {
                $this->email_remove_alias('majordomo', $domain);
            }
            $lines = [];
            foreach (explode("\n", file_get_contents(Postfix::getAliasesPath())) as $line) {
                if (preg_match('!' . $list . '(?:-outgoing|-request)?\+' . $domain . ':!', $line)) {
                    continue;
                } else {
                    if (!$moreLists && preg_match('!majordomo\+' . $domain . ':!', $line)) {
                        continue;
                    }
                }
                $lines[] = $line;
            }

            $this->email_remove_alias($list, $domain);
            $this->email_remove_alias($list . '-approval', $domain);
            $this->email_remove_alias($list . '-owner', $domain);
            $this->email_remove_alias('owner-' . $list, $domain);
            $this->email_remove_alias($list . '-request', $domain);


            file_put_contents(Postfix::getAliasesPath(), join("\n", $lines));
            Util_Process::exec('postalias -r %s', Postfix::getAliasesPath());

            return true;

        }

        public function mailing_list_exists($list)
        {
            if (!IS_CLI) {
                return $this->query('majordomo_mailing_list_exists', $list);
            }

            return file_exists(Majordomo::bindTo($this->domain_fs_path())->getListHome() . "/lists/${list}.config");
        }

        public function get_domain_from_list_name($list)
        {
            if (!IS_CLI) {
                return $this->query('majordomo_get_domain_from_list_name', $list);
            }

            if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
                return error('Invalid list ' . $list);
            }

            $listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
            if (!file_exists("${listHome}/lists/${list}")) {
                return error($list . ' does not exist');
            }

            $file = "${listHome}/lists/${list}.config";
            if (!file_exists($file)) {
                return null;
            } else if (preg_match('/^\s*resend_host\s*=[ \t]*(\S+)/m', file_get_contents($file), $domain)) {
                $domain = $domain[1];
            } else {
                $domain = $this->getServiceValue('siteinfo', 'domain');
            }

            return $domain;
        }

        // currently handled by create_mailing_list

        /**
         * @return bool At least one mailing list exists
         */
        public function mailing_lists_exist()
        {
            if (!IS_CLI) {
                return $this->query('majordomo_mailing_lists_exist');
            }

            $listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
            if (!file_exists("${listHome}/lists")) {
                return false;
            }

            $glob = glob("${listHome}/lists");

            return sizeof($glob) > 1;

        }

        public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
        {
            return true;
        }

        public function _create()
        {
            // TODO: Implement _create() method.
        }

        public function _edit()
        {
            // TODO: Implement _edit() method.
        }

        public function _create_user(string $user)
        {
            // TODO: Implement _create_user() method.
        }

        public function _delete_user(string $user)
        {
            // TODO: Implement _delete_user() method.
        }

        public function _edit_user(string $userold, string $usernew, array $oldpwd)
        {
            // TODO: Implement _edit_user() method.
        }


    }