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: 
<?php declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    use Module\Skeleton\Contracts\Proxied;
    use Opcenter\Contracts\Hookable;
    use Opcenter\Mail\Services\Rspamd;

    /**
     *  Control group interfacing
     *
     * @package core
     */
    class Spamfilter_Module extends Module_Skeleton implements Hookable, Proxied
    {
        const DEPENDENCY_MAP = [
            'mail'
        ];

        const MAILFILTER_FILE = '/etc/maildroprc';
        const THRESHOLD_VAR = 'DELETE_THRESHOLD';

        protected $exportedFunctions = [
            '*'                       => PRIVILEGE_SITE,
            'get_threshold'           => PRIVILEGE_SITE|PRIVILEGE_USER,
            'set_controller_password' => PRIVILEGE_ADMIN
        ];

        public function _proxy(): \Module_Skeleton
        {
            return $this;
        }

        public function get_provider(): string
        {
            return $this->getServiceValue('spamfilter', 'provider', MAIL_SPAM_FILTER);
        }

        /**
         * Get configured deletion threshold
         *
         * @return int
         */
        public function get_delivery_threshold(): float
        {
            $filter = $this->domain_fs_path(self::MAILFILTER_FILE);
            if (!file_exists($filter)) {
                return $this->get_default_delivery_threshold();
            }

            $contents = file_get_contents($filter);
            if (!preg_match('/^\s*' . static::THRESHOLD_VAR . '\s*=\s*([\'"]?)([\d\-.]+)\1$/m', $contents, $matches)) {
                return $this->get_default_delivery_threshold();
            }

            return (float)$matches[2];
        }

        /**
         * Set account-wide spam threshold
         *
         * @param float $score deletion threshold
         * @return bool
         */
        public function set_delivery_threshold(float $score): bool
        {
            $filter = $this->domain_fs_path(static::MAILFILTER_FILE);
            if (!file_exists($filter)) {
                return error("File `%s' does not exist", static::MAILFILTER_FILE);
            }

            if ($score < ($default = $this->get_default_delivery_threshold())) {
                warn('Spam delivery threshold less than recommended default %.2f', $default);
            }

            if ($score > Rspamd::REJECTION_THRESHOLD && $this->get_provider() === 'rspamd') {
                return error('Value %f cannot exceed rejection threshold %d', $score, Rspamd::REJECTION_THRESHOLD);
            }

            $contents = file_get_contents($filter);
            $count = 0;
            $contents = preg_replace_callback('/^(\s*' . static::THRESHOLD_VAR . '\s*=\s*)([\'"]?)[\d\-.]+\2$/m', static function ($m) use ($score) {
                return $m[1] . $score;
            }, $contents, -1, $count);

            if ($count === 0) {
                return error('Threshold var %s missing - is filter corrupt?', static::THRESHOLD_VAR);
            }

            return $this->file_put_file_contents(static::MAILFILTER_FILE, $contents);
        }

        /**
         * Get spam filter account-wide deletion threshold
         *
         * @return int
         */
        public function get_default_delivery_threshold(): int
        {
            switch ($this->get_provider()) {
                case 'spamassassin':
                    return 7;
                case 'rspamd':
                    return 25;
            }

            return 999;
        }

        public function _create()
        {

        }

        public function _delete()
        {

        }

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

        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.
        }

        public function set_controller_password(string $value) {
            if (!IS_CLI) {
                return $this->query('spamfilter_set_controller_password');
            }
            if (!\Opcenter\Auth\Password::strong($value)) {
                return error('Supplied controller password is weak');
            }

            Rspamd::setPassword($value);
            $ctx = $this->getAuthContext();
            $prefs = \Preferences::factory($ctx);
            $prefs->unlock($this->getApnscpFunctionInterceptor());
            array_set($prefs, Rspamd::ADMIN_PREFKEY, $value);
            $prefs->sync();
        }

        public function _housekeeping() {
            if (!Rspamd::present()) {
                return;
            }

            if ($pass = Rspamd::getPassword()) {
                $endpoint = array_get(new Rspamd\Configuration('worker-controller.inc', 'r'), 'bind_socket');
                if (!$endpoint) {
                    return;
                }
                $opts = [
                    'http' => [
                        'header' => 'Password: ' . urlencode($pass)
                    ]
                ];
                $resp = @file_get_contents(
                    "http://${endpoint}/auth",
                    false,
                    stream_context_create($opts)
                );

                if ($resp && array_get((array)json_decode($resp, true), 'auth') === 'ok') {
                    // works OK
                    return;
                }
            }

            $pass = \Opcenter\Auth\Password::generate(24);
            return $this->set_controller_password($pass);
        }
    }