1: | <?php declare(strict_types=1); |
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
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: | |
21: | |
22: | |
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: | |
51: | |
52: | |
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: | |
71: | |
72: | |
73: | |
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: | |
105: | |
106: | |
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: | |
134: | } |
135: | |
136: | public function _create_user(string $user) |
137: | { |
138: | |
139: | } |
140: | |
141: | public function _delete_user(string $user) |
142: | { |
143: | |
144: | } |
145: | |
146: | public function _edit_user(string $userold, string $usernew, array $oldpwd) |
147: | { |
148: | |
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: | |
190: | return; |
191: | } |
192: | } |
193: | |
194: | $pass = \Opcenter\Auth\Password::generate(24); |
195: | return $this->set_controller_password($pass); |
196: | } |
197: | } |