blob: 142a9fed28786a6f68a32bb4c56170193edbba67 [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}
254function last_login($action, $username, $sasl_limit_days = 7) {
255 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)) {
261 $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service`, `app_password` FROM `sasl_log`
262 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
322 AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET 1');
323 $stmt->execute(array(':username' => $username));
324 $ui = $stmt->fetch(PDO::FETCH_ASSOC);
325 }
326 else {
327 $ui = array();
328 }
329
330 return array('ui' => $ui, 'sasl' => $sasl);
331 break;
332 case 'reset':
333 if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
334 $stmt = $pdo->prepare('DELETE FROM `sasl_log`
335 WHERE `username` = :username');
336 $stmt->execute(array(':username' => $username));
337 }
338 if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
339 $stmt = $pdo->prepare('DELETE FROM `logs`
340 WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
341 AND JSON_EXTRACT(`call`, "$[1]") = :username
342 AND `type` = "success"');
343 $stmt->execute(array(':username' => $username));
344 }
345 return true;
346 break;
347 }
348
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100349}
350function flush_memcached() {
351 try {
352 $m = new Memcached();
353 $m->addServer('memcached', 11211);
354 $m->flush();
355 }
356 catch ( Exception $e ) {
357 // Dunno
358 }
359}
360function sys_mail($_data) {
361 if ($_SESSION['mailcow_cc_role'] != "admin") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200362 $_SESSION['return'][] = array(
363 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100364 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200365 'msg' => 'access_denied'
366 );
367 return false;
368 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100369 $excludes = $_data['mass_exclude'];
370 $includes = $_data['mass_include'];
371 $mailboxes = array();
372 $mass_from = $_data['mass_from'];
373 $mass_text = $_data['mass_text'];
374 $mass_html = $_data['mass_html'];
375 $mass_subject = $_data['mass_subject'];
376 if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200377 $_SESSION['return'][] = array(
378 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100379 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200380 'msg' => 'from_invalid'
381 );
382 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100383 }
384 if (empty($mass_subject)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200385 $_SESSION['return'][] = array(
386 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100387 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200388 'msg' => 'subject_empty'
389 );
390 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100391 }
392 if (empty($mass_text)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200393 $_SESSION['return'][] = array(
394 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100395 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200396 'msg' => 'text_empty'
397 );
398 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100399 }
400 $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
401 foreach ($domains as $domain) {
402 foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
403 $mailboxes[] = $mailbox;
404 }
405 }
406 if (!empty($includes)) {
407 $rcpts = array_intersect($mailboxes, $includes);
408 }
409 elseif (!empty($excludes)) {
410 $rcpts = array_diff($mailboxes, $excludes);
411 }
412 else {
413 $rcpts = $mailboxes;
414 }
415 if (!empty($rcpts)) {
416 ini_set('max_execution_time', 0);
417 ini_set('max_input_time', 0);
418 $mail = new PHPMailer;
419 $mail->Timeout = 10;
420 $mail->SMTPOptions = array(
421 'ssl' => array(
422 'verify_peer' => false,
423 'verify_peer_name' => false,
424 'allow_self_signed' => true
425 )
426 );
427 $mail->isSMTP();
428 $mail->Host = 'dovecot-mailcow';
429 $mail->SMTPAuth = false;
430 $mail->Port = 24;
431 $mail->setFrom($mass_from);
432 $mail->Subject = $mass_subject;
433 $mail->CharSet ="UTF-8";
434 if (!empty($mass_html)) {
435 $mail->Body = $mass_html;
436 $mail->AltBody = $mass_text;
437 }
438 else {
439 $mail->Body = $mass_text;
440 }
441 $mail->XMailer = 'MooMassMail';
442 foreach ($rcpts as $rcpt) {
443 $mail->AddAddress($rcpt);
444 if (!$mail->send()) {
445 $_SESSION['return'][] = array(
446 'type' => 'warning',
447 'log' => array(__FUNCTION__),
448 'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
449 );
450 }
451 $mail->ClearAllRecipients();
452 }
453 }
454 $_SESSION['return'][] = array(
455 'type' => 'success',
456 'log' => array(__FUNCTION__),
457 'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
458 );
459}
460function logger($_data = false) {
461 /*
462 logger() will be called as last function
463 To manually log a message, logger needs to be called like below.
464
465 logger(array(
466 'return' => array(
467 array(
468 'type' => 'danger',
469 'log' => array(__FUNCTION__),
470 'msg' => $err
471 )
472 )
473 ));
474
475 These messages will not be printed as alert box.
476 To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
477
478 $_SESSION['return'][] = array(
479 'type' => 'danger',
480 'log' => array(__FUNCTION__, $user, '*'),
481 'msg' => $err
482 );
483 */
484 global $pdo;
485 if (!$_data) {
486 $_data = $_SESSION;
487 }
488 if (!empty($_data['return'])) {
489 $task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
490 foreach ($_data['return'] as $return) {
491 $type = $return['type'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200492 $msg = null;
493 if (isset($return['msg'])) {
494 $msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
495 }
496 $call = null;
497 if (isset($return['log'])) {
498 $call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
499 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100500 if (!empty($_SESSION["dual-login"]["username"])) {
501 $user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
502 $role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
503 }
504 elseif (!empty($_SESSION['mailcow_cc_username'])) {
505 $user = $_SESSION['mailcow_cc_username'];
506 $role = $_SESSION['mailcow_cc_role'];
507 }
508 else {
509 $user = 'unauthenticated';
510 $role = 'unauthenticated';
511 }
512 // We cannot log when logs is missing...
513 try {
514 $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
515 (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
516 $stmt->execute(array(
517 ':type' => $type,
518 ':task' => $task,
519 ':call' => $call,
520 ':msg' => $msg,
521 ':user' => $user,
522 ':role' => $role,
523 ':remote' => get_remote_ip()
524 ));
525 }
526 catch (Exception $e) {
527 // Do nothing
528 }
529 }
530 }
531 else {
532 return true;
533 }
534}
535function hasDomainAccess($username, $role, $domain) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200536 global $pdo;
537 if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
538 return false;
539 }
540 if (empty($domain) || !is_valid_domain_name($domain)) {
541 return false;
542 }
543 if ($role != 'admin' && $role != 'domainadmin') {
544 return false;
545 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100546 if ($role == 'admin') {
547 $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
548 WHERE `domain` = :domain");
549 $stmt->execute(array(':domain' => $domain));
550 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
551 $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
552 WHERE `alias_domain` = :domain");
553 $stmt->execute(array(':domain' => $domain));
554 $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
555 if ($num_results != 0) {
556 return true;
557 }
558 }
559 elseif ($role == 'domainadmin') {
560 $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
561 WHERE (
562 `active`='1'
563 AND `username` = :username
564 AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
565 )");
566 $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
567 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
568 if (!empty($num_results)) {
569 return true;
570 }
571 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200572 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100573}
574function hasMailboxObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200575 global $pdo;
576 if (empty($username) || empty($role) || empty($object)) {
577 return false;
578 }
579 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
580 return false;
581 }
582 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
583 return false;
584 }
585 if ($username == $object) {
586 return true;
587 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100588 $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
589 $stmt->execute(array(':object' => $object));
590 $row = $stmt->fetch(PDO::FETCH_ASSOC);
591 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
592 return true;
593 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200594 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100595}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200596// does also verify mailboxes as a mailbox is a alias == goto
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100597function hasAliasObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200598 global $pdo;
599 if (empty($username) || empty($role) || empty($object)) {
600 return false;
601 }
602 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
603 return false;
604 }
605 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
606 return false;
607 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100608 $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
609 $stmt->execute(array(':object' => $object));
610 $row = $stmt->fetch(PDO::FETCH_ASSOC);
611 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
612 return true;
613 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200614 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100615}
616function pem_to_der($pem_key) {
617 // Need to remove BEGIN/END PUBLIC KEY
618 $lines = explode("\n", trim($pem_key));
619 unset($lines[count($lines)-1]);
620 unset($lines[0]);
621 return base64_decode(implode('', $lines));
622}
623function expand_ipv6($ip) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200624 $hex = unpack("H*hex", inet_pton($ip));
625 $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
626 return $ip;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100627}
628function generate_tlsa_digest($hostname, $port, $starttls = null) {
629 if (!is_valid_domain_name($hostname)) {
630 return "Not a valid hostname";
631 }
632 if (empty($starttls)) {
633 $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
634 $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
635 if (!$stream) {
636 $error_msg = isset($error_msg) ? $error_msg : '-';
637 return $error_nr . ': ' . $error_msg;
638 }
639 }
640 else {
641 $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
642 if (!$stream) {
643 return $error_nr . ': ' . $error_msg;
644 }
645 $banner = fread($stream, 512 );
646 if (preg_match("/^220/i", $banner)) { // SMTP
647 fwrite($stream,"HELO tlsa.generator.local\r\n");
648 fread($stream, 512);
649 fwrite($stream,"STARTTLS\r\n");
650 fread($stream, 512);
651 }
652 elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
653 fwrite($stream,"A1 STARTTLS\r\n");
654 fread($stream, 512);
655 }
656 elseif (preg_match("/^\+OK/", $banner)) { // POP3
657 fwrite($stream,"STLS\r\n");
658 fread($stream, 512);
659 }
660 elseif (preg_match("/^OK/m", $banner)) { // Sieve
661 fwrite($stream,"STARTTLS\r\n");
662 fread($stream, 512);
663 }
664 else {
665 return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
666 }
667 // Upgrade connection
668 stream_set_blocking($stream, true);
669 stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
670 stream_context_set_option($stream, 'ssl', 'verify_peer', false);
671 stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
672 stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
673 stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
674 stream_set_blocking($stream, false);
675 }
676 $params = stream_context_get_params($stream);
677 if (!empty($params['options']['ssl']['peer_certificate'])) {
678 $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
679 // We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
680 $key_data = openssl_pkey_get_details($key_resource)['key'];
681 return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
682 }
683 else {
684 return 'Error: Cannot read peer certificate';
685 }
686}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200687function alertbox_log_parser($_data) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100688 global $lang;
689 if (isset($_data['return'])) {
690 foreach ($_data['return'] as $return) {
691 // Get type
692 $type = $return['type'];
693 // If a lang[type][msg] string exists, use it as message
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200694 if (isset($return['type']) && isset($return['msg']) && !is_array($return['msg'])) {
695 if (isset($lang[$return['type']][$return['msg']])) {
696 $msg = $lang[$return['type']][$return['msg']];
697 }
698 else {
699 $msg = $return['msg'];
700 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100701 }
702 // If msg is an array, use first element as language string and run printf on it with remaining array elements
703 elseif (is_array($return['msg'])) {
704 $msg = array_shift($return['msg']);
705 $msg = vsprintf(
706 $lang[$return['type']][$msg],
707 $return['msg']
708 );
709 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100710 else {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200711 $msg = '-';
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100712 }
713 $log_array[] = array('msg' => $msg, 'type' => json_encode($type));
714 }
715 if (!empty($log_array)) {
716 return $log_array;
717 }
718 }
719 return false;
720}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200721function verify_salted_hash($hash, $password, $algo, $salt_length) {
722 // Decode hash
723 $dhash = base64_decode($hash);
724 // Get first n bytes of binary which equals a SSHA hash
725 $ohash = substr($dhash, 0, $salt_length);
726 // Remove SSHA hash from decoded hash to get original salt string
727 $osalt = str_replace($ohash, '', $dhash);
728 // Check single salted SSHA hash against extracted hash
729 if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
730 return true;
731 }
732 return false;
733}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100734function verify_hash($hash, $password) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200735 if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
736 $scheme = strtoupper($hash_array[1]);
737 $hash = $hash_array[2];
738 switch ($scheme) {
739 case "ARGON2I":
740 case "ARGON2ID":
741 case "BLF-CRYPT":
742 case "CRYPT":
743 case "DES-CRYPT":
744 case "MD5-CRYPT":
745 case "MD5":
746 case "SHA256-CRYPT":
747 case "SHA512-CRYPT":
748 return password_verify($password, $hash);
749
750 case "CLEAR":
751 case "CLEARTEXT":
752 case "PLAIN":
753 return $password == $hash;
754
755 case "LDAP-MD5":
756 $hash = base64_decode($hash);
757 return hash_equals(hash('md5', $password, true), $hash);
758
759 case "PBKDF2":
760 $components = explode('$', $hash);
761 $salt = $components[2];
762 $rounds = $components[3];
763 $hash = $components[4];
764 return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
765
766 case "PLAIN-MD4":
767 return hash_equals(hash('md4', $password), $hash);
768
769 case "PLAIN-MD5":
770 return md5($password) == $hash;
771
772 case "PLAIN-TRUNC":
773 $components = explode('-', $hash);
774 if (count($components) > 1) {
775 $trunc_len = $components[0];
776 $trunc_password = $components[1];
777
778 return substr($password, 0, $trunc_len) == $trunc_password;
779 } else {
780 return $password == $hash;
781 }
782
783 case "SHA":
784 case "SHA1":
785 case "SHA256":
786 case "SHA512":
787 // SHA is an alias for SHA1
788 $scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
789 $hash = base64_decode($hash);
790 return hash_equals(hash($scheme, $password, true), $hash);
791
792 case "SMD5":
793 return verify_salted_hash($hash, $password, 'md5', 16);
794
795 case "SSHA":
796 return verify_salted_hash($hash, $password, 'sha1', 20);
797
798 case "SSHA256":
799 return verify_salted_hash($hash, $password, 'sha256', 32);
800
801 case "SSHA512":
802 return verify_salted_hash($hash, $password, 'sha512', 64);
803
804 default:
805 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100806 }
807 }
808 return false;
809}
810function check_login($user, $pass) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200811 global $pdo;
812 global $redis;
813 global $imap_server;
814 if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100815 $_SESSION['return'][] = array(
816 'type' => 'danger',
817 'log' => array(__FUNCTION__, $user, '*'),
818 'msg' => 'malformed_username'
819 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200820 return false;
821 }
822 $user = strtolower(trim($user));
823 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
824 WHERE `superadmin` = '1'
825 AND `active` = '1'
826 AND `username` = :user");
827 $stmt->execute(array(':user' => $user));
828 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
829 foreach ($rows as $row) {
830 if (verify_hash($row['password'], $pass)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100831 if (get_tfa($user)['name'] != "none") {
832 $_SESSION['pending_mailcow_cc_username'] = $user;
833 $_SESSION['pending_mailcow_cc_role'] = "admin";
834 $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
835 unset($_SESSION['ldelay']);
836 $_SESSION['return'][] = array(
837 'type' => 'info',
838 'log' => array(__FUNCTION__, $user, '*'),
839 'msg' => 'awaiting_tfa_confirmation'
840 );
841 return "pending";
842 }
843 else {
844 unset($_SESSION['ldelay']);
845 // Reactivate TFA if it was set to "deactivate TFA for next login"
846 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
847 $stmt->execute(array(':user' => $user));
848 $_SESSION['return'][] = array(
849 'type' => 'success',
850 'log' => array(__FUNCTION__, $user, '*'),
851 'msg' => array('logged_in_as', $user)
852 );
853 return "admin";
854 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200855 }
856 }
857 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
858 WHERE `superadmin` = '0'
859 AND `active`='1'
860 AND `username` = :user");
861 $stmt->execute(array(':user' => $user));
862 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
863 foreach ($rows as $row) {
864 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100865 if (get_tfa($user)['name'] != "none") {
866 $_SESSION['pending_mailcow_cc_username'] = $user;
867 $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
868 $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
869 unset($_SESSION['ldelay']);
870 $_SESSION['return'][] = array(
871 'type' => 'info',
872 'log' => array(__FUNCTION__, $user, '*'),
873 'msg' => 'awaiting_tfa_confirmation'
874 );
875 return "pending";
876 }
877 else {
878 unset($_SESSION['ldelay']);
879 // Reactivate TFA if it was set to "deactivate TFA for next login"
880 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
881 $stmt->execute(array(':user' => $user));
882 $_SESSION['return'][] = array(
883 'type' => 'success',
884 'log' => array(__FUNCTION__, $user, '*'),
885 'msg' => array('logged_in_as', $user)
886 );
887 return "domainadmin";
888 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200889 }
890 }
891 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100892 INNER JOIN domain on mailbox.domain = domain.domain
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200893 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100894 AND `mailbox`.`active`='1'
895 AND `domain`.`active`='1'
896 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200897 $stmt->execute(array(':user' => $user));
898 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
899 foreach ($rows as $row) {
900 if (verify_hash($row['password'], $pass) !== false) {
901 unset($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100902 $_SESSION['return'][] = array(
903 'type' => 'success',
904 'log' => array(__FUNCTION__, $user, '*'),
905 'msg' => array('logged_in_as', $user)
906 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200907 return "user";
908 }
909 }
910 if (!isset($_SESSION['ldelay'])) {
911 $_SESSION['ldelay'] = "0";
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100912 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
913 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200914 }
915 elseif (!isset($_SESSION['mailcow_cc_username'])) {
916 $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100917 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200918 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
919 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100920 $_SESSION['return'][] = array(
921 'type' => 'danger',
922 'log' => array(__FUNCTION__, $user, '*'),
923 'msg' => 'login_failed'
924 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200925 sleep($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100926 return false;
927}
928function formatBytes($size, $precision = 2) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200929 if(!is_numeric($size)) {
930 return "0";
931 }
932 $base = log($size, 1024);
933 $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
934 if ($size == "0") {
935 return "0";
936 }
937 return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100938}
939function update_sogo_static_view() {
940 if (getenv('SKIP_SOGO') == "y") {
941 return true;
942 }
943 global $pdo;
944 global $lang;
945 $stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
946 WHERE TABLE_NAME = 'sogo_view'");
947 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
948 if ($num_results != 0) {
949 $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
950 SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
951 $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
952 }
953 flush_memcached();
954}
955function edit_user_account($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200956 global $lang;
957 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100958 $_data_log = $_data;
959 !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
960 !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
961 !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
962 $username = $_SESSION['mailcow_cc_username'];
963 $role = $_SESSION['mailcow_cc_role'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200964 $password_old = $_data['user_old_pass'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100965 if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
966 $_SESSION['return'][] = array(
967 'type' => 'danger',
968 'log' => array(__FUNCTION__, $_data_log),
969 'msg' => 'access_denied'
970 );
971 return false;
972 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200973 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
974 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100975 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200976 $stmt->execute(array(':user' => $username));
977 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100978 if (!verify_hash($row['password'], $password_old)) {
979 $_SESSION['return'][] = array(
980 'type' => 'danger',
981 'log' => array(__FUNCTION__, $_data_log),
982 'msg' => 'access_denied'
983 );
984 return false;
985 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200986 if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
987 $password_new = $_data['user_new_pass'];
988 $password_new2 = $_data['user_new_pass2'];
989 if (password_check($password_new, $password_new2) !== true) {
990 return false;
991 }
992 $password_hashed = hash_password($password_new);
993 $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
994 `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
995 `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
996 WHERE `username` = :username");
997 $stmt->execute(array(
998 ':password_hashed' => $password_hashed,
999 ':username' => $username
1000 ));
1001 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001002 update_sogo_static_view();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001003 $_SESSION['return'][] = array(
1004 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001005 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001006 'msg' => array('mailbox_modified', htmlspecialchars($username))
1007 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001008}
1009function user_get_alias_details($username) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001010 global $pdo;
1011 global $lang;
1012 $data['direct_aliases'] = array();
1013 $data['shared_aliases'] = array();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001014 if ($_SESSION['mailcow_cc_role'] == "user") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001015 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001016 }
1017 if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
1018 return false;
1019 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001020 if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
1021 return false;
1022 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001023 $data['address'] = $username;
1024 $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
1025 WHERE `goto` REGEXP :username_goto
1026 AND `address` NOT LIKE '@%'
1027 AND `goto` != :username_goto2
1028 AND `address` != :username_address");
1029 $stmt->execute(array(
1030 ':username_goto' => '(^|,)'.$username.'($|,)',
1031 ':username_goto2' => $username,
1032 ':username_address' => $username
1033 ));
1034 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1035 while ($row = array_shift($run)) {
1036 $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001037 //$data['shared_aliases'][] = $row['shared_aliases'];
1038 }
1039
1040 $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
1041 WHERE `goto` = :username_goto
1042 AND `address` NOT LIKE '@%'
1043 AND `address` != :username_address");
1044 $stmt->execute(
1045 array(
1046 ':username_goto' => $username,
1047 ':username_address' => $username
1048 ));
1049 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1050 while ($row = array_shift($run)) {
1051 $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
1052 }
1053 $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
1054 LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
1055 WHERE `username` = :username ;");
1056 $stmt->execute(array(':username' => $username));
1057 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1058 while ($row = array_shift($run)) {
1059 if (empty($row['ad_alias'])) {
1060 continue;
1061 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001062 $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
1063 $data['alias_domains'][] = $row['alias_domain'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001064 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001065 $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 +01001066 $stmt->execute(array(':username' => $username));
1067 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1068 while ($row = array_shift($run)) {
1069 $data['aliases_also_send_as'] = $row['send_as'];
1070 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001071 $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 +01001072 $stmt->execute(array(':username' => $username));
1073 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1074 while ($row = array_shift($run)) {
1075 $data['aliases_send_as_all'] = $row['send_as'];
1076 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001077 $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 +01001078 $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
1079 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1080 while ($row = array_shift($run)) {
1081 $data['is_catch_all'] = $row['address'];
1082 }
1083 return $data;
1084}
1085function is_valid_domain_name($domain_name) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001086 if (empty($domain_name)) {
1087 return false;
1088 }
1089 $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
1090 return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
1091 && preg_match("/^.{1,253}$/", $domain_name)
1092 && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001093}
1094function set_tfa($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001095 global $pdo;
1096 global $yubi;
1097 global $u2f;
1098 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001099 $_data_log = $_data;
1100 !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
1101 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001102 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001103 $_SESSION['return'][] = array(
1104 'type' => 'danger',
1105 'log' => array(__FUNCTION__, $_data_log),
1106 'msg' => 'access_denied'
1107 );
1108 return false;
1109 }
1110 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001111 WHERE `username` = :username");
1112 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001113 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001114 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1115 if (!empty($num_results)) {
1116 if (!verify_hash($row['password'], $_data["confirm_password"])) {
1117 $_SESSION['return'][] = array(
1118 'type' => 'danger',
1119 'log' => array(__FUNCTION__, $_data_log),
1120 'msg' => 'access_denied'
1121 );
1122 return false;
1123 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001124 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001125 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1126 WHERE `username` = :username");
1127 $stmt->execute(array(':username' => $username));
1128 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1129 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1130 if (!empty($num_results)) {
1131 if (!verify_hash($row['password'], $_data["confirm_password"])) {
1132 $_SESSION['return'][] = array(
1133 'type' => 'danger',
1134 'log' => array(__FUNCTION__, $_data_log),
1135 'msg' => 'access_denied'
1136 );
1137 return false;
1138 }
1139 }
1140 switch ($_data["tfa_method"]) {
1141 case "yubi_otp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001142 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1143 $yubico_id = $_data['yubico_id'];
1144 $yubico_key = $_data['yubico_key'];
1145 $yubi = new Auth_Yubico($yubico_id, $yubico_key);
1146 if (!$yubi) {
1147 $_SESSION['return'][] = array(
1148 'type' => 'danger',
1149 'log' => array(__FUNCTION__, $_data_log),
1150 'msg' => 'access_denied'
1151 );
1152 return false;
1153 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001154 if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
1155 $_SESSION['return'][] = array(
1156 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001157 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001158 'msg' => 'tfa_token_invalid'
1159 );
1160 return false;
1161 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001162 $yauth = $yubi->verify($_data["otp_token"]);
1163 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001164 $_SESSION['return'][] = array(
1165 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001166 'log' => array(__FUNCTION__, $_data_log),
1167 'msg' => array('yotp_verification_failed', $yauth->getMessage())
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001168 );
1169 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001170 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001171 try {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001172 // We could also do a modhex translation here
1173 $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
1174 $stmt = $pdo->prepare("DELETE FROM `tfa`
1175 WHERE `username` = :username
1176 AND (`authmech` != 'yubi_otp')
1177 OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001178 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1179 $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
1180 (:key_id, :username, 'yubi_otp', '1', :secret)");
1181 $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
1182 }
1183 catch (PDOException $e) {
1184 $_SESSION['return'][] = array(
1185 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001186 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001187 'msg' => array('mysql_error', $e)
1188 );
1189 return false;
1190 }
1191 $_SESSION['return'][] = array(
1192 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001193 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001194 'msg' => array('object_modified', htmlspecialchars($username))
1195 );
1196 break;
1197 case "u2f":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001198 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1199 try {
1200 $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_data['token']));
1201 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001202 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001203 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
1204 $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
1205 $_SESSION['return'][] = array(
1206 'type' => 'success',
1207 'log' => array(__FUNCTION__, $_data_log),
1208 'msg' => array('object_modified', $username)
1209 );
1210 $_SESSION['regReq'] = null;
1211 }
1212 catch (Exception $e) {
1213 $_SESSION['return'][] = array(
1214 'type' => 'danger',
1215 'log' => array(__FUNCTION__, $_data_log),
1216 'msg' => array('u2f_verification_failed', $e->getMessage())
1217 );
1218 $_SESSION['regReq'] = null;
1219 return false;
1220 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001221 break;
1222 case "totp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001223 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1224 if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
1225 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1226 $stmt->execute(array(':username' => $username));
1227 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
1228 $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
1229 $_SESSION['return'][] = array(
1230 'type' => 'success',
1231 'log' => array(__FUNCTION__, $_data_log),
1232 'msg' => array('object_modified', $username)
1233 );
1234 }
1235 else {
1236 $_SESSION['return'][] = array(
1237 'type' => 'danger',
1238 'log' => array(__FUNCTION__, $_data_log),
1239 'msg' => 'totp_verification_failed'
1240 );
1241 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001242 break;
1243 case "none":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001244 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1245 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001246 $_SESSION['return'][] = array(
1247 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001248 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001249 'msg' => array('object_modified', htmlspecialchars($username))
1250 );
1251 break;
1252 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001253}
1254function fido2($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001255 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001256 $_data_log = $_data;
1257 // Not logging registration data, only actions
1258 // Silent errors for "get" requests
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001259 switch ($_data["action"]) {
1260 case "register":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001261 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001262 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001263 $_SESSION['return'][] = array(
1264 'type' => 'danger',
1265 'log' => array(__FUNCTION__, $_data["action"]),
1266 'msg' => 'access_denied'
1267 );
1268 return false;
1269 }
1270 $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
1271 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1272 $stmt->execute(array(
1273 $username,
1274 $_data['registration']->rpId,
1275 $_data['registration']->credentialPublicKey,
1276 $_data['registration']->certificateChain,
1277 $_data['registration']->certificate,
1278 $_data['registration']->certificateIssuer,
1279 $_data['registration']->certificateSubject,
1280 $_data['registration']->signatureCounter,
1281 $_data['registration']->AAGUID,
1282 $_data['registration']->credentialId)
1283 );
1284 $_SESSION['return'][] = array(
1285 'type' => 'success',
1286 'log' => array(__FUNCTION__, $_data["action"]),
1287 'msg' => array('object_modified', $username)
1288 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001289 break;
1290 case "get_user_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001291 // Used to exclude existing CredentialIds while registering
1292 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001293 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1294 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001295 }
1296 $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
1297 $stmt->execute(array(':username' => $username));
1298 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1299 while($row = array_shift($rows)) {
1300 $cids[] = $row['credentialId'];
1301 }
1302 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001303 break;
1304 case "get_all_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001305 // Only needed when using fido2 with username
1306 $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
1307 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1308 while($row = array_shift($rows)) {
1309 $cids[] = $row['credentialId'];
1310 }
1311 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001312 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001313 case "get_by_b64cid":
1314 if (!isset($_data['cid']) || empty($_data['cid'])) {
1315 return false;
1316 }
1317 $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");
1318 $stmt->execute(array(':cid' => $_data['cid']));
1319 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1320 if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
1321 return false;
1322 }
1323 $data['pub_key'] = $row['credentialPublicKey'];
1324 $data['username'] = $row['username'];
1325 $data['subject'] = $row['certificateSubject'];
1326 $data['cid'] = $row['cid'];
1327 return $data;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001328 break;
1329 case "get_friendly_names":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001330 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001331 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1332 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001333 }
1334 $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
1335 $stmt->execute(array(':username' => $username));
1336 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1337 while($row = array_shift($rows)) {
1338 $fns[] = array(
1339 "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
1340 "fn" => $row['friendlyName'],
1341 "cid" => $row['cid']
1342 );
1343 }
1344 return $fns;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001345 break;
1346 case "unset_fido2_key":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001347 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001348 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1349 $_SESSION['return'][] = array(
1350 'type' => 'danger',
1351 'log' => array(__FUNCTION__, $_data["action"]),
1352 'msg' => 'access_denied'
1353 );
1354 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001355 }
1356 $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
1357 $stmt->execute(array(
1358 ':username' => $username,
1359 ':cid' => $_data['post_data']['unset_fido2_key']
1360 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001361 $_SESSION['return'][] = array(
1362 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001363 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001364 'msg' => array('object_modified', htmlspecialchars($username))
1365 );
1366 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001367 case "edit_fn":
1368 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001369 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1370 $_SESSION['return'][] = array(
1371 'type' => 'danger',
1372 'log' => array(__FUNCTION__, $_data["action"]),
1373 'msg' => 'access_denied'
1374 );
1375 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001376 }
1377 $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
1378 $stmt->execute(array(
1379 ':username' => $username,
1380 ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
1381 ':cid' => $_data['fido2_attrs']['fido2_cid']
1382 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001383 $_SESSION['return'][] = array(
1384 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001385 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001386 'msg' => array('object_modified', htmlspecialchars($username))
1387 );
1388 break;
1389 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001390}
1391function unset_tfa_key($_data) {
1392 // Can only unset own keys
1393 // Needs at least one key left
1394 global $pdo;
1395 global $lang;
1396 $_data_log = $_data;
1397 $id = intval($_data['unset_tfa_key']);
1398 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001399 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1400 $_SESSION['return'][] = array(
1401 'type' => 'danger',
1402 'log' => array(__FUNCTION__, $_data_log),
1403 'msg' => 'access_denied'
1404 );
1405 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001406 }
1407 try {
1408 if (!is_numeric($id)) {
1409 $_SESSION['return'][] = array(
1410 'type' => 'danger',
1411 'log' => array(__FUNCTION__, $_data_log),
1412 'msg' => 'access_denied'
1413 );
1414 return false;
1415 }
1416 $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
1417 WHERE `username` = :username AND `active` = '1'");
1418 $stmt->execute(array(':username' => $username));
1419 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1420 if ($row['keys'] == "1") {
1421 $_SESSION['return'][] = array(
1422 'type' => 'danger',
1423 'log' => array(__FUNCTION__, $_data_log),
1424 'msg' => 'last_key'
1425 );
1426 return false;
1427 }
1428 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
1429 $stmt->execute(array(':username' => $username, ':id' => $id));
1430 $_SESSION['return'][] = array(
1431 'type' => 'success',
1432 'log' => array(__FUNCTION__, $_data_log),
1433 'msg' => array('object_modified', $username)
1434 );
1435 }
1436 catch (PDOException $e) {
1437 $_SESSION['return'][] = array(
1438 'type' => 'danger',
1439 'log' => array(__FUNCTION__, $_data_log),
1440 'msg' => array('mysql_error', $e)
1441 );
1442 return false;
1443 }
1444}
1445function get_tfa($username = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001446 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001447 if (isset($_SESSION['mailcow_cc_username'])) {
1448 $username = $_SESSION['mailcow_cc_username'];
1449 }
1450 elseif (empty($username)) {
1451 return false;
1452 }
1453 $stmt = $pdo->prepare("SELECT * FROM `tfa`
1454 WHERE `username` = :username AND `active` = '1'");
1455 $stmt->execute(array(':username' => $username));
1456 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1457
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001458 if (isset($row["authmech"])) {
1459 switch ($row["authmech"]) {
1460 case "yubi_otp":
1461 $data['name'] = "yubi_otp";
1462 $data['pretty'] = "Yubico OTP";
1463 $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
1464 $stmt->execute(array(
1465 ':username' => $username,
1466 ));
1467 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1468 while($row = array_shift($rows)) {
1469 $data['additional'][] = $row;
1470 }
1471 return $data;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001472 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001473 case "u2f":
1474 $data['name'] = "u2f";
1475 $data['pretty'] = "Fido U2F";
1476 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1477 $stmt->execute(array(
1478 ':username' => $username,
1479 ));
1480 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1481 while($row = array_shift($rows)) {
1482 $data['additional'][] = $row;
1483 }
1484 return $data;
1485 break;
1486 case "hotp":
1487 $data['name'] = "hotp";
1488 $data['pretty'] = "HMAC-based OTP";
1489 return $data;
1490 break;
1491 case "totp":
1492 $data['name'] = "totp";
1493 $data['pretty'] = "Time-based OTP";
1494 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
1495 $stmt->execute(array(
1496 ':username' => $username,
1497 ));
1498 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1499 while($row = array_shift($rows)) {
1500 $data['additional'][] = $row;
1501 }
1502 return $data;
1503 break;
1504 default:
1505 $data['name'] = 'none';
1506 $data['pretty'] = "-";
1507 return $data;
1508 break;
1509 }
1510 }
1511 else {
1512 $data['name'] = 'none';
1513 $data['pretty'] = "-";
1514 return $data;
1515 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001516}
1517function verify_tfa_login($username, $token) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001518 global $pdo;
1519 global $yubi;
1520 global $u2f;
1521 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001522 $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
1523 WHERE `username` = :username AND `active` = '1'");
1524 $stmt->execute(array(':username' => $username));
1525 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1526
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001527 switch ($row["authmech"]) {
1528 case "yubi_otp":
1529 if (!ctype_alnum($token) || strlen($token) != 44) {
1530 $_SESSION['return'][] = array(
1531 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001532 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001533 'msg' => array('yotp_verification_failed', 'token length error')
1534 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001535 return false;
1536 }
1537 $yubico_modhex_id = substr($token, 0, 12);
1538 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1539 WHERE `username` = :username
1540 AND `authmech` = 'yubi_otp'
1541 AND `active`='1'
1542 AND `secret` LIKE :modhex");
1543 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1544 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1545 $yubico_auth = explode(':', $row['secret']);
1546 $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
1547 $yauth = $yubi->verify($token);
1548 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001549 $_SESSION['return'][] = array(
1550 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001551 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001552 'msg' => array('yotp_verification_failed', $yauth->getMessage())
1553 );
1554 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001555 }
1556 else {
1557 $_SESSION['tfa_id'] = $row['id'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001558 $_SESSION['return'][] = array(
1559 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001560 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001561 'msg' => 'verified_yotp_login'
1562 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001563 return true;
1564 }
1565 $_SESSION['return'][] = array(
1566 'type' => 'danger',
1567 'log' => array(__FUNCTION__, $username, '*'),
1568 'msg' => array('yotp_verification_failed', 'unknown')
1569 );
1570 return false;
1571 break;
1572 case "u2f":
1573 try {
1574 $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
1575 $stmt = $pdo->prepare("SELECT `id` FROM `tfa` WHERE `keyHandle` = ?");
1576 $stmt->execute(array($reg->keyHandle));
1577 $row_key_id = $stmt->fetch(PDO::FETCH_ASSOC);
1578 $_SESSION['tfa_id'] = $row_key_id['id'];
1579 $_SESSION['authReq'] = null;
1580 $_SESSION['return'][] = array(
1581 'type' => 'success',
1582 'log' => array(__FUNCTION__, $username, '*'),
1583 'msg' => 'verified_u2f_login'
1584 );
1585 return true;
1586 }
1587 catch (Exception $e) {
1588 $_SESSION['return'][] = array(
1589 'type' => 'danger',
1590 'log' => array(__FUNCTION__, $username, '*'),
1591 'msg' => array('u2f_verification_failed', $e->getMessage())
1592 );
1593 $_SESSION['regReq'] = null;
1594 return false;
1595 }
1596 $_SESSION['return'][] = array(
1597 'type' => 'danger',
1598 'log' => array(__FUNCTION__, $username, '*'),
1599 'msg' => array('u2f_verification_failed', 'unknown')
1600 );
1601 return false;
1602 break;
1603 case "hotp":
1604 return false;
1605 break;
1606 case "totp":
1607 try {
1608 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1609 WHERE `username` = :username
1610 AND `authmech` = 'totp'
1611 AND `active`='1'");
1612 $stmt->execute(array(':username' => $username));
1613 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1614 if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) {
1615 $_SESSION['tfa_id'] = $row['id'];
1616 $_SESSION['return'][] = array(
1617 'type' => 'success',
1618 'log' => array(__FUNCTION__, $username, '*'),
1619 'msg' => 'verified_totp_login'
1620 );
1621 return true;
1622 }
1623 $_SESSION['return'][] = array(
1624 'type' => 'danger',
1625 'log' => array(__FUNCTION__, $username, '*'),
1626 'msg' => 'totp_verification_failed'
1627 );
1628 return false;
1629 }
1630 catch (PDOException $e) {
1631 $_SESSION['return'][] = array(
1632 'type' => 'danger',
1633 'log' => array(__FUNCTION__, $username, '*'),
1634 'msg' => array('mysql_error', $e)
1635 );
1636 return false;
1637 }
1638 break;
1639 default:
1640 $_SESSION['return'][] = array(
1641 'type' => 'danger',
1642 'log' => array(__FUNCTION__, $username, '*'),
1643 'msg' => 'unknown_tfa_method'
1644 );
1645 return false;
1646 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001647 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001648 return false;
1649}
1650function admin_api($access, $action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001651 global $pdo;
1652 if ($_SESSION['mailcow_cc_role'] != "admin") {
1653 $_SESSION['return'][] = array(
1654 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001655 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001656 'msg' => 'access_denied'
1657 );
1658 return false;
1659 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001660 if ($access !== "ro" && $access !== "rw") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001661 $_SESSION['return'][] = array(
1662 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001663 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001664 'msg' => 'invalid access type'
1665 );
1666 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001667 }
1668 if ($action == "edit") {
1669 $active = (!empty($data['active'])) ? 1 : 0;
1670 $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001671 $allow_from = array();
1672 if (isset($data['allow_from'])) {
1673 $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
1674 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001675 foreach ($allow_from as $key => $val) {
1676 if (empty($val)) {
1677 unset($allow_from[$key]);
1678 continue;
1679 }
1680 if (valid_network($val) !== true) {
1681 $_SESSION['return'][] = array(
1682 'type' => 'warning',
1683 'log' => array(__FUNCTION__, $data),
1684 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
1685 );
1686 unset($allow_from[$key]);
1687 continue;
1688 }
1689 }
1690 $allow_from = implode(',', array_unique(array_filter($allow_from)));
1691 if (empty($allow_from) && $skip_ip_check == 0) {
1692 $_SESSION['return'][] = array(
1693 'type' => 'danger',
1694 'log' => array(__FUNCTION__, $data),
1695 'msg' => 'ip_list_empty'
1696 );
1697 return false;
1698 }
1699 $api_key = implode('-', array(
1700 strtoupper(bin2hex(random_bytes(3))),
1701 strtoupper(bin2hex(random_bytes(3))),
1702 strtoupper(bin2hex(random_bytes(3))),
1703 strtoupper(bin2hex(random_bytes(3))),
1704 strtoupper(bin2hex(random_bytes(3)))
1705 ));
1706 $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
1707 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1708 if (empty($num_results)) {
1709 $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
1710 VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
1711 $stmt->execute(array(
1712 ':api_key' => $api_key,
1713 ':skip_ip_check' => $skip_ip_check,
1714 ':active' => $active,
1715 ':allow_from' => $allow_from,
1716 ':access' => $access
1717 ));
1718 }
1719 else {
1720 if ($skip_ip_check == 0) {
1721 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1722 `active` = :active,
1723 `allow_from` = :allow_from
1724 WHERE `access` = :access;");
1725 $stmt->execute(array(
1726 ':active' => $active,
1727 ':skip_ip_check' => $skip_ip_check,
1728 ':allow_from' => $allow_from,
1729 ':access' => $access
1730 ));
1731 }
1732 else {
1733 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1734 `active` = :active
1735 WHERE `access` = :access;");
1736 $stmt->execute(array(
1737 ':active' => $active,
1738 ':skip_ip_check' => $skip_ip_check,
1739 ':access' => $access
1740 ));
1741 }
1742 }
1743 }
1744 elseif ($action == "regen_key") {
1745 $api_key = implode('-', array(
1746 strtoupper(bin2hex(random_bytes(3))),
1747 strtoupper(bin2hex(random_bytes(3))),
1748 strtoupper(bin2hex(random_bytes(3))),
1749 strtoupper(bin2hex(random_bytes(3))),
1750 strtoupper(bin2hex(random_bytes(3)))
1751 ));
1752 $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
1753 $stmt->execute(array(
1754 ':api_key' => $api_key,
1755 ':access' => $access
1756 ));
1757 }
1758 elseif ($action == "get") {
1759 $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
1760 $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001761 if ($apidata !== false) {
1762 $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
1763 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001764 return $apidata;
1765 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001766 $_SESSION['return'][] = array(
1767 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001768 'log' => array(__FUNCTION__, $data),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001769 'msg' => 'admin_api_modified'
1770 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001771}
1772function license($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001773 global $pdo;
1774 global $redis;
1775 global $lang;
1776 if ($_SESSION['mailcow_cc_role'] != "admin") {
1777 $_SESSION['return'][] = array(
1778 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001779 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001780 'msg' => 'access_denied'
1781 );
1782 return false;
1783 }
1784 switch ($action) {
1785 case "verify":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001786 // Keep result until revalidate button is pressed or session expired
1787 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
1788 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
1789 $post = array('guid' => $versions['version']);
1790 $curl = curl_init('https://verify.mailcow.email');
1791 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1792 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
1793 curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
1794 $response = curl_exec($curl);
1795 curl_close($curl);
1796 $json_return = json_decode($response, true);
1797 if ($response && $json_return) {
1798 if ($json_return['response'] === "ok") {
1799 $_SESSION['gal']['valid'] = "true";
1800 $_SESSION['gal']['c'] = $json_return['c'];
1801 $_SESSION['gal']['s'] = $json_return['s'];
1802 if ($json_return['m'] == 'NoMoore') {
1803 $_SESSION['gal']['m'] = '🐄';
1804 }
1805 else {
1806 $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
1807 }
1808 }
1809 elseif ($json_return['response'] === "invalid") {
1810 $_SESSION['gal']['valid'] = "false";
1811 $_SESSION['gal']['c'] = $lang['mailbox']['no'];
1812 $_SESSION['gal']['s'] = $lang['mailbox']['no'];
1813 $_SESSION['gal']['m'] = $lang['mailbox']['no'];
1814 }
1815 }
1816 else {
1817 $_SESSION['gal']['valid'] = "false";
1818 $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
1819 $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
1820 $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
1821 }
1822 try {
1823 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
1824 $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
1825 }
1826 catch (RedisException $e) {
1827 $_SESSION['return'][] = array(
1828 'type' => 'danger',
1829 'log' => array(__FUNCTION__, $_action, $_data_log),
1830 'msg' => array('redis_error', $e)
1831 );
1832 return false;
1833 }
1834 return $_SESSION['gal']['valid'];
1835 break;
1836 case "guid":
1837 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
1838 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
1839 return $versions['version'];
1840 break;
1841 }
1842}
1843function rspamd_ui($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001844 if ($_SESSION['mailcow_cc_role'] != "admin") {
1845 $_SESSION['return'][] = array(
1846 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001847 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001848 'msg' => 'access_denied'
1849 );
1850 return false;
1851 }
1852 switch ($action) {
1853 case "edit":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001854 $rspamd_ui_pass = $data['rspamd_ui_pass'];
1855 $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
1856 if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
1857 $_SESSION['return'][] = array(
1858 'type' => 'danger',
1859 'log' => array(__FUNCTION__, '*', '*'),
1860 'msg' => 'password_empty'
1861 );
1862 return false;
1863 }
1864 if ($rspamd_ui_pass != $rspamd_ui_pass2) {
1865 $_SESSION['return'][] = array(
1866 'type' => 'danger',
1867 'log' => array(__FUNCTION__, '*', '*'),
1868 'msg' => 'password_mismatch'
1869 );
1870 return false;
1871 }
1872 if (strlen($rspamd_ui_pass) < 6) {
1873 $_SESSION['return'][] = array(
1874 'type' => 'danger',
1875 'log' => array(__FUNCTION__, '*', '*'),
1876 'msg' => 'rspamd_ui_pw_length'
1877 );
1878 return false;
1879 }
1880 $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
1881 if ($docker_return_array = json_decode($docker_return, true)) {
1882 if ($docker_return_array['type'] == 'success') {
1883 $_SESSION['return'][] = array(
1884 'type' => 'success',
1885 'log' => array(__FUNCTION__, '*', '*'),
1886 'msg' => 'rspamd_ui_pw_set'
1887 );
1888 return true;
1889 }
1890 else {
1891 $_SESSION['return'][] = array(
1892 'type' => $docker_return_array['type'],
1893 'log' => array(__FUNCTION__, '*', '*'),
1894 'msg' => $docker_return_array['msg']
1895 );
1896 return false;
1897 }
1898 }
1899 else {
1900 $_SESSION['return'][] = array(
1901 'type' => 'danger',
1902 'log' => array(__FUNCTION__, '*', '*'),
1903 'msg' => 'unknown'
1904 );
1905 return false;
1906 }
1907 break;
1908 }
1909}
1910function get_u2f_registrations($username) {
1911 global $pdo;
1912 $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
1913 $sel->execute(array($username));
1914 return $sel->fetchAll(PDO::FETCH_OBJ);
1915}
1916function get_logs($application, $lines = false) {
1917 if ($lines === false) {
1918 $lines = $GLOBALS['LOG_LINES'] - 1;
1919 }
1920 elseif(is_numeric($lines) && $lines >= 1) {
1921 $lines = abs(intval($lines) - 1);
1922 }
1923 else {
1924 list ($from, $to) = explode('-', $lines);
1925 $from = intval($from);
1926 $to = intval($to);
1927 if ($from < 1 || $to < $from) { return false; }
1928 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001929 global $redis;
1930 global $pdo;
1931 if ($_SESSION['mailcow_cc_role'] != "admin") {
1932 return false;
1933 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001934 // SQL
1935 if ($application == "mailcow-ui") {
1936 if (isset($from) && isset($to)) {
1937 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
1938 $stmt->execute(array(
1939 ':from' => $from - 1,
1940 ':to' => $to
1941 ));
1942 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1943 }
1944 else {
1945 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
1946 $stmt->execute(array(
1947 ':lines' => $lines + 1,
1948 ));
1949 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1950 }
1951 if (is_array($data)) {
1952 return $data;
1953 }
1954 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001955 if ($application == "sasl") {
1956 if (isset($from) && isset($to)) {
1957 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
1958 $stmt->execute(array(
1959 ':from' => $from - 1,
1960 ':to' => $to
1961 ));
1962 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1963 }
1964 else {
1965 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
1966 $stmt->execute(array(
1967 ':lines' => $lines + 1,
1968 ));
1969 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1970 }
1971 if (is_array($data)) {
1972 return $data;
1973 }
1974 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001975 // Redis
1976 if ($application == "dovecot-mailcow") {
1977 if (isset($from) && isset($to)) {
1978 $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
1979 }
1980 else {
1981 $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
1982 }
1983 if ($data) {
1984 foreach ($data as $json_line) {
1985 $data_array[] = json_decode($json_line, true);
1986 }
1987 return $data_array;
1988 }
1989 }
1990 if ($application == "postfix-mailcow") {
1991 if (isset($from) && isset($to)) {
1992 $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
1993 }
1994 else {
1995 $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
1996 }
1997 if ($data) {
1998 foreach ($data as $json_line) {
1999 $data_array[] = json_decode($json_line, true);
2000 }
2001 return $data_array;
2002 }
2003 }
2004 if ($application == "sogo-mailcow") {
2005 if (isset($from) && isset($to)) {
2006 $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
2007 }
2008 else {
2009 $data = $redis->lRange('SOGO_LOG', 0, $lines);
2010 }
2011 if ($data) {
2012 foreach ($data as $json_line) {
2013 $data_array[] = json_decode($json_line, true);
2014 }
2015 return $data_array;
2016 }
2017 }
2018 if ($application == "watchdog-mailcow") {
2019 if (isset($from) && isset($to)) {
2020 $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
2021 }
2022 else {
2023 $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
2024 }
2025 if ($data) {
2026 foreach ($data as $json_line) {
2027 $data_array[] = json_decode($json_line, true);
2028 }
2029 return $data_array;
2030 }
2031 }
2032 if ($application == "acme-mailcow") {
2033 if (isset($from) && isset($to)) {
2034 $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
2035 }
2036 else {
2037 $data = $redis->lRange('ACME_LOG', 0, $lines);
2038 }
2039 if ($data) {
2040 foreach ($data as $json_line) {
2041 $data_array[] = json_decode($json_line, true);
2042 }
2043 return $data_array;
2044 }
2045 }
2046 if ($application == "ratelimited") {
2047 if (isset($from) && isset($to)) {
2048 $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
2049 }
2050 else {
2051 $data = $redis->lRange('RL_LOG', 0, $lines);
2052 }
2053 if ($data) {
2054 foreach ($data as $json_line) {
2055 $data_array[] = json_decode($json_line, true);
2056 }
2057 return $data_array;
2058 }
2059 }
2060 if ($application == "api-mailcow") {
2061 if (isset($from) && isset($to)) {
2062 $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
2063 }
2064 else {
2065 $data = $redis->lRange('API_LOG', 0, $lines);
2066 }
2067 if ($data) {
2068 foreach ($data as $json_line) {
2069 $data_array[] = json_decode($json_line, true);
2070 }
2071 return $data_array;
2072 }
2073 }
2074 if ($application == "netfilter-mailcow") {
2075 if (isset($from) && isset($to)) {
2076 $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
2077 }
2078 else {
2079 $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
2080 }
2081 if ($data) {
2082 foreach ($data as $json_line) {
2083 $data_array[] = json_decode($json_line, true);
2084 }
2085 return $data_array;
2086 }
2087 }
2088 if ($application == "autodiscover-mailcow") {
2089 if (isset($from) && isset($to)) {
2090 $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
2091 }
2092 else {
2093 $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
2094 }
2095 if ($data) {
2096 foreach ($data as $json_line) {
2097 $data_array[] = json_decode($json_line, true);
2098 }
2099 return $data_array;
2100 }
2101 }
2102 if ($application == "rspamd-history") {
2103 $curl = curl_init();
2104 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2105 if (!is_numeric($lines)) {
2106 list ($from, $to) = explode('-', $lines);
2107 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
2108 }
2109 else {
2110 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
2111 }
2112 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2113 $history = curl_exec($curl);
2114 if (!curl_errno($curl)) {
2115 $data_array = json_decode($history, true);
2116 curl_close($curl);
2117 return $data_array['rows'];
2118 }
2119 curl_close($curl);
2120 return false;
2121 }
2122 if ($application == "rspamd-stats") {
2123 $curl = curl_init();
2124 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2125 curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
2126 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2127 $stats = curl_exec($curl);
2128 if (!curl_errno($curl)) {
2129 $data_array = json_decode($stats, true);
2130 curl_close($curl);
2131 return $data_array;
2132 }
2133 curl_close($curl);
2134 return false;
2135 }
2136 return false;
2137}
2138function getGUID() {
2139 if (function_exists('com_create_guid')) {
2140 return com_create_guid();
2141 }
2142 mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
2143 $charid = strtoupper(md5(uniqid(rand(), true)));
2144 $hyphen = chr(45);// "-"
2145 return substr($charid, 0, 8).$hyphen
2146 .substr($charid, 8, 4).$hyphen
2147 .substr($charid,12, 4).$hyphen
2148 .substr($charid,16, 4).$hyphen
2149 .substr($charid,20,12);
2150}
2151function solr_status() {
2152 $curl = curl_init();
2153 $endpoint = 'http://solr:8983/solr/admin/cores';
2154 $params = array(
2155 'action' => 'STATUS',
2156 'core' => 'dovecot-fts',
2157 'indexInfo' => 'true'
2158 );
2159 $url = $endpoint . '?' . http_build_query($params);
2160 curl_setopt($curl, CURLOPT_URL, $url);
2161 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2162 curl_setopt($curl, CURLOPT_POST, 0);
2163 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2164 $response_core = curl_exec($curl);
2165 if ($response_core === false) {
2166 $err = curl_error($curl);
2167 curl_close($curl);
2168 return false;
2169 }
2170 else {
2171 curl_close($curl);
2172 $curl = curl_init();
2173 $status_core = json_decode($response_core, true);
2174 $url = 'http://solr:8983/solr/admin/info/system';
2175 curl_setopt($curl, CURLOPT_URL, $url);
2176 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2177 curl_setopt($curl, CURLOPT_POST, 0);
2178 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2179 $response_sysinfo = curl_exec($curl);
2180 if ($response_sysinfo === false) {
2181 $err = curl_error($curl);
2182 curl_close($curl);
2183 return false;
2184 }
2185 else {
2186 curl_close($curl);
2187 $status_sysinfo = json_decode($response_sysinfo, true);
2188 $status = array_merge($status_core, $status_sysinfo);
2189 return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
2190 }
2191 return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
2192 }
2193 return false;
2194}
2195
2196function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
2197 $now = time();
2198 foreach (glob($folder) as $filename) {
2199 if(strpos($filename, $ignore) !== false) {
2200 continue;
2201 }
2202 if (is_file($filename)) {
2203 if ($now - filemtime($filename) >= 60 * 60) {
2204 unlink($filename);
2205 }
2206 }
2207 }
2208}
2209
2210function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
2211 $now = time();
2212 foreach (glob($folder) as $filename) {
2213 if(strpos($filename, $ignore) !== false) {
2214 continue;
2215 }
2216 if (is_file($filename)) {
2217 if ($now - filemtime($filename) >= 60 * 60) {
2218 unlink($filename);
2219 }
2220 }
2221 }
2222}
2223
2224?>