1: <?php declare(strict_types=1);
2: /**
3: * +------------------------------------------------------------+
4: * | apnscp |
5: * +------------------------------------------------------------+
6: * | Copyright (c) Apis Networks |
7: * +------------------------------------------------------------+
8: * | Licensed under Artistic License 2.0 |
9: * +------------------------------------------------------------+
10: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
11: * +------------------------------------------------------------+
12: */
13:
14: use Module\Skeleton\Contracts\Hookable;
15: use Module\Skeleton\Contracts\Proxied;
16: use Opcenter\Admin\Settings\Mail\SpamDeletionThreshold;
17: use Opcenter\Mail\Services\Rspamd;
18:
19: /**
20: * Control group interfacing
21: *
22: * @package core
23: */
24: class Spamfilter_Module extends Module_Skeleton implements Hookable, Proxied
25: {
26: const DEPENDENCY_MAP = [
27: 'mail'
28: ];
29:
30: const MAILFILTER_FILE = '/etc/maildroprc';
31: const THRESHOLD_VAR = 'DELETE_THRESHOLD';
32:
33: protected $exportedFunctions = [
34: '*' => PRIVILEGE_SITE,
35: 'get_threshold' => PRIVILEGE_SITE|PRIVILEGE_USER,
36: 'set_controller_password' => PRIVILEGE_ADMIN
37: ];
38:
39: public function _proxy(): \Module_Skeleton
40: {
41: return $this;
42: }
43:
44: public function get_provider(): string
45: {
46: return $this->getServiceValue('spamfilter', 'provider', MAIL_SPAM_FILTER);
47: }
48:
49: /**
50: * Get configured deletion threshold
51: *
52: * @return int
53: */
54: public function get_delivery_threshold(): float
55: {
56: $filter = $this->domain_fs_path(self::MAILFILTER_FILE);
57: if (!file_exists($filter)) {
58: return $this->get_default_delivery_threshold();
59: }
60:
61: $contents = file_get_contents($filter);
62: if (!preg_match('/^\s*' . static::THRESHOLD_VAR . '\s*=\s*([\'"]?)([\d\-.]+)\1$/m', $contents, $matches)) {
63: return $this->get_default_delivery_threshold();
64: }
65:
66: return (float)$matches[2];
67: }
68:
69: /**
70: * Set account-wide spam threshold
71: *
72: * @param float $score deletion threshold
73: * @return bool
74: */
75: public function set_delivery_threshold(float $score): bool
76: {
77: $filter = $this->domain_fs_path(static::MAILFILTER_FILE);
78: if (!file_exists($filter)) {
79: return error("File `%s' does not exist", static::MAILFILTER_FILE);
80: }
81:
82: if ($score < ($default = $this->get_default_delivery_threshold())) {
83: warn('Spam delivery threshold less than recommended default %.2f', $default);
84: }
85:
86: if ($score > Rspamd::REJECTION_THRESHOLD && $this->get_provider() === 'rspamd') {
87: return error('Value %f cannot exceed rejection threshold %d', $score, Rspamd::REJECTION_THRESHOLD);
88: }
89:
90: $contents = file_get_contents($filter);
91: $count = 0;
92: $contents = preg_replace_callback('/^(\s*' . static::THRESHOLD_VAR . '\s*=\s*)([\'"]?)[\d\-.]+\2$/m', static function ($m) use ($score) {
93: return $m[1] . $score;
94: }, $contents, -1, $count);
95:
96: if ($count === 0) {
97: return error('Threshold var %s missing - is filter corrupt?', static::THRESHOLD_VAR);
98: }
99:
100: return $this->file_put_file_contents(static::MAILFILTER_FILE, $contents);
101: }
102:
103: /**
104: * Get spam filter account-wide deletion threshold
105: *
106: * @return int
107: */
108: public function get_default_delivery_threshold(): int
109: {
110: if (!IS_CLI) {
111: return $this->query('spamfilter_get_default_delivery_threshold');
112: }
113: return (new SpamDeletionThreshold)->get() ?: 999;
114: }
115:
116: public function _create()
117: {
118:
119: }
120:
121: public function _delete()
122: {
123:
124: }
125:
126: public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
127: {
128: return true;
129: }
130:
131: public function _edit()
132: {
133: // TODO: Implement _edit() method.
134: }
135:
136: public function _create_user(string $user)
137: {
138: // TODO: Implement _create_user() method.
139: }
140:
141: public function _delete_user(string $user)
142: {
143: // TODO: Implement _delete_user() method.
144: }
145:
146: public function _edit_user(string $userold, string $usernew, array $oldpwd)
147: {
148: // TODO: Implement _edit_user() method.
149: }
150:
151: public function set_controller_password(string $value) {
152: if (!IS_CLI) {
153: return $this->query('spamfilter_set_controller_password');
154: }
155: if (!\Opcenter\Auth\Password::strong($value)) {
156: return error('Supplied controller password is weak');
157: }
158:
159: Rspamd::setPassword($value);
160: $ctx = $this->getAuthContext();
161: $prefs = \Preferences::factory($ctx);
162: $prefs->unlock($this->getApnscpFunctionInterceptor());
163: array_set($prefs, Rspamd::ADMIN_PREFKEY, $value);
164: $prefs->sync();
165: }
166:
167: public function _housekeeping() {
168: if (!Rspamd::present()) {
169: return;
170: }
171:
172: if ($pass = Rspamd::getPassword()) {
173: $endpoint = array_get(new Rspamd\Configuration('worker-controller.inc', 'r'), 'bind_socket');
174: if (!$endpoint) {
175: return;
176: }
177: $opts = [
178: 'http' => [
179: 'header' => 'Password: ' . urlencode($pass)
180: ]
181: ];
182: $resp = @file_get_contents(
183: "http://{$endpoint}/auth",
184: false,
185: stream_context_create($opts)
186: );
187:
188: if ($resp && array_get((array)json_decode($resp, true), 'auth') === 'ok') {
189: // works OK
190: return;
191: }
192: }
193:
194: $pass = \Opcenter\Auth\Password::generate(24);
195: return $this->set_controller_password($pass);
196: }
197: }