blob: 6418945c56e11ea071f698e5fb6c60961efd2bd4 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2use PHPMailer\PHPMailer\PHPMailer;
3use PHPMailer\PHPMailer\SMTP;
4use PHPMailer\PHPMailer\Exception;
5function is_valid_regex($exp) {
6 return @preg_match($exp, '') !== false;
7}
8function isset_has_content($var) {
9 if (isset($var) && $var != "") {
10 return true;
11 }
12 else {
13 return false;
14 }
15}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020016function readable_random_string($length = 8) {
17 $string = '';
18 $vowels = array('a', 'e', 'i', 'o', 'u');
19 $consonants = array('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z');
20 $max = $length / 2;
21 for ($i = 1; $i <= $max; $i++) {
22 $string .= $consonants[rand(0,19)];
23 $string .= $vowels[rand(0,4)];
24 }
25 return $string;
26}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010027// Validates ips and cidrs
28function valid_network($network) {
29 if (filter_var($network, FILTER_VALIDATE_IP)) {
30 return true;
31 }
32 $parts = explode('/', $network);
33 if (count($parts) != 2) {
34 return false;
35 }
36 $ip = $parts[0];
37 $netmask = $parts[1];
38 if (!preg_match("/^\d+$/", $netmask)){
39 return false;
40 }
41 $netmask = intval($parts[1]);
42 if ($netmask < 0) {
43 return false;
44 }
45 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
46 return $netmask <= 32;
47 }
48 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
49 return $netmask <= 128;
50 }
51 return false;
52}
53function valid_hostname($hostname) {
54 return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
55}
56// Thanks to https://stackoverflow.com/a/49373789
57// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
58function ip_acl($ip, $networks) {
59 foreach($networks as $network) {
60 if (filter_var($network, FILTER_VALIDATE_IP)) {
61 if ($ip == $network) {
62 return true;
63 }
64 else {
65 continue;
66 }
67 }
68 $ipb = inet_pton($ip);
69 $iplen = strlen($ipb);
70 if (strlen($ipb) < 4) {
71 continue;
72 }
73 $ar = explode('/', $network);
74 $ip1 = $ar[0];
75 $ip1b = inet_pton($ip1);
76 $ip1len = strlen($ip1b);
77 if ($ip1len != $iplen) {
78 continue;
79 }
80 if (count($ar)>1) {
81 $bits=(int)($ar[1]);
82 }
83 else {
84 $bits = $iplen * 8;
85 }
86 for ($c=0; $bits>0; $c++) {
87 $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
88 if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) {
89 continue 2;
90 }
91 $bits-=8;
92 }
93 return true;
94 }
95 return false;
96}
97function hash_password($password) {
98 // default_pass_scheme is determined in vars.inc.php (or corresponding local file)
99 // in case default pass scheme is not defined, falling back to BLF-CRYPT.
100 global $default_pass_scheme;
101 $pw_hash = NULL;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200102 // support pre-hashed passwords
103 if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
104 $pw_hash = $password;
105 }
106 else {
107 switch (strtoupper($default_pass_scheme)) {
108 case "SSHA":
109 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
110 $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
111 break;
112 case "SSHA256":
113 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
114 $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
115 break;
116 case "SSHA512":
117 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
118 $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
119 break;
120 case "BLF-CRYPT":
121 default:
122 $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
123 break;
124 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100125 }
126 return $pw_hash;
127}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200128function password_complexity($_action, $_data = null) {
129 global $redis;
130 global $lang;
131 switch ($_action) {
132 case 'edit':
133 if ($_SESSION['mailcow_cc_role'] != "admin") {
134 $_SESSION['return'][] = array(
135 'type' => 'danger',
136 'log' => array(__FUNCTION__, $_action, $_data),
137 'msg' => 'access_denied'
138 );
139 return false;
140 }
141 $is_now = password_complexity('get');
142 if (!empty($is_now)) {
143 $length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
144 $chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
145 $lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
146 $special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
147 $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
148 }
149 try {
150 $redis->hMSet('PASSWD_POLICY', [
151 'length' => $length,
152 'chars' => $chars,
153 'special_chars' => $special_chars,
154 'lowerupper' => $lowerupper,
155 'numbers' => $numbers
156 ]);
157 }
158 catch (RedisException $e) {
159 $_SESSION['return'][] = array(
160 'type' => 'danger',
161 'log' => array(__FUNCTION__, $_action, $_data),
162 'msg' => array('redis_error', $e)
163 );
164 return false;
165 }
166 $_SESSION['return'][] = array(
167 'type' => 'success',
168 'log' => array(__FUNCTION__, $_action, $_data),
169 'msg' => 'password_policy_saved'
170 );
171 break;
172 case 'get':
173 try {
174 $length = $redis->hGet('PASSWD_POLICY', 'length');
175 $chars = $redis->hGet('PASSWD_POLICY', 'chars');
176 $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
177 $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
178 $numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
179 return array(
180 'length' => $length,
181 'chars' => $chars,
182 'special_chars' => $special_chars,
183 'lowerupper' => $lowerupper,
184 'numbers' => $numbers
185 );
186 }
187 catch (RedisException $e) {
188 $_SESSION['return'][] = array(
189 'type' => 'danger',
190 'log' => array(__FUNCTION__, $_action, $_data),
191 'msg' => array('redis_error', $e)
192 );
193 return false;
194 }
195 return false;
196 break;
197 case 'html':
198 $policies = password_complexity('get');
199 foreach ($policies as $name => $value) {
200 if ($value != 0) {
201 $policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
202 }
203 }
204 return '<p class="help-block small">- ' . implode('<br>- ', (array)$policy_text) . '</p>';
205 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100206 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200207}
208function password_check($password1, $password2) {
209 $password_complexity = password_complexity('get');
210
211 if (empty($password1) || empty($password2)) {
212 $_SESSION['return'][] = array(
213 'type' => 'danger',
214 'log' => array(__FUNCTION__, $_action, $_type),
215 'msg' => 'password_complexity'
216 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100217 return false;
218 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200219
220 if ($password1 != $password2) {
221 $_SESSION['return'][] = array(
222 'type' => 'danger',
223 'log' => array(__FUNCTION__, $_action, $_type),
224 'msg' => 'password_mismatch'
225 );
226 return false;
227 }
228
229 $given_password['length'] = strlen($password1);
230 $given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
231 $given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
232 $given_password['numbers'] = preg_match('/\d/', $password1);
233 $lower = strlen(preg_replace("/[^a-z]/", '', $password1));
234 $upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
235 $given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
236
237 if (
238 ($given_password['length'] < $password_complexity['length']) ||
239 ($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
240 ($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
241 ($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
242 ($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
243 ) {
244 $_SESSION['return'][] = array(
245 'type' => 'danger',
246 'log' => array(__FUNCTION__, $_action, $_type),
247 'msg' => 'password_complexity'
248 );
249 return false;
250 }
251
252 return true;
253}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100254function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200255 global $pdo;
256 global $redis;
257 $sasl_limit_days = intval($sasl_limit_days);
258 switch ($action) {
259 case 'get':
260 if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100261 $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service`, `app_password`, MAX(`app_passwd`.`name`) as `app_password_name` FROM `sasl_log`
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200262 LEFT OUTER JOIN `app_passwd` on `sasl_log`.`app_password` = `app_passwd`.`id`
263 WHERE `username` = :username
264 AND HOUR(TIMEDIFF(NOW(), `datetime`)) < :sasl_limit_days
265 GROUP BY `real_rip`, `service`, `app_password`
266 ORDER BY `datetime` DESC;');
267 $stmt->execute(array(':username' => $username, ':sasl_limit_days' => ($sasl_limit_days * 24)));
268 $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC);
269 foreach ($sasl as $k => $v) {
270 if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
271 $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')';
272 }
273 elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
274 try {
275 $sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
276 }
277 catch (RedisException $e) {
278 $_SESSION['return'][] = array(
279 'type' => 'danger',
280 'log' => array(__FUNCTION__, $_action, $_data_log),
281 'msg' => array('redis_error', $e)
282 );
283 return false;
284 }
285 if (!$sasl[$k]['location']) {
286 $curl = curl_init();
287 curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
288 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
289 curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
290 curl_setopt($curl, CURLOPT_TIMEOUT, 5);
291 $ip_data = curl_exec($curl);
292 if (!curl_errno($curl)) {
293 $ip_data_array = json_decode($ip_data, true);
294 if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
295 $sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
296 try {
297 $redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
298 }
299 catch (RedisException $e) {
300 $_SESSION['return'][] = array(
301 'type' => 'danger',
302 'log' => array(__FUNCTION__, $_action, $_data_log),
303 'msg' => array('redis_error', $e)
304 );
305 curl_close($curl);
306 return false;
307 }
308 }
309 }
310 curl_close($curl);
311 }
312 }
313 }
314 }
315 else {
316 $sasl = array();
317 }
318 if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
319 $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
320 WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
321 AND JSON_EXTRACT(`call`, "$[1]") = :username
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100322 AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET :offset');
323 $stmt->execute(array(
324 ':username' => $username,
325 ':offset' => $ui_offset
326 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200327 $ui = $stmt->fetch(PDO::FETCH_ASSOC);
328 }
329 else {
330 $ui = array();
331 }
332
333 return array('ui' => $ui, 'sasl' => $sasl);
334 break;
335 case 'reset':
336 if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
337 $stmt = $pdo->prepare('DELETE FROM `sasl_log`
338 WHERE `username` = :username');
339 $stmt->execute(array(':username' => $username));
340 }
341 if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
342 $stmt = $pdo->prepare('DELETE FROM `logs`
343 WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
344 AND JSON_EXTRACT(`call`, "$[1]") = :username
345 AND `type` = "success"');
346 $stmt->execute(array(':username' => $username));
347 }
348 return true;
349 break;
350 }
351
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100352}
353function flush_memcached() {
354 try {
355 $m = new Memcached();
356 $m->addServer('memcached', 11211);
357 $m->flush();
358 }
359 catch ( Exception $e ) {
360 // Dunno
361 }
362}
363function sys_mail($_data) {
364 if ($_SESSION['mailcow_cc_role'] != "admin") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200365 $_SESSION['return'][] = array(
366 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100367 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200368 'msg' => 'access_denied'
369 );
370 return false;
371 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100372 $excludes = $_data['mass_exclude'];
373 $includes = $_data['mass_include'];
374 $mailboxes = array();
375 $mass_from = $_data['mass_from'];
376 $mass_text = $_data['mass_text'];
377 $mass_html = $_data['mass_html'];
378 $mass_subject = $_data['mass_subject'];
379 if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200380 $_SESSION['return'][] = array(
381 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100382 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200383 'msg' => 'from_invalid'
384 );
385 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100386 }
387 if (empty($mass_subject)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200388 $_SESSION['return'][] = array(
389 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100390 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200391 'msg' => 'subject_empty'
392 );
393 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100394 }
395 if (empty($mass_text)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200396 $_SESSION['return'][] = array(
397 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100398 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200399 'msg' => 'text_empty'
400 );
401 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100402 }
403 $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
404 foreach ($domains as $domain) {
405 foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
406 $mailboxes[] = $mailbox;
407 }
408 }
409 if (!empty($includes)) {
410 $rcpts = array_intersect($mailboxes, $includes);
411 }
412 elseif (!empty($excludes)) {
413 $rcpts = array_diff($mailboxes, $excludes);
414 }
415 else {
416 $rcpts = $mailboxes;
417 }
418 if (!empty($rcpts)) {
419 ini_set('max_execution_time', 0);
420 ini_set('max_input_time', 0);
421 $mail = new PHPMailer;
422 $mail->Timeout = 10;
423 $mail->SMTPOptions = array(
424 'ssl' => array(
425 'verify_peer' => false,
426 'verify_peer_name' => false,
427 'allow_self_signed' => true
428 )
429 );
430 $mail->isSMTP();
431 $mail->Host = 'dovecot-mailcow';
432 $mail->SMTPAuth = false;
433 $mail->Port = 24;
434 $mail->setFrom($mass_from);
435 $mail->Subject = $mass_subject;
436 $mail->CharSet ="UTF-8";
437 if (!empty($mass_html)) {
438 $mail->Body = $mass_html;
439 $mail->AltBody = $mass_text;
440 }
441 else {
442 $mail->Body = $mass_text;
443 }
444 $mail->XMailer = 'MooMassMail';
445 foreach ($rcpts as $rcpt) {
446 $mail->AddAddress($rcpt);
447 if (!$mail->send()) {
448 $_SESSION['return'][] = array(
449 'type' => 'warning',
450 'log' => array(__FUNCTION__),
451 'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
452 );
453 }
454 $mail->ClearAllRecipients();
455 }
456 }
457 $_SESSION['return'][] = array(
458 'type' => 'success',
459 'log' => array(__FUNCTION__),
460 'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
461 );
462}
463function logger($_data = false) {
464 /*
465 logger() will be called as last function
466 To manually log a message, logger needs to be called like below.
467
468 logger(array(
469 'return' => array(
470 array(
471 'type' => 'danger',
472 'log' => array(__FUNCTION__),
473 'msg' => $err
474 )
475 )
476 ));
477
478 These messages will not be printed as alert box.
479 To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
480
481 $_SESSION['return'][] = array(
482 'type' => 'danger',
483 'log' => array(__FUNCTION__, $user, '*'),
484 'msg' => $err
485 );
486 */
487 global $pdo;
488 if (!$_data) {
489 $_data = $_SESSION;
490 }
491 if (!empty($_data['return'])) {
492 $task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
493 foreach ($_data['return'] as $return) {
494 $type = $return['type'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200495 $msg = null;
496 if (isset($return['msg'])) {
497 $msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
498 }
499 $call = null;
500 if (isset($return['log'])) {
501 $call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
502 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100503 if (!empty($_SESSION["dual-login"]["username"])) {
504 $user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
505 $role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
506 }
507 elseif (!empty($_SESSION['mailcow_cc_username'])) {
508 $user = $_SESSION['mailcow_cc_username'];
509 $role = $_SESSION['mailcow_cc_role'];
510 }
511 else {
512 $user = 'unauthenticated';
513 $role = 'unauthenticated';
514 }
515 // We cannot log when logs is missing...
516 try {
517 $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
518 (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
519 $stmt->execute(array(
520 ':type' => $type,
521 ':task' => $task,
522 ':call' => $call,
523 ':msg' => $msg,
524 ':user' => $user,
525 ':role' => $role,
526 ':remote' => get_remote_ip()
527 ));
528 }
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100529 catch (PDOException $e) {
530 # handle the exception here, as the exception handler function results in a white page
531 error_log($e->getMessage(), 0);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100532 }
533 }
534 }
535 else {
536 return true;
537 }
538}
539function hasDomainAccess($username, $role, $domain) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200540 global $pdo;
541 if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
542 return false;
543 }
544 if (empty($domain) || !is_valid_domain_name($domain)) {
545 return false;
546 }
547 if ($role != 'admin' && $role != 'domainadmin') {
548 return false;
549 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100550 if ($role == 'admin') {
551 $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
552 WHERE `domain` = :domain");
553 $stmt->execute(array(':domain' => $domain));
554 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
555 $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
556 WHERE `alias_domain` = :domain");
557 $stmt->execute(array(':domain' => $domain));
558 $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
559 if ($num_results != 0) {
560 return true;
561 }
562 }
563 elseif ($role == 'domainadmin') {
564 $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
565 WHERE (
566 `active`='1'
567 AND `username` = :username
568 AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
569 )");
570 $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
571 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
572 if (!empty($num_results)) {
573 return true;
574 }
575 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200576 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100577}
578function hasMailboxObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200579 global $pdo;
580 if (empty($username) || empty($role) || empty($object)) {
581 return false;
582 }
583 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
584 return false;
585 }
586 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
587 return false;
588 }
589 if ($username == $object) {
590 return true;
591 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100592 $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
593 $stmt->execute(array(':object' => $object));
594 $row = $stmt->fetch(PDO::FETCH_ASSOC);
595 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
596 return true;
597 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200598 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100599}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200600// does also verify mailboxes as a mailbox is a alias == goto
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100601function hasAliasObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200602 global $pdo;
603 if (empty($username) || empty($role) || empty($object)) {
604 return false;
605 }
606 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
607 return false;
608 }
609 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
610 return false;
611 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100612 $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
613 $stmt->execute(array(':object' => $object));
614 $row = $stmt->fetch(PDO::FETCH_ASSOC);
615 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
616 return true;
617 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200618 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100619}
620function pem_to_der($pem_key) {
621 // Need to remove BEGIN/END PUBLIC KEY
622 $lines = explode("\n", trim($pem_key));
623 unset($lines[count($lines)-1]);
624 unset($lines[0]);
625 return base64_decode(implode('', $lines));
626}
627function expand_ipv6($ip) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200628 $hex = unpack("H*hex", inet_pton($ip));
629 $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
630 return $ip;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100631}
632function generate_tlsa_digest($hostname, $port, $starttls = null) {
633 if (!is_valid_domain_name($hostname)) {
634 return "Not a valid hostname";
635 }
636 if (empty($starttls)) {
637 $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
638 $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
639 if (!$stream) {
640 $error_msg = isset($error_msg) ? $error_msg : '-';
641 return $error_nr . ': ' . $error_msg;
642 }
643 }
644 else {
645 $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
646 if (!$stream) {
647 return $error_nr . ': ' . $error_msg;
648 }
649 $banner = fread($stream, 512 );
650 if (preg_match("/^220/i", $banner)) { // SMTP
651 fwrite($stream,"HELO tlsa.generator.local\r\n");
652 fread($stream, 512);
653 fwrite($stream,"STARTTLS\r\n");
654 fread($stream, 512);
655 }
656 elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
657 fwrite($stream,"A1 STARTTLS\r\n");
658 fread($stream, 512);
659 }
660 elseif (preg_match("/^\+OK/", $banner)) { // POP3
661 fwrite($stream,"STLS\r\n");
662 fread($stream, 512);
663 }
664 elseif (preg_match("/^OK/m", $banner)) { // Sieve
665 fwrite($stream,"STARTTLS\r\n");
666 fread($stream, 512);
667 }
668 else {
669 return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
670 }
671 // Upgrade connection
672 stream_set_blocking($stream, true);
673 stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
674 stream_context_set_option($stream, 'ssl', 'verify_peer', false);
675 stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
676 stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
677 stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
678 stream_set_blocking($stream, false);
679 }
680 $params = stream_context_get_params($stream);
681 if (!empty($params['options']['ssl']['peer_certificate'])) {
682 $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
683 // We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
684 $key_data = openssl_pkey_get_details($key_resource)['key'];
685 return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
686 }
687 else {
688 return 'Error: Cannot read peer certificate';
689 }
690}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200691function alertbox_log_parser($_data) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100692 global $lang;
693 if (isset($_data['return'])) {
694 foreach ($_data['return'] as $return) {
695 // Get type
696 $type = $return['type'];
697 // If a lang[type][msg] string exists, use it as message
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200698 if (isset($return['type']) && isset($return['msg']) && !is_array($return['msg'])) {
699 if (isset($lang[$return['type']][$return['msg']])) {
700 $msg = $lang[$return['type']][$return['msg']];
701 }
702 else {
703 $msg = $return['msg'];
704 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100705 }
706 // If msg is an array, use first element as language string and run printf on it with remaining array elements
707 elseif (is_array($return['msg'])) {
708 $msg = array_shift($return['msg']);
709 $msg = vsprintf(
710 $lang[$return['type']][$msg],
711 $return['msg']
712 );
713 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100714 else {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200715 $msg = '-';
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100716 }
717 $log_array[] = array('msg' => $msg, 'type' => json_encode($type));
718 }
719 if (!empty($log_array)) {
720 return $log_array;
721 }
722 }
723 return false;
724}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200725function verify_salted_hash($hash, $password, $algo, $salt_length) {
726 // Decode hash
727 $dhash = base64_decode($hash);
728 // Get first n bytes of binary which equals a SSHA hash
729 $ohash = substr($dhash, 0, $salt_length);
730 // Remove SSHA hash from decoded hash to get original salt string
731 $osalt = str_replace($ohash, '', $dhash);
732 // Check single salted SSHA hash against extracted hash
733 if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
734 return true;
735 }
736 return false;
737}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100738function verify_hash($hash, $password) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200739 if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
740 $scheme = strtoupper($hash_array[1]);
741 $hash = $hash_array[2];
742 switch ($scheme) {
743 case "ARGON2I":
744 case "ARGON2ID":
745 case "BLF-CRYPT":
746 case "CRYPT":
747 case "DES-CRYPT":
748 case "MD5-CRYPT":
749 case "MD5":
750 case "SHA256-CRYPT":
751 case "SHA512-CRYPT":
752 return password_verify($password, $hash);
753
754 case "CLEAR":
755 case "CLEARTEXT":
756 case "PLAIN":
757 return $password == $hash;
758
759 case "LDAP-MD5":
760 $hash = base64_decode($hash);
761 return hash_equals(hash('md5', $password, true), $hash);
762
763 case "PBKDF2":
764 $components = explode('$', $hash);
765 $salt = $components[2];
766 $rounds = $components[3];
767 $hash = $components[4];
768 return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
769
770 case "PLAIN-MD4":
771 return hash_equals(hash('md4', $password), $hash);
772
773 case "PLAIN-MD5":
774 return md5($password) == $hash;
775
776 case "PLAIN-TRUNC":
777 $components = explode('-', $hash);
778 if (count($components) > 1) {
779 $trunc_len = $components[0];
780 $trunc_password = $components[1];
781
782 return substr($password, 0, $trunc_len) == $trunc_password;
783 } else {
784 return $password == $hash;
785 }
786
787 case "SHA":
788 case "SHA1":
789 case "SHA256":
790 case "SHA512":
791 // SHA is an alias for SHA1
792 $scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
793 $hash = base64_decode($hash);
794 return hash_equals(hash($scheme, $password, true), $hash);
795
796 case "SMD5":
797 return verify_salted_hash($hash, $password, 'md5', 16);
798
799 case "SSHA":
800 return verify_salted_hash($hash, $password, 'sha1', 20);
801
802 case "SSHA256":
803 return verify_salted_hash($hash, $password, 'sha256', 32);
804
805 case "SSHA512":
806 return verify_salted_hash($hash, $password, 'sha512', 64);
807
808 default:
809 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100810 }
811 }
812 return false;
813}
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100814function check_login($user, $pass, $app_passwd_data = false) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200815 global $pdo;
816 global $redis;
817 global $imap_server;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100818
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200819 if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100820 $_SESSION['return'][] = array(
821 'type' => 'danger',
822 'log' => array(__FUNCTION__, $user, '*'),
823 'msg' => 'malformed_username'
824 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200825 return false;
826 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100827
828 // Validate admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200829 $user = strtolower(trim($user));
830 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
831 WHERE `superadmin` = '1'
832 AND `active` = '1'
833 AND `username` = :user");
834 $stmt->execute(array(':user' => $user));
835 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
836 foreach ($rows as $row) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100837 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200838 if (verify_hash($row['password'], $pass)) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100839 // check for tfa authenticators
840 $authenticators = get_tfa($user);
841 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
842 // active tfa authenticators found, set pending user login
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100843 $_SESSION['pending_mailcow_cc_username'] = $user;
844 $_SESSION['pending_mailcow_cc_role'] = "admin";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100845 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100846 unset($_SESSION['ldelay']);
847 $_SESSION['return'][] = array(
848 'type' => 'info',
849 'log' => array(__FUNCTION__, $user, '*'),
850 'msg' => 'awaiting_tfa_confirmation'
851 );
852 return "pending";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100853 } else {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100854 unset($_SESSION['ldelay']);
855 // Reactivate TFA if it was set to "deactivate TFA for next login"
856 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
857 $stmt->execute(array(':user' => $user));
858 $_SESSION['return'][] = array(
859 'type' => 'success',
860 'log' => array(__FUNCTION__, $user, '*'),
861 'msg' => array('logged_in_as', $user)
862 );
863 return "admin";
864 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200865 }
866 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100867
868 // Validate domain admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200869 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
870 WHERE `superadmin` = '0'
871 AND `active`='1'
872 AND `username` = :user");
873 $stmt->execute(array(':user' => $user));
874 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
875 foreach ($rows as $row) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100876 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200877 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100878 // check for tfa authenticators
879 $authenticators = get_tfa($user);
880 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100881 $_SESSION['pending_mailcow_cc_username'] = $user;
882 $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100883 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100884 unset($_SESSION['ldelay']);
885 $_SESSION['return'][] = array(
886 'type' => 'info',
887 'log' => array(__FUNCTION__, $user, '*'),
888 'msg' => 'awaiting_tfa_confirmation'
889 );
890 return "pending";
891 }
892 else {
893 unset($_SESSION['ldelay']);
894 // Reactivate TFA if it was set to "deactivate TFA for next login"
895 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
896 $stmt->execute(array(':user' => $user));
897 $_SESSION['return'][] = array(
898 'type' => 'success',
899 'log' => array(__FUNCTION__, $user, '*'),
900 'msg' => array('logged_in_as', $user)
901 );
902 return "domainadmin";
903 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200904 }
905 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100906
907 // Validate mailbox user
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200908 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100909 INNER JOIN domain on mailbox.domain = domain.domain
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200910 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100911 AND `mailbox`.`active`='1'
912 AND `domain`.`active`='1'
913 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200914 $stmt->execute(array(':user' => $user));
915 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100916 if ($app_passwd_data['eas'] === true) {
917 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
918 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
919 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
920 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
921 AND `mailbox`.`active` = '1'
922 AND `domain`.`active` = '1'
923 AND `app_passwd`.`active` = '1'
924 AND `app_passwd`.`eas_access` = '1'
925 AND `app_passwd`.`mailbox` = :user");
926 $stmt->execute(array(':user' => $user));
927 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
928 }
929 elseif ($app_passwd_data['dav'] === true) {
930 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
931 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
932 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
933 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
934 AND `mailbox`.`active` = '1'
935 AND `domain`.`active` = '1'
936 AND `app_passwd`.`active` = '1'
937 AND `app_passwd`.`dav_access` = '1'
938 AND `app_passwd`.`mailbox` = :user");
939 $stmt->execute(array(':user' => $user));
940 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
941 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100942 foreach ($rows as $row) {
943 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200944 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100945 if (!array_key_exists("app_passwd_id", $row)){
946 // password is not a app password
947 // check for tfa authenticators
948 $authenticators = get_tfa($user);
949 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
950 $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
951 // authenticators found, init TFA flow
952 $_SESSION['pending_mailcow_cc_username'] = $user;
953 $_SESSION['pending_mailcow_cc_role'] = "user";
954 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
955 unset($_SESSION['ldelay']);
956 $_SESSION['return'][] = array(
957 'type' => 'success',
958 'log' => array(__FUNCTION__, $user, '*'),
959 'msg' => array('logged_in_as', $user)
960 );
961 return "pending";
962 } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
963 // no authenticators found, login successfull
964 // Reactivate TFA if it was set to "deactivate TFA for next login"
965 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
966 $stmt->execute(array(':user' => $user));
967
968 unset($_SESSION['ldelay']);
969 return "user";
970 }
971 } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
972 // password is a app password
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100973 $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
974 $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
975 $stmt->execute(array(
976 ':service' => $service,
977 ':app_id' => $row['app_passwd_id'],
978 ':username' => $user,
979 ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
980 ));
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100981
982 unset($_SESSION['ldelay']);
983 return "user";
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100984 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200985 }
986 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100987
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200988 if (!isset($_SESSION['ldelay'])) {
989 $_SESSION['ldelay'] = "0";
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100990 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
991 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200992 }
993 elseif (!isset($_SESSION['mailcow_cc_username'])) {
994 $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100995 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200996 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
997 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100998
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100999 $_SESSION['return'][] = array(
1000 'type' => 'danger',
1001 'log' => array(__FUNCTION__, $user, '*'),
1002 'msg' => 'login_failed'
1003 );
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001004
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001005 sleep($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001006 return false;
1007}
1008function formatBytes($size, $precision = 2) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001009 if(!is_numeric($size)) {
1010 return "0";
1011 }
1012 $base = log($size, 1024);
1013 $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
1014 if ($size == "0") {
1015 return "0";
1016 }
1017 return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001018}
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001019function update_sogo_static_view($mailbox = null) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001020 if (getenv('SKIP_SOGO') == "y") {
1021 return true;
1022 }
1023 global $pdo;
1024 global $lang;
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001025
1026 $mailbox_exists = false;
1027 if ($mailbox !== null) {
1028 // Check if the mailbox exists
1029 $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
1030 $stmt->execute(array(':mailbox' => $mailbox));
1031 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1032 if ($row){
1033 $mailbox_exists = true;
1034 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001035 }
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001036
1037 $query = "REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
1038 SELECT
1039 mailbox.username,
1040 mailbox.domain,
1041 mailbox.username,
1042 IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0',
1043 IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
1044 '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
1045 mailbox.name,
1046 mailbox.username,
1047 IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
1048 IFNULL(gda.ad_alias, ''),
1049 IFNULL(external_acl.send_as_acl, ''),
1050 mailbox.kind,
1051 mailbox.multiple_bookings
1052 FROM
1053 mailbox
1054 LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
1055 LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
1056 LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
1057 WHERE
1058 mailbox.active = '1'";
1059
1060 if ($mailbox_exists) {
1061 $query .= " AND mailbox.username = :mailbox";
1062 $stmt = $pdo->prepare($query);
1063 $stmt->execute(array(':mailbox' => $mailbox));
1064 } else {
1065 $query .= " GROUP BY mailbox.username";
1066 $stmt = $pdo->query($query);
1067 }
1068
1069 $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
1070
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001071 flush_memcached();
1072}
1073function edit_user_account($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001074 global $lang;
1075 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001076 $_data_log = $_data;
1077 !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
1078 !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
1079 !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
1080 $username = $_SESSION['mailcow_cc_username'];
1081 $role = $_SESSION['mailcow_cc_role'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001082 $password_old = $_data['user_old_pass'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001083 if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
1084 $_SESSION['return'][] = array(
1085 'type' => 'danger',
1086 'log' => array(__FUNCTION__, $_data_log),
1087 'msg' => 'access_denied'
1088 );
1089 return false;
1090 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001091 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1092 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001093 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001094 $stmt->execute(array(':user' => $username));
1095 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001096 if (!verify_hash($row['password'], $password_old)) {
1097 $_SESSION['return'][] = array(
1098 'type' => 'danger',
1099 'log' => array(__FUNCTION__, $_data_log),
1100 'msg' => 'access_denied'
1101 );
1102 return false;
1103 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001104 if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
1105 $password_new = $_data['user_new_pass'];
1106 $password_new2 = $_data['user_new_pass2'];
1107 if (password_check($password_new, $password_new2) !== true) {
1108 return false;
1109 }
1110 $password_hashed = hash_password($password_new);
1111 $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
1112 `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
1113 `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
1114 WHERE `username` = :username");
1115 $stmt->execute(array(
1116 ':password_hashed' => $password_hashed,
1117 ':username' => $username
1118 ));
1119 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001120 update_sogo_static_view();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001121 $_SESSION['return'][] = array(
1122 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001123 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001124 'msg' => array('mailbox_modified', htmlspecialchars($username))
1125 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001126}
1127function user_get_alias_details($username) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001128 global $pdo;
1129 global $lang;
1130 $data['direct_aliases'] = array();
1131 $data['shared_aliases'] = array();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001132 if ($_SESSION['mailcow_cc_role'] == "user") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001133 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001134 }
1135 if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
1136 return false;
1137 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001138 if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
1139 return false;
1140 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001141 $data['address'] = $username;
1142 $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
1143 WHERE `goto` REGEXP :username_goto
1144 AND `address` NOT LIKE '@%'
1145 AND `goto` != :username_goto2
1146 AND `address` != :username_address");
1147 $stmt->execute(array(
1148 ':username_goto' => '(^|,)'.$username.'($|,)',
1149 ':username_goto2' => $username,
1150 ':username_address' => $username
1151 ));
1152 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1153 while ($row = array_shift($run)) {
1154 $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001155 //$data['shared_aliases'][] = $row['shared_aliases'];
1156 }
1157
1158 $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
1159 WHERE `goto` = :username_goto
1160 AND `address` NOT LIKE '@%'
1161 AND `address` != :username_address");
1162 $stmt->execute(
1163 array(
1164 ':username_goto' => $username,
1165 ':username_address' => $username
1166 ));
1167 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1168 while ($row = array_shift($run)) {
1169 $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
1170 }
1171 $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
1172 LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
1173 WHERE `username` = :username ;");
1174 $stmt->execute(array(':username' => $username));
1175 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1176 while ($row = array_shift($run)) {
1177 if (empty($row['ad_alias'])) {
1178 continue;
1179 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001180 $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
1181 $data['alias_domains'][] = $row['alias_domain'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001182 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001183 $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001184 $stmt->execute(array(':username' => $username));
1185 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1186 while ($row = array_shift($run)) {
1187 $data['aliases_also_send_as'] = $row['send_as'];
1188 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001189 $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ''), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001190 $stmt->execute(array(':username' => $username));
1191 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1192 while ($row = array_shift($run)) {
1193 $data['aliases_send_as_all'] = $row['send_as'];
1194 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001195 $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '') as `address` FROM `alias` WHERE `goto` REGEXP :username AND `address` LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001196 $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
1197 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1198 while ($row = array_shift($run)) {
1199 $data['is_catch_all'] = $row['address'];
1200 }
1201 return $data;
1202}
1203function is_valid_domain_name($domain_name) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001204 if (empty($domain_name)) {
1205 return false;
1206 }
1207 $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
1208 return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
1209 && preg_match("/^.{1,253}$/", $domain_name)
1210 && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001211}
1212function set_tfa($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001213 global $pdo;
1214 global $yubi;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001215 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001216 $_data_log = $_data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001217 $access_denied = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001218 !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
1219 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001220
1221 // check for empty user and role
1222 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
1223
1224 // check admin confirm password
1225 if ($access_denied === null) {
1226 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
1227 WHERE `username` = :username");
1228 $stmt->execute(array(':username' => $username));
1229 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1230 if ($row) {
1231 if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
1232 else $access_denied = false;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001233 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001234 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001235
1236 // check mailbox confirm password
1237 if ($access_denied === null) {
1238 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1239 WHERE `username` = :username");
1240 $stmt->execute(array(':username' => $username));
1241 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1242 if ($row) {
1243 if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
1244 else $access_denied = false;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001245 }
1246 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001247
1248 // set access_denied error
1249 if ($access_denied){
1250 $_SESSION['return'][] = array(
1251 'type' => 'danger',
1252 'log' => array(__FUNCTION__, $_data_log),
1253 'msg' => 'access_denied'
1254 );
1255 return false;
1256 }
1257
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001258 switch ($_data["tfa_method"]) {
1259 case "yubi_otp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001260 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1261 $yubico_id = $_data['yubico_id'];
1262 $yubico_key = $_data['yubico_key'];
1263 $yubi = new Auth_Yubico($yubico_id, $yubico_key);
1264 if (!$yubi) {
1265 $_SESSION['return'][] = array(
1266 'type' => 'danger',
1267 'log' => array(__FUNCTION__, $_data_log),
1268 'msg' => 'access_denied'
1269 );
1270 return false;
1271 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001272 if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
1273 $_SESSION['return'][] = array(
1274 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001275 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001276 'msg' => 'tfa_token_invalid'
1277 );
1278 return false;
1279 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001280 $yauth = $yubi->verify($_data["otp_token"]);
1281 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001282 $_SESSION['return'][] = array(
1283 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001284 'log' => array(__FUNCTION__, $_data_log),
1285 'msg' => array('yotp_verification_failed', $yauth->getMessage())
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001286 );
1287 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001288 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001289 try {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001290 // We could also do a modhex translation here
1291 $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
1292 $stmt = $pdo->prepare("DELETE FROM `tfa`
1293 WHERE `username` = :username
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001294 AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001295 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1296 $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
1297 (:key_id, :username, 'yubi_otp', '1', :secret)");
1298 $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
1299 }
1300 catch (PDOException $e) {
1301 $_SESSION['return'][] = array(
1302 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001303 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001304 'msg' => array('mysql_error', $e)
1305 );
1306 return false;
1307 }
1308 $_SESSION['return'][] = array(
1309 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001310 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001311 'msg' => array('object_modified', htmlspecialchars($username))
1312 );
1313 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001314 case "totp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001315 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1316 if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001317 //$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1318 //$stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001319 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
1320 $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
1321 $_SESSION['return'][] = array(
1322 'type' => 'success',
1323 'log' => array(__FUNCTION__, $_data_log),
1324 'msg' => array('object_modified', $username)
1325 );
1326 }
1327 else {
1328 $_SESSION['return'][] = array(
1329 'type' => 'danger',
1330 'log' => array(__FUNCTION__, $_data_log),
1331 'msg' => 'totp_verification_failed'
1332 );
1333 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001334 break;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001335 case "webauthn":
1336 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1337
1338 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
1339 VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
1340 $stmt->execute(array(
1341 $username,
1342 $key_id,
1343 base64_encode($_data['registration']->credentialId),
1344 $_data['registration']->credentialPublicKey,
1345 $_data['registration']->certificate,
1346 0
1347 ));
1348
1349 $_SESSION['return'][] = array(
1350 'type' => 'success',
1351 'log' => array(__FUNCTION__, $_data_log),
1352 'msg' => array('object_modified', $username)
1353 );
1354 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001355 case "none":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001356 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1357 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001358 $_SESSION['return'][] = array(
1359 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001360 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001361 'msg' => array('object_modified', htmlspecialchars($username))
1362 );
1363 break;
1364 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001365}
1366function fido2($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001367 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001368 $_data_log = $_data;
1369 // Not logging registration data, only actions
1370 // Silent errors for "get" requests
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001371 switch ($_data["action"]) {
1372 case "register":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001373 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001374 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001375 $_SESSION['return'][] = array(
1376 'type' => 'danger',
1377 'log' => array(__FUNCTION__, $_data["action"]),
1378 'msg' => 'access_denied'
1379 );
1380 return false;
1381 }
1382 $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
1383 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1384 $stmt->execute(array(
1385 $username,
1386 $_data['registration']->rpId,
1387 $_data['registration']->credentialPublicKey,
1388 $_data['registration']->certificateChain,
1389 $_data['registration']->certificate,
1390 $_data['registration']->certificateIssuer,
1391 $_data['registration']->certificateSubject,
1392 $_data['registration']->signatureCounter,
1393 $_data['registration']->AAGUID,
1394 $_data['registration']->credentialId)
1395 );
1396 $_SESSION['return'][] = array(
1397 'type' => 'success',
1398 'log' => array(__FUNCTION__, $_data["action"]),
1399 'msg' => array('object_modified', $username)
1400 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001401 break;
1402 case "get_user_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001403 // Used to exclude existing CredentialIds while registering
1404 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001405 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1406 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001407 }
1408 $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
1409 $stmt->execute(array(':username' => $username));
1410 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1411 while($row = array_shift($rows)) {
1412 $cids[] = $row['credentialId'];
1413 }
1414 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001415 break;
1416 case "get_all_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001417 // Only needed when using fido2 with username
1418 $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
1419 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1420 while($row = array_shift($rows)) {
1421 $cids[] = $row['credentialId'];
1422 }
1423 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001424 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001425 case "get_by_b64cid":
1426 if (!isset($_data['cid']) || empty($_data['cid'])) {
1427 return false;
1428 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001429 $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");
1430 $stmt->execute(array(':cid' => base64_decode($_data['cid'])));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001431 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1432 if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
1433 return false;
1434 }
1435 $data['pub_key'] = $row['credentialPublicKey'];
1436 $data['username'] = $row['username'];
1437 $data['subject'] = $row['certificateSubject'];
1438 $data['cid'] = $row['cid'];
1439 return $data;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001440 break;
1441 case "get_friendly_names":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001442 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001443 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1444 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001445 }
1446 $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
1447 $stmt->execute(array(':username' => $username));
1448 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1449 while($row = array_shift($rows)) {
1450 $fns[] = array(
1451 "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
1452 "fn" => $row['friendlyName'],
1453 "cid" => $row['cid']
1454 );
1455 }
1456 return $fns;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001457 break;
1458 case "unset_fido2_key":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001459 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001460 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1461 $_SESSION['return'][] = array(
1462 'type' => 'danger',
1463 'log' => array(__FUNCTION__, $_data["action"]),
1464 'msg' => 'access_denied'
1465 );
1466 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001467 }
1468 $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
1469 $stmt->execute(array(
1470 ':username' => $username,
1471 ':cid' => $_data['post_data']['unset_fido2_key']
1472 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001473 $_SESSION['return'][] = array(
1474 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001475 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001476 'msg' => array('object_modified', htmlspecialchars($username))
1477 );
1478 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001479 case "edit_fn":
1480 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001481 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1482 $_SESSION['return'][] = array(
1483 'type' => 'danger',
1484 'log' => array(__FUNCTION__, $_data["action"]),
1485 'msg' => 'access_denied'
1486 );
1487 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001488 }
1489 $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
1490 $stmt->execute(array(
1491 ':username' => $username,
1492 ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
1493 ':cid' => $_data['fido2_attrs']['fido2_cid']
1494 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001495 $_SESSION['return'][] = array(
1496 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001497 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001498 'msg' => array('object_modified', htmlspecialchars($username))
1499 );
1500 break;
1501 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001502}
1503function unset_tfa_key($_data) {
1504 // Can only unset own keys
1505 // Needs at least one key left
1506 global $pdo;
1507 global $lang;
1508 $_data_log = $_data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001509 $access_denied = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001510 $id = intval($_data['unset_tfa_key']);
1511 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001512
1513 // check for empty user and role
1514 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
1515
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001516 try {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001517 if (!is_numeric($id)) $access_denied = true;
1518
1519 // set access_denied error
1520 if ($access_denied){
1521 $_SESSION['return'][] = array(
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001522 'type' => 'danger',
1523 'log' => array(__FUNCTION__, $_data_log),
1524 'msg' => 'access_denied'
1525 );
1526 return false;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001527 }
1528
1529 // check if it's last key
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001530 $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
1531 WHERE `username` = :username AND `active` = '1'");
1532 $stmt->execute(array(':username' => $username));
1533 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1534 if ($row['keys'] == "1") {
1535 $_SESSION['return'][] = array(
1536 'type' => 'danger',
1537 'log' => array(__FUNCTION__, $_data_log),
1538 'msg' => 'last_key'
1539 );
1540 return false;
1541 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001542
1543 // delete key
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001544 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
1545 $stmt->execute(array(':username' => $username, ':id' => $id));
1546 $_SESSION['return'][] = array(
1547 'type' => 'success',
1548 'log' => array(__FUNCTION__, $_data_log),
1549 'msg' => array('object_modified', $username)
1550 );
1551 }
1552 catch (PDOException $e) {
1553 $_SESSION['return'][] = array(
1554 'type' => 'danger',
1555 'log' => array(__FUNCTION__, $_data_log),
1556 'msg' => array('mysql_error', $e)
1557 );
1558 return false;
1559 }
1560}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001561function get_tfa($username = null, $id = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001562 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001563 if (isset($_SESSION['mailcow_cc_username'])) {
1564 $username = $_SESSION['mailcow_cc_username'];
1565 }
1566 elseif (empty($username)) {
1567 return false;
1568 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001569
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001570 if (!isset($id)){
1571 // fetch all tfa methods - just get information about possible authenticators
1572 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
1573 WHERE `username` = :username AND `active` = '1'");
1574 $stmt->execute(array(':username' => $username));
1575 $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
1576
1577 // no tfa methods found
1578 if (count($results) == 0) {
1579 $data['name'] = 'none';
1580 $data['pretty'] = "-";
1581 $data['additional'] = array();
1582 return $data;
1583 }
1584
1585 $data['additional'] = $results;
1586 return $data;
1587 } else {
1588 // fetch specific authenticator details by id
1589 $stmt = $pdo->prepare("SELECT * FROM `tfa`
1590 WHERE `username` = :username AND `id` = :id AND `active` = '1'");
1591 $stmt->execute(array(':username' => $username, ':id' => $id));
1592 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1593
1594 if (isset($row["authmech"])) {
1595 switch ($row["authmech"]) {
1596 case "yubi_otp":
1597 $data['name'] = "yubi_otp";
1598 $data['pretty'] = "Yubico OTP";
1599 $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
1600 $stmt->execute(array(
1601 ':username' => $username,
1602 ':id' => $id
1603 ));
1604 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1605 while($row = array_shift($rows)) {
1606 $data['additional'][] = $row;
1607 }
1608 return $data;
1609 break;
1610 // u2f - deprecated, should be removed
1611 case "u2f":
1612 $data['name'] = "u2f";
1613 $data['pretty'] = "Fido U2F";
1614 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
1615 $stmt->execute(array(
1616 ':username' => $username,
1617 ':id' => $id
1618 ));
1619 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1620 while($row = array_shift($rows)) {
1621 $data['additional'][] = $row;
1622 }
1623 return $data;
1624 break;
1625 case "hotp":
1626 $data['name'] = "hotp";
1627 $data['pretty'] = "HMAC-based OTP";
1628 return $data;
1629 break;
1630 case "totp":
1631 $data['name'] = "totp";
1632 $data['pretty'] = "Time-based OTP";
1633 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
1634 $stmt->execute(array(
1635 ':username' => $username,
1636 ':id' => $id
1637 ));
1638 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1639 while($row = array_shift($rows)) {
1640 $data['additional'][] = $row;
1641 }
1642 return $data;
1643 break;
1644 case "webauthn":
1645 $data['name'] = "webauthn";
1646 $data['pretty'] = "WebAuthn";
1647 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
1648 $stmt->execute(array(
1649 ':username' => $username,
1650 ':id' => $id
1651 ));
1652 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1653 while($row = array_shift($rows)) {
1654 $data['additional'][] = $row;
1655 }
1656 return $data;
1657 break;
1658 default:
1659 $data['name'] = 'none';
1660 $data['pretty'] = "-";
1661 return $data;
1662 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001663 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001664 }
1665 else {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001666 $data['name'] = 'none';
1667 $data['pretty'] = "-";
1668 return $data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001669 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001670 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001671}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001672function verify_tfa_login($username, $_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001673 global $pdo;
1674 global $yubi;
1675 global $u2f;
1676 global $tfa;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001677 global $WebAuthn;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001678
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001679 if ($_data['tfa_method'] != 'u2f'){
1680
1681 switch ($_data["tfa_method"]) {
1682 case "yubi_otp":
1683 if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
1684 $_SESSION['return'][] = array(
1685 'type' => 'danger',
1686 'log' => array(__FUNCTION__, $username, '*'),
1687 'msg' => array('yotp_verification_failed', 'token length error')
1688 );
1689 return false;
1690 }
1691 $yubico_modhex_id = substr($_data['token'], 0, 12);
1692 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1693 WHERE `username` = :username
1694 AND `authmech` = 'yubi_otp'
1695 AND `active` = '1'
1696 AND `secret` LIKE :modhex");
1697 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1698 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1699 $yubico_auth = explode(':', $row['secret']);
1700 $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
1701 $yauth = $yubi->verify($_data['token']);
1702 if (PEAR::isError($yauth)) {
1703 $_SESSION['return'][] = array(
1704 'type' => 'danger',
1705 'log' => array(__FUNCTION__, $username, '*'),
1706 'msg' => array('yotp_verification_failed', $yauth->getMessage())
1707 );
1708 return false;
1709 }
1710 else {
1711 $_SESSION['tfa_id'] = $row['id'];
1712 $_SESSION['return'][] = array(
1713 'type' => 'success',
1714 'log' => array(__FUNCTION__, $username, '*'),
1715 'msg' => 'verified_yotp_login'
1716 );
1717 return true;
1718 }
1719 $_SESSION['return'][] = array(
1720 'type' => 'danger',
1721 'log' => array(__FUNCTION__, $username, '*'),
1722 'msg' => array('yotp_verification_failed', 'unknown')
1723 );
1724 return false;
1725 break;
1726 case "hotp":
1727 return false;
1728 break;
1729 case "totp":
1730 try {
1731 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1732 WHERE `username` = :username
1733 AND `authmech` = 'totp'
1734 AND `id` = :id
1735 AND `active`='1'");
1736 $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
1737 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1738 foreach ($rows as $row) {
1739 if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
1740 $_SESSION['tfa_id'] = $row['id'];
1741 $_SESSION['return'][] = array(
1742 'type' => 'success',
1743 'log' => array(__FUNCTION__, $username, '*'),
1744 'msg' => 'verified_totp_login'
1745 );
1746 return true;
1747 }
1748 }
1749 $_SESSION['return'][] = array(
1750 'type' => 'danger',
1751 'log' => array(__FUNCTION__, $username, '*'),
1752 'msg' => 'totp_verification_failed'
1753 );
1754 return false;
1755 }
1756 catch (PDOException $e) {
1757 $_SESSION['return'][] = array(
1758 'type' => 'danger',
1759 'log' => array(__FUNCTION__, $username, '*'),
1760 'msg' => array('mysql_error', $e)
1761 );
1762 return false;
1763 }
1764 break;
1765 case "webauthn":
1766 $tokenData = json_decode($_data['token']);
1767 $clientDataJSON = base64_decode($tokenData->clientDataJSON);
1768 $authenticatorData = base64_decode($tokenData->authenticatorData);
1769 $signature = base64_decode($tokenData->signature);
1770 $id = base64_decode($tokenData->id);
1771 $challenge = $_SESSION['challenge'];
1772
1773 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
1774 $stmt->execute(array(':id' => $_data['id']));
1775 $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
1776
1777 if (empty($process_webauthn)){
1778 $_SESSION['return'][] = array(
1779 'type' => 'danger',
1780 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001781 'msg' => array('webauthn_authenticator_failed')
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001782 );
1783 return false;
1784 }
1785
1786 if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
1787 $_SESSION['return'][] = array(
1788 'type' => 'danger',
1789 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001790 'msg' => array('webauthn_publickey_failed')
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001791 );
1792 return false;
1793 }
1794
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001795 if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
1796 $_SESSION['return'][] = array(
1797 'type' => 'danger',
1798 'log' => array(__FUNCTION__, $username, '*'),
1799 'msg' => array('webauthn_username_failed')
1800 );
1801 return false;
1802 }
1803
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001804 try {
1805 $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
1806 }
1807 catch (Throwable $ex) {
1808 $_SESSION['return'][] = array(
1809 'type' => 'danger',
1810 'log' => array(__FUNCTION__, $username, '*'),
1811 'msg' => array('webauthn_verification_failed', $ex->getMessage())
1812 );
1813 return false;
1814 }
1815
1816 $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
1817 $stmt->execute(array(':username' => $process_webauthn['username']));
1818 $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
1819 if ($obj_props['superadmin'] === 1) {
1820 $_SESSION["mailcow_cc_role"] = "admin";
1821 }
1822 elseif ($obj_props['superadmin'] === 0) {
1823 $_SESSION["mailcow_cc_role"] = "domainadmin";
1824 }
1825 else {
1826 $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
1827 $stmt->execute(array(':username' => $process_webauthn['username']));
1828 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1829 if (!empty($row['username'])) {
1830 $_SESSION["mailcow_cc_role"] = "user";
1831 } else {
1832 $_SESSION['return'][] = array(
1833 'type' => 'danger',
1834 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01001835 'msg' => array('webauthn_role_failed')
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001836 );
1837 return false;
1838 }
1839 }
1840
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001841 $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
1842 $_SESSION['tfa_id'] = $process_webauthn['id'];
1843 $_SESSION['authReq'] = null;
1844 unset($_SESSION["challenge"]);
1845 $_SESSION['return'][] = array(
1846 'type' => 'success',
1847 'log' => array("webauthn_login"),
1848 'msg' => array('logged_in_as', $process_webauthn['username'])
1849 );
1850 return true;
1851 break;
1852 default:
1853 $_SESSION['return'][] = array(
1854 'type' => 'danger',
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001855 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001856 'msg' => 'unknown_tfa_method'
1857 );
1858 return false;
1859 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001860 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001861
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001862 return false;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001863 } else {
1864 // delete old keys that used u2f
1865 $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1866 $stmt->execute(array(':username' => $username));
1867 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1868 if (count($rows) == 0) return false;
1869
1870 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1871 $stmt->execute(array(':username' => $username));
1872 return true;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001873 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001874}
1875function admin_api($access, $action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001876 global $pdo;
1877 if ($_SESSION['mailcow_cc_role'] != "admin") {
1878 $_SESSION['return'][] = array(
1879 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001880 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001881 'msg' => 'access_denied'
1882 );
1883 return false;
1884 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001885 if ($access !== "ro" && $access !== "rw") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001886 $_SESSION['return'][] = array(
1887 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001888 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001889 'msg' => 'invalid access type'
1890 );
1891 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001892 }
1893 if ($action == "edit") {
1894 $active = (!empty($data['active'])) ? 1 : 0;
1895 $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001896 $allow_from = array();
1897 if (isset($data['allow_from'])) {
1898 $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
1899 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001900 foreach ($allow_from as $key => $val) {
1901 if (empty($val)) {
1902 unset($allow_from[$key]);
1903 continue;
1904 }
1905 if (valid_network($val) !== true) {
1906 $_SESSION['return'][] = array(
1907 'type' => 'warning',
1908 'log' => array(__FUNCTION__, $data),
1909 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
1910 );
1911 unset($allow_from[$key]);
1912 continue;
1913 }
1914 }
1915 $allow_from = implode(',', array_unique(array_filter($allow_from)));
1916 if (empty($allow_from) && $skip_ip_check == 0) {
1917 $_SESSION['return'][] = array(
1918 'type' => 'danger',
1919 'log' => array(__FUNCTION__, $data),
1920 'msg' => 'ip_list_empty'
1921 );
1922 return false;
1923 }
1924 $api_key = implode('-', array(
1925 strtoupper(bin2hex(random_bytes(3))),
1926 strtoupper(bin2hex(random_bytes(3))),
1927 strtoupper(bin2hex(random_bytes(3))),
1928 strtoupper(bin2hex(random_bytes(3))),
1929 strtoupper(bin2hex(random_bytes(3)))
1930 ));
1931 $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
1932 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1933 if (empty($num_results)) {
1934 $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
1935 VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
1936 $stmt->execute(array(
1937 ':api_key' => $api_key,
1938 ':skip_ip_check' => $skip_ip_check,
1939 ':active' => $active,
1940 ':allow_from' => $allow_from,
1941 ':access' => $access
1942 ));
1943 }
1944 else {
1945 if ($skip_ip_check == 0) {
1946 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1947 `active` = :active,
1948 `allow_from` = :allow_from
1949 WHERE `access` = :access;");
1950 $stmt->execute(array(
1951 ':active' => $active,
1952 ':skip_ip_check' => $skip_ip_check,
1953 ':allow_from' => $allow_from,
1954 ':access' => $access
1955 ));
1956 }
1957 else {
1958 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1959 `active` = :active
1960 WHERE `access` = :access;");
1961 $stmt->execute(array(
1962 ':active' => $active,
1963 ':skip_ip_check' => $skip_ip_check,
1964 ':access' => $access
1965 ));
1966 }
1967 }
1968 }
1969 elseif ($action == "regen_key") {
1970 $api_key = implode('-', array(
1971 strtoupper(bin2hex(random_bytes(3))),
1972 strtoupper(bin2hex(random_bytes(3))),
1973 strtoupper(bin2hex(random_bytes(3))),
1974 strtoupper(bin2hex(random_bytes(3))),
1975 strtoupper(bin2hex(random_bytes(3)))
1976 ));
1977 $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
1978 $stmt->execute(array(
1979 ':api_key' => $api_key,
1980 ':access' => $access
1981 ));
1982 }
1983 elseif ($action == "get") {
1984 $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
1985 $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001986 if ($apidata !== false) {
1987 $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
1988 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001989 return $apidata;
1990 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001991 $_SESSION['return'][] = array(
1992 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001993 'log' => array(__FUNCTION__, $data),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001994 'msg' => 'admin_api_modified'
1995 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001996}
1997function license($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001998 global $pdo;
1999 global $redis;
2000 global $lang;
2001 if ($_SESSION['mailcow_cc_role'] != "admin") {
2002 $_SESSION['return'][] = array(
2003 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002004 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002005 'msg' => 'access_denied'
2006 );
2007 return false;
2008 }
2009 switch ($action) {
2010 case "verify":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002011 // Keep result until revalidate button is pressed or session expired
2012 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
2013 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
2014 $post = array('guid' => $versions['version']);
2015 $curl = curl_init('https://verify.mailcow.email');
2016 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2017 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
2018 curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
2019 $response = curl_exec($curl);
2020 curl_close($curl);
2021 $json_return = json_decode($response, true);
2022 if ($response && $json_return) {
2023 if ($json_return['response'] === "ok") {
2024 $_SESSION['gal']['valid'] = "true";
2025 $_SESSION['gal']['c'] = $json_return['c'];
2026 $_SESSION['gal']['s'] = $json_return['s'];
2027 if ($json_return['m'] == 'NoMoore') {
2028 $_SESSION['gal']['m'] = '🐄';
2029 }
2030 else {
2031 $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
2032 }
2033 }
2034 elseif ($json_return['response'] === "invalid") {
2035 $_SESSION['gal']['valid'] = "false";
2036 $_SESSION['gal']['c'] = $lang['mailbox']['no'];
2037 $_SESSION['gal']['s'] = $lang['mailbox']['no'];
2038 $_SESSION['gal']['m'] = $lang['mailbox']['no'];
2039 }
2040 }
2041 else {
2042 $_SESSION['gal']['valid'] = "false";
2043 $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
2044 $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
2045 $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
2046 }
2047 try {
2048 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
2049 $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
2050 }
2051 catch (RedisException $e) {
2052 $_SESSION['return'][] = array(
2053 'type' => 'danger',
2054 'log' => array(__FUNCTION__, $_action, $_data_log),
2055 'msg' => array('redis_error', $e)
2056 );
2057 return false;
2058 }
2059 return $_SESSION['gal']['valid'];
2060 break;
2061 case "guid":
2062 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
2063 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
2064 return $versions['version'];
2065 break;
2066 }
2067}
2068function rspamd_ui($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002069 if ($_SESSION['mailcow_cc_role'] != "admin") {
2070 $_SESSION['return'][] = array(
2071 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002072 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002073 'msg' => 'access_denied'
2074 );
2075 return false;
2076 }
2077 switch ($action) {
2078 case "edit":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002079 $rspamd_ui_pass = $data['rspamd_ui_pass'];
2080 $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
2081 if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
2082 $_SESSION['return'][] = array(
2083 'type' => 'danger',
2084 'log' => array(__FUNCTION__, '*', '*'),
2085 'msg' => 'password_empty'
2086 );
2087 return false;
2088 }
2089 if ($rspamd_ui_pass != $rspamd_ui_pass2) {
2090 $_SESSION['return'][] = array(
2091 'type' => 'danger',
2092 'log' => array(__FUNCTION__, '*', '*'),
2093 'msg' => 'password_mismatch'
2094 );
2095 return false;
2096 }
2097 if (strlen($rspamd_ui_pass) < 6) {
2098 $_SESSION['return'][] = array(
2099 'type' => 'danger',
2100 'log' => array(__FUNCTION__, '*', '*'),
2101 'msg' => 'rspamd_ui_pw_length'
2102 );
2103 return false;
2104 }
2105 $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
2106 if ($docker_return_array = json_decode($docker_return, true)) {
2107 if ($docker_return_array['type'] == 'success') {
2108 $_SESSION['return'][] = array(
2109 'type' => 'success',
2110 'log' => array(__FUNCTION__, '*', '*'),
2111 'msg' => 'rspamd_ui_pw_set'
2112 );
2113 return true;
2114 }
2115 else {
2116 $_SESSION['return'][] = array(
2117 'type' => $docker_return_array['type'],
2118 'log' => array(__FUNCTION__, '*', '*'),
2119 'msg' => $docker_return_array['msg']
2120 );
2121 return false;
2122 }
2123 }
2124 else {
2125 $_SESSION['return'][] = array(
2126 'type' => 'danger',
2127 'log' => array(__FUNCTION__, '*', '*'),
2128 'msg' => 'unknown'
2129 );
2130 return false;
2131 }
2132 break;
2133 }
2134}
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +01002135function cors($action, $data = null) {
2136 global $redis;
2137
2138 switch ($action) {
2139 case "edit":
2140 if ($_SESSION['mailcow_cc_role'] != "admin") {
2141 $_SESSION['return'][] = array(
2142 'type' => 'danger',
2143 'log' => array(__FUNCTION__, $action, $data),
2144 'msg' => 'access_denied'
2145 );
2146 return false;
2147 }
2148
2149 $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
2150 $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
2151 foreach ($allowed_origins as $origin) {
2152 if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
2153 $_SESSION['return'][] = array(
2154 'type' => 'danger',
2155 'log' => array(__FUNCTION__, $action, $data),
2156 'msg' => 'cors_invalid_origin'
2157 );
2158 return false;
2159 }
2160 }
2161
2162 $allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
2163 $allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
2164 $available_methods = array('GET', 'POST', 'PUT', 'DELETE');
2165 foreach ($allowed_methods as $method) {
2166 if (!in_array($method, $available_methods)) {
2167 $_SESSION['return'][] = array(
2168 'type' => 'danger',
2169 'log' => array(__FUNCTION__, $action, $data),
2170 'msg' => 'cors_invalid_method'
2171 );
2172 return false;
2173 }
2174 }
2175
2176 try {
2177 $redis->hMSet('CORS_SETTINGS', array(
2178 'allowed_origins' => implode(', ', $allowed_origins),
2179 'allowed_methods' => implode(', ', $allowed_methods)
2180 ));
2181 } catch (RedisException $e) {
2182 $_SESSION['return'][] = array(
2183 'type' => 'danger',
2184 'log' => array(__FUNCTION__, $action, $data),
2185 'msg' => array('redis_error', $e)
2186 );
2187 return false;
2188 }
2189
2190 $_SESSION['return'][] = array(
2191 'type' => 'success',
2192 'log' => array(__FUNCTION__, $action, $data),
2193 'msg' => 'cors_headers_edited'
2194 );
2195 return true;
2196 break;
2197 case "get":
2198 try {
2199 $cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
2200 } catch (RedisException $e) {
2201 $_SESSION['return'][] = array(
2202 'type' => 'danger',
2203 'log' => array(__FUNCTION__, $action, $data),
2204 'msg' => array('redis_error', $e)
2205 );
2206 }
2207
2208 $cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
2209 $cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
2210 $cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
2211
2212 return $cors_settings;
2213 break;
2214 case "set_headers":
2215 $cors_settings = cors('get');
2216 // check if requested origin is in allowed origins
2217 $allowed_origins = explode(', ', $cors_settings['allowed_origins']);
2218 $cors_settings['allowed_origins'] = $allowed_origins[0];
2219 if (in_array('*', $allowed_origins)){
2220 $cors_settings['allowed_origins'] = '*';
2221 } else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
2222 $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
2223 }
2224 // always allow OPTIONS for preflight request
2225 $cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
2226
2227 header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
2228 header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
2229 header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
2230
2231 // Access-Control settings requested, this is just a preflight request
2232 if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
2233 isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
2234 isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
2235
2236 $allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
2237 if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
2238 // method allowed send 200 OK
2239 http_response_code(200);
2240 else
2241 // method not allowed send 405 METHOD NOT ALLOWED
2242 http_response_code(405);
2243
2244 exit;
2245 }
2246 break;
2247 }
2248}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002249
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002250function get_logs($application, $lines = false) {
2251 if ($lines === false) {
2252 $lines = $GLOBALS['LOG_LINES'] - 1;
2253 }
2254 elseif(is_numeric($lines) && $lines >= 1) {
2255 $lines = abs(intval($lines) - 1);
2256 }
2257 else {
2258 list ($from, $to) = explode('-', $lines);
2259 $from = intval($from);
2260 $to = intval($to);
2261 if ($from < 1 || $to < $from) { return false; }
2262 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002263 global $redis;
2264 global $pdo;
2265 if ($_SESSION['mailcow_cc_role'] != "admin") {
2266 return false;
2267 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002268 // SQL
2269 if ($application == "mailcow-ui") {
2270 if (isset($from) && isset($to)) {
2271 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
2272 $stmt->execute(array(
2273 ':from' => $from - 1,
2274 ':to' => $to
2275 ));
2276 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2277 }
2278 else {
2279 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
2280 $stmt->execute(array(
2281 ':lines' => $lines + 1,
2282 ));
2283 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2284 }
2285 if (is_array($data)) {
2286 return $data;
2287 }
2288 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002289 if ($application == "sasl") {
2290 if (isset($from) && isset($to)) {
2291 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
2292 $stmt->execute(array(
2293 ':from' => $from - 1,
2294 ':to' => $to
2295 ));
2296 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2297 }
2298 else {
2299 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
2300 $stmt->execute(array(
2301 ':lines' => $lines + 1,
2302 ));
2303 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2304 }
2305 if (is_array($data)) {
2306 return $data;
2307 }
2308 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002309 // Redis
2310 if ($application == "dovecot-mailcow") {
2311 if (isset($from) && isset($to)) {
2312 $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
2313 }
2314 else {
2315 $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
2316 }
2317 if ($data) {
2318 foreach ($data as $json_line) {
2319 $data_array[] = json_decode($json_line, true);
2320 }
2321 return $data_array;
2322 }
2323 }
2324 if ($application == "postfix-mailcow") {
2325 if (isset($from) && isset($to)) {
2326 $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
2327 }
2328 else {
2329 $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
2330 }
2331 if ($data) {
2332 foreach ($data as $json_line) {
2333 $data_array[] = json_decode($json_line, true);
2334 }
2335 return $data_array;
2336 }
2337 }
2338 if ($application == "sogo-mailcow") {
2339 if (isset($from) && isset($to)) {
2340 $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
2341 }
2342 else {
2343 $data = $redis->lRange('SOGO_LOG', 0, $lines);
2344 }
2345 if ($data) {
2346 foreach ($data as $json_line) {
2347 $data_array[] = json_decode($json_line, true);
2348 }
2349 return $data_array;
2350 }
2351 }
2352 if ($application == "watchdog-mailcow") {
2353 if (isset($from) && isset($to)) {
2354 $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
2355 }
2356 else {
2357 $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
2358 }
2359 if ($data) {
2360 foreach ($data as $json_line) {
2361 $data_array[] = json_decode($json_line, true);
2362 }
2363 return $data_array;
2364 }
2365 }
2366 if ($application == "acme-mailcow") {
2367 if (isset($from) && isset($to)) {
2368 $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
2369 }
2370 else {
2371 $data = $redis->lRange('ACME_LOG', 0, $lines);
2372 }
2373 if ($data) {
2374 foreach ($data as $json_line) {
2375 $data_array[] = json_decode($json_line, true);
2376 }
2377 return $data_array;
2378 }
2379 }
2380 if ($application == "ratelimited") {
2381 if (isset($from) && isset($to)) {
2382 $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
2383 }
2384 else {
2385 $data = $redis->lRange('RL_LOG', 0, $lines);
2386 }
2387 if ($data) {
2388 foreach ($data as $json_line) {
2389 $data_array[] = json_decode($json_line, true);
2390 }
2391 return $data_array;
2392 }
2393 }
2394 if ($application == "api-mailcow") {
2395 if (isset($from) && isset($to)) {
2396 $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
2397 }
2398 else {
2399 $data = $redis->lRange('API_LOG', 0, $lines);
2400 }
2401 if ($data) {
2402 foreach ($data as $json_line) {
2403 $data_array[] = json_decode($json_line, true);
2404 }
2405 return $data_array;
2406 }
2407 }
2408 if ($application == "netfilter-mailcow") {
2409 if (isset($from) && isset($to)) {
2410 $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
2411 }
2412 else {
2413 $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
2414 }
2415 if ($data) {
2416 foreach ($data as $json_line) {
2417 $data_array[] = json_decode($json_line, true);
2418 }
2419 return $data_array;
2420 }
2421 }
2422 if ($application == "autodiscover-mailcow") {
2423 if (isset($from) && isset($to)) {
2424 $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
2425 }
2426 else {
2427 $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
2428 }
2429 if ($data) {
2430 foreach ($data as $json_line) {
2431 $data_array[] = json_decode($json_line, true);
2432 }
2433 return $data_array;
2434 }
2435 }
2436 if ($application == "rspamd-history") {
2437 $curl = curl_init();
2438 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2439 if (!is_numeric($lines)) {
2440 list ($from, $to) = explode('-', $lines);
2441 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
2442 }
2443 else {
2444 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
2445 }
2446 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2447 $history = curl_exec($curl);
2448 if (!curl_errno($curl)) {
2449 $data_array = json_decode($history, true);
2450 curl_close($curl);
2451 return $data_array['rows'];
2452 }
2453 curl_close($curl);
2454 return false;
2455 }
2456 if ($application == "rspamd-stats") {
2457 $curl = curl_init();
2458 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2459 curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
2460 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2461 $stats = curl_exec($curl);
2462 if (!curl_errno($curl)) {
2463 $data_array = json_decode($stats, true);
2464 curl_close($curl);
2465 return $data_array;
2466 }
2467 curl_close($curl);
2468 return false;
2469 }
2470 return false;
2471}
2472function getGUID() {
2473 if (function_exists('com_create_guid')) {
2474 return com_create_guid();
2475 }
2476 mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
2477 $charid = strtoupper(md5(uniqid(rand(), true)));
2478 $hyphen = chr(45);// "-"
2479 return substr($charid, 0, 8).$hyphen
2480 .substr($charid, 8, 4).$hyphen
2481 .substr($charid,12, 4).$hyphen
2482 .substr($charid,16, 4).$hyphen
2483 .substr($charid,20,12);
2484}
2485function solr_status() {
2486 $curl = curl_init();
2487 $endpoint = 'http://solr:8983/solr/admin/cores';
2488 $params = array(
2489 'action' => 'STATUS',
2490 'core' => 'dovecot-fts',
2491 'indexInfo' => 'true'
2492 );
2493 $url = $endpoint . '?' . http_build_query($params);
2494 curl_setopt($curl, CURLOPT_URL, $url);
2495 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2496 curl_setopt($curl, CURLOPT_POST, 0);
2497 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2498 $response_core = curl_exec($curl);
2499 if ($response_core === false) {
2500 $err = curl_error($curl);
2501 curl_close($curl);
2502 return false;
2503 }
2504 else {
2505 curl_close($curl);
2506 $curl = curl_init();
2507 $status_core = json_decode($response_core, true);
2508 $url = 'http://solr:8983/solr/admin/info/system';
2509 curl_setopt($curl, CURLOPT_URL, $url);
2510 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2511 curl_setopt($curl, CURLOPT_POST, 0);
2512 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2513 $response_sysinfo = curl_exec($curl);
2514 if ($response_sysinfo === false) {
2515 $err = curl_error($curl);
2516 curl_close($curl);
2517 return false;
2518 }
2519 else {
2520 curl_close($curl);
2521 $status_sysinfo = json_decode($response_sysinfo, true);
2522 $status = array_merge($status_core, $status_sysinfo);
2523 return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
2524 }
2525 return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
2526 }
2527 return false;
2528}
2529
2530function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
2531 $now = time();
2532 foreach (glob($folder) as $filename) {
2533 if(strpos($filename, $ignore) !== false) {
2534 continue;
2535 }
2536 if (is_file($filename)) {
2537 if ($now - filemtime($filename) >= 60 * 60) {
2538 unlink($filename);
2539 }
2540 }
2541 }
2542}
2543
2544function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
2545 $now = time();
2546 foreach (glob($folder) as $filename) {
2547 if(strpos($filename, $ignore) !== false) {
2548 continue;
2549 }
2550 if (is_file($filename)) {
2551 if ($now - filemtime($filename) >= 60 * 60) {
2552 unlink($filename);
2553 }
2554 }
2555 }
2556}
2557
2558?>