blob: 7ac0af58f292fa7d766d21d30b376a54ff27c70e [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)) {
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
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}
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100810function check_login($user, $pass, $app_passwd_data = false) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200811 global $pdo;
812 global $redis;
813 global $imap_server;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100814
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200815 if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100816 $_SESSION['return'][] = array(
817 'type' => 'danger',
818 'log' => array(__FUNCTION__, $user, '*'),
819 'msg' => 'malformed_username'
820 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200821 return false;
822 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100823
824 // Validate admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200825 $user = strtolower(trim($user));
826 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
827 WHERE `superadmin` = '1'
828 AND `active` = '1'
829 AND `username` = :user");
830 $stmt->execute(array(':user' => $user));
831 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
832 foreach ($rows as $row) {
833 if (verify_hash($row['password'], $pass)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100834 if (get_tfa($user)['name'] != "none") {
835 $_SESSION['pending_mailcow_cc_username'] = $user;
836 $_SESSION['pending_mailcow_cc_role'] = "admin";
837 $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
838 unset($_SESSION['ldelay']);
839 $_SESSION['return'][] = array(
840 'type' => 'info',
841 'log' => array(__FUNCTION__, $user, '*'),
842 'msg' => 'awaiting_tfa_confirmation'
843 );
844 return "pending";
845 }
846 else {
847 unset($_SESSION['ldelay']);
848 // Reactivate TFA if it was set to "deactivate TFA for next login"
849 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
850 $stmt->execute(array(':user' => $user));
851 $_SESSION['return'][] = array(
852 'type' => 'success',
853 'log' => array(__FUNCTION__, $user, '*'),
854 'msg' => array('logged_in_as', $user)
855 );
856 return "admin";
857 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200858 }
859 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100860
861 // Validate domain admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200862 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
863 WHERE `superadmin` = '0'
864 AND `active`='1'
865 AND `username` = :user");
866 $stmt->execute(array(':user' => $user));
867 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
868 foreach ($rows as $row) {
869 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100870 if (get_tfa($user)['name'] != "none") {
871 $_SESSION['pending_mailcow_cc_username'] = $user;
872 $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
873 $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
874 unset($_SESSION['ldelay']);
875 $_SESSION['return'][] = array(
876 'type' => 'info',
877 'log' => array(__FUNCTION__, $user, '*'),
878 'msg' => 'awaiting_tfa_confirmation'
879 );
880 return "pending";
881 }
882 else {
883 unset($_SESSION['ldelay']);
884 // Reactivate TFA if it was set to "deactivate TFA for next login"
885 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
886 $stmt->execute(array(':user' => $user));
887 $_SESSION['return'][] = array(
888 'type' => 'success',
889 'log' => array(__FUNCTION__, $user, '*'),
890 'msg' => array('logged_in_as', $user)
891 );
892 return "domainadmin";
893 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200894 }
895 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100896
897 // Validate mailbox user
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200898 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100899 INNER JOIN domain on mailbox.domain = domain.domain
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200900 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100901 AND `mailbox`.`active`='1'
902 AND `domain`.`active`='1'
903 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200904 $stmt->execute(array(':user' => $user));
905 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100906 if ($app_passwd_data['eas'] === true) {
907 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
908 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
909 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
910 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
911 AND `mailbox`.`active` = '1'
912 AND `domain`.`active` = '1'
913 AND `app_passwd`.`active` = '1'
914 AND `app_passwd`.`eas_access` = '1'
915 AND `app_passwd`.`mailbox` = :user");
916 $stmt->execute(array(':user' => $user));
917 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
918 }
919 elseif ($app_passwd_data['dav'] === true) {
920 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
921 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
922 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
923 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
924 AND `mailbox`.`active` = '1'
925 AND `domain`.`active` = '1'
926 AND `app_passwd`.`active` = '1'
927 AND `app_passwd`.`dav_access` = '1'
928 AND `app_passwd`.`mailbox` = :user");
929 $stmt->execute(array(':user' => $user));
930 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
931 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200932 foreach ($rows as $row) {
933 if (verify_hash($row['password'], $pass) !== false) {
934 unset($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100935 $_SESSION['return'][] = array(
936 'type' => 'success',
937 'log' => array(__FUNCTION__, $user, '*'),
938 'msg' => array('logged_in_as', $user)
939 );
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100940 if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
941 $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
942 $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
943 $stmt->execute(array(
944 ':service' => $service,
945 ':app_id' => $row['app_passwd_id'],
946 ':username' => $user,
947 ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
948 ));
949 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200950 return "user";
951 }
952 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100953
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200954 if (!isset($_SESSION['ldelay'])) {
955 $_SESSION['ldelay'] = "0";
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100956 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
957 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200958 }
959 elseif (!isset($_SESSION['mailcow_cc_username'])) {
960 $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100961 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200962 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
963 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100964
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100965 $_SESSION['return'][] = array(
966 'type' => 'danger',
967 'log' => array(__FUNCTION__, $user, '*'),
968 'msg' => 'login_failed'
969 );
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100970
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200971 sleep($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100972 return false;
973}
974function formatBytes($size, $precision = 2) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200975 if(!is_numeric($size)) {
976 return "0";
977 }
978 $base = log($size, 1024);
979 $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
980 if ($size == "0") {
981 return "0";
982 }
983 return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100984}
985function update_sogo_static_view() {
986 if (getenv('SKIP_SOGO') == "y") {
987 return true;
988 }
989 global $pdo;
990 global $lang;
991 $stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
992 WHERE TABLE_NAME = 'sogo_view'");
993 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
994 if ($num_results != 0) {
995 $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`)
996 SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
997 $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
998 }
999 flush_memcached();
1000}
1001function edit_user_account($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001002 global $lang;
1003 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001004 $_data_log = $_data;
1005 !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
1006 !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
1007 !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
1008 $username = $_SESSION['mailcow_cc_username'];
1009 $role = $_SESSION['mailcow_cc_role'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001010 $password_old = $_data['user_old_pass'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001011 if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
1012 $_SESSION['return'][] = array(
1013 'type' => 'danger',
1014 'log' => array(__FUNCTION__, $_data_log),
1015 'msg' => 'access_denied'
1016 );
1017 return false;
1018 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001019 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1020 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001021 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001022 $stmt->execute(array(':user' => $username));
1023 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001024 if (!verify_hash($row['password'], $password_old)) {
1025 $_SESSION['return'][] = array(
1026 'type' => 'danger',
1027 'log' => array(__FUNCTION__, $_data_log),
1028 'msg' => 'access_denied'
1029 );
1030 return false;
1031 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001032 if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
1033 $password_new = $_data['user_new_pass'];
1034 $password_new2 = $_data['user_new_pass2'];
1035 if (password_check($password_new, $password_new2) !== true) {
1036 return false;
1037 }
1038 $password_hashed = hash_password($password_new);
1039 $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
1040 `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
1041 `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
1042 WHERE `username` = :username");
1043 $stmt->execute(array(
1044 ':password_hashed' => $password_hashed,
1045 ':username' => $username
1046 ));
1047 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001048 update_sogo_static_view();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001049 $_SESSION['return'][] = array(
1050 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001051 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001052 'msg' => array('mailbox_modified', htmlspecialchars($username))
1053 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001054}
1055function user_get_alias_details($username) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001056 global $pdo;
1057 global $lang;
1058 $data['direct_aliases'] = array();
1059 $data['shared_aliases'] = array();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001060 if ($_SESSION['mailcow_cc_role'] == "user") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001061 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001062 }
1063 if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
1064 return false;
1065 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001066 if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
1067 return false;
1068 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001069 $data['address'] = $username;
1070 $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
1071 WHERE `goto` REGEXP :username_goto
1072 AND `address` NOT LIKE '@%'
1073 AND `goto` != :username_goto2
1074 AND `address` != :username_address");
1075 $stmt->execute(array(
1076 ':username_goto' => '(^|,)'.$username.'($|,)',
1077 ':username_goto2' => $username,
1078 ':username_address' => $username
1079 ));
1080 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1081 while ($row = array_shift($run)) {
1082 $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001083 //$data['shared_aliases'][] = $row['shared_aliases'];
1084 }
1085
1086 $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
1087 WHERE `goto` = :username_goto
1088 AND `address` NOT LIKE '@%'
1089 AND `address` != :username_address");
1090 $stmt->execute(
1091 array(
1092 ':username_goto' => $username,
1093 ':username_address' => $username
1094 ));
1095 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1096 while ($row = array_shift($run)) {
1097 $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
1098 }
1099 $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
1100 LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
1101 WHERE `username` = :username ;");
1102 $stmt->execute(array(':username' => $username));
1103 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1104 while ($row = array_shift($run)) {
1105 if (empty($row['ad_alias'])) {
1106 continue;
1107 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001108 $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
1109 $data['alias_domains'][] = $row['alias_domain'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001110 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001111 $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 +01001112 $stmt->execute(array(':username' => $username));
1113 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1114 while ($row = array_shift($run)) {
1115 $data['aliases_also_send_as'] = $row['send_as'];
1116 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001117 $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 +01001118 $stmt->execute(array(':username' => $username));
1119 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1120 while ($row = array_shift($run)) {
1121 $data['aliases_send_as_all'] = $row['send_as'];
1122 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001123 $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 +01001124 $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
1125 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1126 while ($row = array_shift($run)) {
1127 $data['is_catch_all'] = $row['address'];
1128 }
1129 return $data;
1130}
1131function is_valid_domain_name($domain_name) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001132 if (empty($domain_name)) {
1133 return false;
1134 }
1135 $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
1136 return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
1137 && preg_match("/^.{1,253}$/", $domain_name)
1138 && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001139}
1140function set_tfa($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001141 global $pdo;
1142 global $yubi;
1143 global $u2f;
1144 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001145 $_data_log = $_data;
1146 !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
1147 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001148 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001149 $_SESSION['return'][] = array(
1150 'type' => 'danger',
1151 'log' => array(__FUNCTION__, $_data_log),
1152 'msg' => 'access_denied'
1153 );
1154 return false;
1155 }
1156 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001157 WHERE `username` = :username");
1158 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001159 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001160 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1161 if (!empty($num_results)) {
1162 if (!verify_hash($row['password'], $_data["confirm_password"])) {
1163 $_SESSION['return'][] = array(
1164 'type' => 'danger',
1165 'log' => array(__FUNCTION__, $_data_log),
1166 'msg' => 'access_denied'
1167 );
1168 return false;
1169 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001170 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001171 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1172 WHERE `username` = :username");
1173 $stmt->execute(array(':username' => $username));
1174 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1175 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1176 if (!empty($num_results)) {
1177 if (!verify_hash($row['password'], $_data["confirm_password"])) {
1178 $_SESSION['return'][] = array(
1179 'type' => 'danger',
1180 'log' => array(__FUNCTION__, $_data_log),
1181 'msg' => 'access_denied'
1182 );
1183 return false;
1184 }
1185 }
1186 switch ($_data["tfa_method"]) {
1187 case "yubi_otp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001188 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1189 $yubico_id = $_data['yubico_id'];
1190 $yubico_key = $_data['yubico_key'];
1191 $yubi = new Auth_Yubico($yubico_id, $yubico_key);
1192 if (!$yubi) {
1193 $_SESSION['return'][] = array(
1194 'type' => 'danger',
1195 'log' => array(__FUNCTION__, $_data_log),
1196 'msg' => 'access_denied'
1197 );
1198 return false;
1199 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001200 if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
1201 $_SESSION['return'][] = array(
1202 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001203 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001204 'msg' => 'tfa_token_invalid'
1205 );
1206 return false;
1207 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001208 $yauth = $yubi->verify($_data["otp_token"]);
1209 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001210 $_SESSION['return'][] = array(
1211 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001212 'log' => array(__FUNCTION__, $_data_log),
1213 'msg' => array('yotp_verification_failed', $yauth->getMessage())
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001214 );
1215 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001216 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001217 try {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001218 // We could also do a modhex translation here
1219 $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
1220 $stmt = $pdo->prepare("DELETE FROM `tfa`
1221 WHERE `username` = :username
1222 AND (`authmech` != 'yubi_otp')
1223 OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001224 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1225 $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
1226 (:key_id, :username, 'yubi_otp', '1', :secret)");
1227 $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
1228 }
1229 catch (PDOException $e) {
1230 $_SESSION['return'][] = array(
1231 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001232 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001233 'msg' => array('mysql_error', $e)
1234 );
1235 return false;
1236 }
1237 $_SESSION['return'][] = array(
1238 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001239 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001240 'msg' => array('object_modified', htmlspecialchars($username))
1241 );
1242 break;
1243 case "u2f":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001244 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1245 try {
1246 $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_data['token']));
1247 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001248 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001249 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
1250 $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
1251 $_SESSION['return'][] = array(
1252 'type' => 'success',
1253 'log' => array(__FUNCTION__, $_data_log),
1254 'msg' => array('object_modified', $username)
1255 );
1256 $_SESSION['regReq'] = null;
1257 }
1258 catch (Exception $e) {
1259 $_SESSION['return'][] = array(
1260 'type' => 'danger',
1261 'log' => array(__FUNCTION__, $_data_log),
1262 'msg' => array('u2f_verification_failed', $e->getMessage())
1263 );
1264 $_SESSION['regReq'] = null;
1265 return false;
1266 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001267 break;
1268 case "totp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001269 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1270 if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001271 //$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1272 //$stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001273 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
1274 $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
1275 $_SESSION['return'][] = array(
1276 'type' => 'success',
1277 'log' => array(__FUNCTION__, $_data_log),
1278 'msg' => array('object_modified', $username)
1279 );
1280 }
1281 else {
1282 $_SESSION['return'][] = array(
1283 'type' => 'danger',
1284 'log' => array(__FUNCTION__, $_data_log),
1285 'msg' => 'totp_verification_failed'
1286 );
1287 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001288 break;
1289 case "none":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001290 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1291 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001292 $_SESSION['return'][] = array(
1293 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001294 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001295 'msg' => array('object_modified', htmlspecialchars($username))
1296 );
1297 break;
1298 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001299}
1300function fido2($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001301 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001302 $_data_log = $_data;
1303 // Not logging registration data, only actions
1304 // Silent errors for "get" requests
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001305 switch ($_data["action"]) {
1306 case "register":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001307 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001308 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001309 $_SESSION['return'][] = array(
1310 'type' => 'danger',
1311 'log' => array(__FUNCTION__, $_data["action"]),
1312 'msg' => 'access_denied'
1313 );
1314 return false;
1315 }
1316 $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
1317 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1318 $stmt->execute(array(
1319 $username,
1320 $_data['registration']->rpId,
1321 $_data['registration']->credentialPublicKey,
1322 $_data['registration']->certificateChain,
1323 $_data['registration']->certificate,
1324 $_data['registration']->certificateIssuer,
1325 $_data['registration']->certificateSubject,
1326 $_data['registration']->signatureCounter,
1327 $_data['registration']->AAGUID,
1328 $_data['registration']->credentialId)
1329 );
1330 $_SESSION['return'][] = array(
1331 'type' => 'success',
1332 'log' => array(__FUNCTION__, $_data["action"]),
1333 'msg' => array('object_modified', $username)
1334 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001335 break;
1336 case "get_user_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001337 // Used to exclude existing CredentialIds while registering
1338 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001339 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1340 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001341 }
1342 $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
1343 $stmt->execute(array(':username' => $username));
1344 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1345 while($row = array_shift($rows)) {
1346 $cids[] = $row['credentialId'];
1347 }
1348 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001349 break;
1350 case "get_all_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001351 // Only needed when using fido2 with username
1352 $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
1353 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1354 while($row = array_shift($rows)) {
1355 $cids[] = $row['credentialId'];
1356 }
1357 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001358 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001359 case "get_by_b64cid":
1360 if (!isset($_data['cid']) || empty($_data['cid'])) {
1361 return false;
1362 }
1363 $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");
1364 $stmt->execute(array(':cid' => $_data['cid']));
1365 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1366 if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
1367 return false;
1368 }
1369 $data['pub_key'] = $row['credentialPublicKey'];
1370 $data['username'] = $row['username'];
1371 $data['subject'] = $row['certificateSubject'];
1372 $data['cid'] = $row['cid'];
1373 return $data;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001374 break;
1375 case "get_friendly_names":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001376 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001377 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1378 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001379 }
1380 $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
1381 $stmt->execute(array(':username' => $username));
1382 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1383 while($row = array_shift($rows)) {
1384 $fns[] = array(
1385 "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
1386 "fn" => $row['friendlyName'],
1387 "cid" => $row['cid']
1388 );
1389 }
1390 return $fns;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001391 break;
1392 case "unset_fido2_key":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001393 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001394 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1395 $_SESSION['return'][] = array(
1396 'type' => 'danger',
1397 'log' => array(__FUNCTION__, $_data["action"]),
1398 'msg' => 'access_denied'
1399 );
1400 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001401 }
1402 $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
1403 $stmt->execute(array(
1404 ':username' => $username,
1405 ':cid' => $_data['post_data']['unset_fido2_key']
1406 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001407 $_SESSION['return'][] = array(
1408 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001409 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001410 'msg' => array('object_modified', htmlspecialchars($username))
1411 );
1412 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001413 case "edit_fn":
1414 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001415 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1416 $_SESSION['return'][] = array(
1417 'type' => 'danger',
1418 'log' => array(__FUNCTION__, $_data["action"]),
1419 'msg' => 'access_denied'
1420 );
1421 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001422 }
1423 $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
1424 $stmt->execute(array(
1425 ':username' => $username,
1426 ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
1427 ':cid' => $_data['fido2_attrs']['fido2_cid']
1428 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001429 $_SESSION['return'][] = array(
1430 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001431 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001432 'msg' => array('object_modified', htmlspecialchars($username))
1433 );
1434 break;
1435 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001436}
1437function unset_tfa_key($_data) {
1438 // Can only unset own keys
1439 // Needs at least one key left
1440 global $pdo;
1441 global $lang;
1442 $_data_log = $_data;
1443 $id = intval($_data['unset_tfa_key']);
1444 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001445 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1446 $_SESSION['return'][] = array(
1447 'type' => 'danger',
1448 'log' => array(__FUNCTION__, $_data_log),
1449 'msg' => 'access_denied'
1450 );
1451 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001452 }
1453 try {
1454 if (!is_numeric($id)) {
1455 $_SESSION['return'][] = array(
1456 'type' => 'danger',
1457 'log' => array(__FUNCTION__, $_data_log),
1458 'msg' => 'access_denied'
1459 );
1460 return false;
1461 }
1462 $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
1463 WHERE `username` = :username AND `active` = '1'");
1464 $stmt->execute(array(':username' => $username));
1465 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1466 if ($row['keys'] == "1") {
1467 $_SESSION['return'][] = array(
1468 'type' => 'danger',
1469 'log' => array(__FUNCTION__, $_data_log),
1470 'msg' => 'last_key'
1471 );
1472 return false;
1473 }
1474 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
1475 $stmt->execute(array(':username' => $username, ':id' => $id));
1476 $_SESSION['return'][] = array(
1477 'type' => 'success',
1478 'log' => array(__FUNCTION__, $_data_log),
1479 'msg' => array('object_modified', $username)
1480 );
1481 }
1482 catch (PDOException $e) {
1483 $_SESSION['return'][] = array(
1484 'type' => 'danger',
1485 'log' => array(__FUNCTION__, $_data_log),
1486 'msg' => array('mysql_error', $e)
1487 );
1488 return false;
1489 }
1490}
1491function get_tfa($username = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001492 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001493 if (isset($_SESSION['mailcow_cc_username'])) {
1494 $username = $_SESSION['mailcow_cc_username'];
1495 }
1496 elseif (empty($username)) {
1497 return false;
1498 }
1499 $stmt = $pdo->prepare("SELECT * FROM `tfa`
1500 WHERE `username` = :username AND `active` = '1'");
1501 $stmt->execute(array(':username' => $username));
1502 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1503
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001504 if (isset($row["authmech"])) {
1505 switch ($row["authmech"]) {
1506 case "yubi_otp":
1507 $data['name'] = "yubi_otp";
1508 $data['pretty'] = "Yubico OTP";
1509 $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
1510 $stmt->execute(array(
1511 ':username' => $username,
1512 ));
1513 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1514 while($row = array_shift($rows)) {
1515 $data['additional'][] = $row;
1516 }
1517 return $data;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001518 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001519 case "u2f":
1520 $data['name'] = "u2f";
1521 $data['pretty'] = "Fido U2F";
1522 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1523 $stmt->execute(array(
1524 ':username' => $username,
1525 ));
1526 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1527 while($row = array_shift($rows)) {
1528 $data['additional'][] = $row;
1529 }
1530 return $data;
1531 break;
1532 case "hotp":
1533 $data['name'] = "hotp";
1534 $data['pretty'] = "HMAC-based OTP";
1535 return $data;
1536 break;
1537 case "totp":
1538 $data['name'] = "totp";
1539 $data['pretty'] = "Time-based OTP";
1540 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
1541 $stmt->execute(array(
1542 ':username' => $username,
1543 ));
1544 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1545 while($row = array_shift($rows)) {
1546 $data['additional'][] = $row;
1547 }
1548 return $data;
1549 break;
1550 default:
1551 $data['name'] = 'none';
1552 $data['pretty'] = "-";
1553 return $data;
1554 break;
1555 }
1556 }
1557 else {
1558 $data['name'] = 'none';
1559 $data['pretty'] = "-";
1560 return $data;
1561 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001562}
1563function verify_tfa_login($username, $token) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001564 global $pdo;
1565 global $yubi;
1566 global $u2f;
1567 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001568 $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
1569 WHERE `username` = :username AND `active` = '1'");
1570 $stmt->execute(array(':username' => $username));
1571 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1572
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001573 switch ($row["authmech"]) {
1574 case "yubi_otp":
1575 if (!ctype_alnum($token) || strlen($token) != 44) {
1576 $_SESSION['return'][] = array(
1577 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001578 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001579 'msg' => array('yotp_verification_failed', 'token length error')
1580 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001581 return false;
1582 }
1583 $yubico_modhex_id = substr($token, 0, 12);
1584 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1585 WHERE `username` = :username
1586 AND `authmech` = 'yubi_otp'
1587 AND `active`='1'
1588 AND `secret` LIKE :modhex");
1589 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1590 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1591 $yubico_auth = explode(':', $row['secret']);
1592 $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
1593 $yauth = $yubi->verify($token);
1594 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001595 $_SESSION['return'][] = array(
1596 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001597 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001598 'msg' => array('yotp_verification_failed', $yauth->getMessage())
1599 );
1600 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001601 }
1602 else {
1603 $_SESSION['tfa_id'] = $row['id'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001604 $_SESSION['return'][] = array(
1605 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001606 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001607 'msg' => 'verified_yotp_login'
1608 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001609 return true;
1610 }
1611 $_SESSION['return'][] = array(
1612 'type' => 'danger',
1613 'log' => array(__FUNCTION__, $username, '*'),
1614 'msg' => array('yotp_verification_failed', 'unknown')
1615 );
1616 return false;
1617 break;
1618 case "u2f":
1619 try {
1620 $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
1621 $stmt = $pdo->prepare("SELECT `id` FROM `tfa` WHERE `keyHandle` = ?");
1622 $stmt->execute(array($reg->keyHandle));
1623 $row_key_id = $stmt->fetch(PDO::FETCH_ASSOC);
1624 $_SESSION['tfa_id'] = $row_key_id['id'];
1625 $_SESSION['authReq'] = null;
1626 $_SESSION['return'][] = array(
1627 'type' => 'success',
1628 'log' => array(__FUNCTION__, $username, '*'),
1629 'msg' => 'verified_u2f_login'
1630 );
1631 return true;
1632 }
1633 catch (Exception $e) {
1634 $_SESSION['return'][] = array(
1635 'type' => 'danger',
1636 'log' => array(__FUNCTION__, $username, '*'),
1637 'msg' => array('u2f_verification_failed', $e->getMessage())
1638 );
1639 $_SESSION['regReq'] = null;
1640 return false;
1641 }
1642 $_SESSION['return'][] = array(
1643 'type' => 'danger',
1644 'log' => array(__FUNCTION__, $username, '*'),
1645 'msg' => array('u2f_verification_failed', 'unknown')
1646 );
1647 return false;
1648 break;
1649 case "hotp":
1650 return false;
1651 break;
1652 case "totp":
1653 try {
1654 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1655 WHERE `username` = :username
1656 AND `authmech` = 'totp'
1657 AND `active`='1'");
1658 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001659 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1660 foreach ($rows as $row) {
1661 if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) {
1662 $_SESSION['tfa_id'] = $row['id'];
1663 $_SESSION['return'][] = array(
1664 'type' => 'success',
1665 'log' => array(__FUNCTION__, $username, '*'),
1666 'msg' => 'verified_totp_login'
1667 );
1668 return true;
1669 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001670 }
1671 $_SESSION['return'][] = array(
1672 'type' => 'danger',
1673 'log' => array(__FUNCTION__, $username, '*'),
1674 'msg' => 'totp_verification_failed'
1675 );
1676 return false;
1677 }
1678 catch (PDOException $e) {
1679 $_SESSION['return'][] = array(
1680 'type' => 'danger',
1681 'log' => array(__FUNCTION__, $username, '*'),
1682 'msg' => array('mysql_error', $e)
1683 );
1684 return false;
1685 }
1686 break;
1687 default:
1688 $_SESSION['return'][] = array(
1689 'type' => 'danger',
1690 'log' => array(__FUNCTION__, $username, '*'),
1691 'msg' => 'unknown_tfa_method'
1692 );
1693 return false;
1694 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001695 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001696 return false;
1697}
1698function admin_api($access, $action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001699 global $pdo;
1700 if ($_SESSION['mailcow_cc_role'] != "admin") {
1701 $_SESSION['return'][] = array(
1702 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001703 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001704 'msg' => 'access_denied'
1705 );
1706 return false;
1707 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001708 if ($access !== "ro" && $access !== "rw") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001709 $_SESSION['return'][] = array(
1710 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001711 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001712 'msg' => 'invalid access type'
1713 );
1714 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001715 }
1716 if ($action == "edit") {
1717 $active = (!empty($data['active'])) ? 1 : 0;
1718 $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001719 $allow_from = array();
1720 if (isset($data['allow_from'])) {
1721 $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
1722 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001723 foreach ($allow_from as $key => $val) {
1724 if (empty($val)) {
1725 unset($allow_from[$key]);
1726 continue;
1727 }
1728 if (valid_network($val) !== true) {
1729 $_SESSION['return'][] = array(
1730 'type' => 'warning',
1731 'log' => array(__FUNCTION__, $data),
1732 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
1733 );
1734 unset($allow_from[$key]);
1735 continue;
1736 }
1737 }
1738 $allow_from = implode(',', array_unique(array_filter($allow_from)));
1739 if (empty($allow_from) && $skip_ip_check == 0) {
1740 $_SESSION['return'][] = array(
1741 'type' => 'danger',
1742 'log' => array(__FUNCTION__, $data),
1743 'msg' => 'ip_list_empty'
1744 );
1745 return false;
1746 }
1747 $api_key = implode('-', array(
1748 strtoupper(bin2hex(random_bytes(3))),
1749 strtoupper(bin2hex(random_bytes(3))),
1750 strtoupper(bin2hex(random_bytes(3))),
1751 strtoupper(bin2hex(random_bytes(3))),
1752 strtoupper(bin2hex(random_bytes(3)))
1753 ));
1754 $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
1755 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1756 if (empty($num_results)) {
1757 $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
1758 VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
1759 $stmt->execute(array(
1760 ':api_key' => $api_key,
1761 ':skip_ip_check' => $skip_ip_check,
1762 ':active' => $active,
1763 ':allow_from' => $allow_from,
1764 ':access' => $access
1765 ));
1766 }
1767 else {
1768 if ($skip_ip_check == 0) {
1769 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1770 `active` = :active,
1771 `allow_from` = :allow_from
1772 WHERE `access` = :access;");
1773 $stmt->execute(array(
1774 ':active' => $active,
1775 ':skip_ip_check' => $skip_ip_check,
1776 ':allow_from' => $allow_from,
1777 ':access' => $access
1778 ));
1779 }
1780 else {
1781 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1782 `active` = :active
1783 WHERE `access` = :access;");
1784 $stmt->execute(array(
1785 ':active' => $active,
1786 ':skip_ip_check' => $skip_ip_check,
1787 ':access' => $access
1788 ));
1789 }
1790 }
1791 }
1792 elseif ($action == "regen_key") {
1793 $api_key = implode('-', array(
1794 strtoupper(bin2hex(random_bytes(3))),
1795 strtoupper(bin2hex(random_bytes(3))),
1796 strtoupper(bin2hex(random_bytes(3))),
1797 strtoupper(bin2hex(random_bytes(3))),
1798 strtoupper(bin2hex(random_bytes(3)))
1799 ));
1800 $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
1801 $stmt->execute(array(
1802 ':api_key' => $api_key,
1803 ':access' => $access
1804 ));
1805 }
1806 elseif ($action == "get") {
1807 $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
1808 $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001809 if ($apidata !== false) {
1810 $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
1811 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001812 return $apidata;
1813 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001814 $_SESSION['return'][] = array(
1815 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001816 'log' => array(__FUNCTION__, $data),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001817 'msg' => 'admin_api_modified'
1818 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001819}
1820function license($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001821 global $pdo;
1822 global $redis;
1823 global $lang;
1824 if ($_SESSION['mailcow_cc_role'] != "admin") {
1825 $_SESSION['return'][] = array(
1826 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001827 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001828 'msg' => 'access_denied'
1829 );
1830 return false;
1831 }
1832 switch ($action) {
1833 case "verify":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001834 // Keep result until revalidate button is pressed or session expired
1835 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
1836 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
1837 $post = array('guid' => $versions['version']);
1838 $curl = curl_init('https://verify.mailcow.email');
1839 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1840 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
1841 curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
1842 $response = curl_exec($curl);
1843 curl_close($curl);
1844 $json_return = json_decode($response, true);
1845 if ($response && $json_return) {
1846 if ($json_return['response'] === "ok") {
1847 $_SESSION['gal']['valid'] = "true";
1848 $_SESSION['gal']['c'] = $json_return['c'];
1849 $_SESSION['gal']['s'] = $json_return['s'];
1850 if ($json_return['m'] == 'NoMoore') {
1851 $_SESSION['gal']['m'] = '🐄';
1852 }
1853 else {
1854 $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
1855 }
1856 }
1857 elseif ($json_return['response'] === "invalid") {
1858 $_SESSION['gal']['valid'] = "false";
1859 $_SESSION['gal']['c'] = $lang['mailbox']['no'];
1860 $_SESSION['gal']['s'] = $lang['mailbox']['no'];
1861 $_SESSION['gal']['m'] = $lang['mailbox']['no'];
1862 }
1863 }
1864 else {
1865 $_SESSION['gal']['valid'] = "false";
1866 $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
1867 $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
1868 $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
1869 }
1870 try {
1871 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
1872 $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
1873 }
1874 catch (RedisException $e) {
1875 $_SESSION['return'][] = array(
1876 'type' => 'danger',
1877 'log' => array(__FUNCTION__, $_action, $_data_log),
1878 'msg' => array('redis_error', $e)
1879 );
1880 return false;
1881 }
1882 return $_SESSION['gal']['valid'];
1883 break;
1884 case "guid":
1885 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
1886 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
1887 return $versions['version'];
1888 break;
1889 }
1890}
1891function rspamd_ui($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001892 if ($_SESSION['mailcow_cc_role'] != "admin") {
1893 $_SESSION['return'][] = array(
1894 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001895 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001896 'msg' => 'access_denied'
1897 );
1898 return false;
1899 }
1900 switch ($action) {
1901 case "edit":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001902 $rspamd_ui_pass = $data['rspamd_ui_pass'];
1903 $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
1904 if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
1905 $_SESSION['return'][] = array(
1906 'type' => 'danger',
1907 'log' => array(__FUNCTION__, '*', '*'),
1908 'msg' => 'password_empty'
1909 );
1910 return false;
1911 }
1912 if ($rspamd_ui_pass != $rspamd_ui_pass2) {
1913 $_SESSION['return'][] = array(
1914 'type' => 'danger',
1915 'log' => array(__FUNCTION__, '*', '*'),
1916 'msg' => 'password_mismatch'
1917 );
1918 return false;
1919 }
1920 if (strlen($rspamd_ui_pass) < 6) {
1921 $_SESSION['return'][] = array(
1922 'type' => 'danger',
1923 'log' => array(__FUNCTION__, '*', '*'),
1924 'msg' => 'rspamd_ui_pw_length'
1925 );
1926 return false;
1927 }
1928 $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
1929 if ($docker_return_array = json_decode($docker_return, true)) {
1930 if ($docker_return_array['type'] == 'success') {
1931 $_SESSION['return'][] = array(
1932 'type' => 'success',
1933 'log' => array(__FUNCTION__, '*', '*'),
1934 'msg' => 'rspamd_ui_pw_set'
1935 );
1936 return true;
1937 }
1938 else {
1939 $_SESSION['return'][] = array(
1940 'type' => $docker_return_array['type'],
1941 'log' => array(__FUNCTION__, '*', '*'),
1942 'msg' => $docker_return_array['msg']
1943 );
1944 return false;
1945 }
1946 }
1947 else {
1948 $_SESSION['return'][] = array(
1949 'type' => 'danger',
1950 'log' => array(__FUNCTION__, '*', '*'),
1951 'msg' => 'unknown'
1952 );
1953 return false;
1954 }
1955 break;
1956 }
1957}
1958function get_u2f_registrations($username) {
1959 global $pdo;
1960 $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
1961 $sel->execute(array($username));
1962 return $sel->fetchAll(PDO::FETCH_OBJ);
1963}
1964function get_logs($application, $lines = false) {
1965 if ($lines === false) {
1966 $lines = $GLOBALS['LOG_LINES'] - 1;
1967 }
1968 elseif(is_numeric($lines) && $lines >= 1) {
1969 $lines = abs(intval($lines) - 1);
1970 }
1971 else {
1972 list ($from, $to) = explode('-', $lines);
1973 $from = intval($from);
1974 $to = intval($to);
1975 if ($from < 1 || $to < $from) { return false; }
1976 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001977 global $redis;
1978 global $pdo;
1979 if ($_SESSION['mailcow_cc_role'] != "admin") {
1980 return false;
1981 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001982 // SQL
1983 if ($application == "mailcow-ui") {
1984 if (isset($from) && isset($to)) {
1985 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
1986 $stmt->execute(array(
1987 ':from' => $from - 1,
1988 ':to' => $to
1989 ));
1990 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1991 }
1992 else {
1993 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
1994 $stmt->execute(array(
1995 ':lines' => $lines + 1,
1996 ));
1997 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
1998 }
1999 if (is_array($data)) {
2000 return $data;
2001 }
2002 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002003 if ($application == "sasl") {
2004 if (isset($from) && isset($to)) {
2005 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
2006 $stmt->execute(array(
2007 ':from' => $from - 1,
2008 ':to' => $to
2009 ));
2010 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2011 }
2012 else {
2013 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
2014 $stmt->execute(array(
2015 ':lines' => $lines + 1,
2016 ));
2017 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2018 }
2019 if (is_array($data)) {
2020 return $data;
2021 }
2022 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002023 // Redis
2024 if ($application == "dovecot-mailcow") {
2025 if (isset($from) && isset($to)) {
2026 $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
2027 }
2028 else {
2029 $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
2030 }
2031 if ($data) {
2032 foreach ($data as $json_line) {
2033 $data_array[] = json_decode($json_line, true);
2034 }
2035 return $data_array;
2036 }
2037 }
2038 if ($application == "postfix-mailcow") {
2039 if (isset($from) && isset($to)) {
2040 $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
2041 }
2042 else {
2043 $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
2044 }
2045 if ($data) {
2046 foreach ($data as $json_line) {
2047 $data_array[] = json_decode($json_line, true);
2048 }
2049 return $data_array;
2050 }
2051 }
2052 if ($application == "sogo-mailcow") {
2053 if (isset($from) && isset($to)) {
2054 $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
2055 }
2056 else {
2057 $data = $redis->lRange('SOGO_LOG', 0, $lines);
2058 }
2059 if ($data) {
2060 foreach ($data as $json_line) {
2061 $data_array[] = json_decode($json_line, true);
2062 }
2063 return $data_array;
2064 }
2065 }
2066 if ($application == "watchdog-mailcow") {
2067 if (isset($from) && isset($to)) {
2068 $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
2069 }
2070 else {
2071 $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
2072 }
2073 if ($data) {
2074 foreach ($data as $json_line) {
2075 $data_array[] = json_decode($json_line, true);
2076 }
2077 return $data_array;
2078 }
2079 }
2080 if ($application == "acme-mailcow") {
2081 if (isset($from) && isset($to)) {
2082 $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
2083 }
2084 else {
2085 $data = $redis->lRange('ACME_LOG', 0, $lines);
2086 }
2087 if ($data) {
2088 foreach ($data as $json_line) {
2089 $data_array[] = json_decode($json_line, true);
2090 }
2091 return $data_array;
2092 }
2093 }
2094 if ($application == "ratelimited") {
2095 if (isset($from) && isset($to)) {
2096 $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
2097 }
2098 else {
2099 $data = $redis->lRange('RL_LOG', 0, $lines);
2100 }
2101 if ($data) {
2102 foreach ($data as $json_line) {
2103 $data_array[] = json_decode($json_line, true);
2104 }
2105 return $data_array;
2106 }
2107 }
2108 if ($application == "api-mailcow") {
2109 if (isset($from) && isset($to)) {
2110 $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
2111 }
2112 else {
2113 $data = $redis->lRange('API_LOG', 0, $lines);
2114 }
2115 if ($data) {
2116 foreach ($data as $json_line) {
2117 $data_array[] = json_decode($json_line, true);
2118 }
2119 return $data_array;
2120 }
2121 }
2122 if ($application == "netfilter-mailcow") {
2123 if (isset($from) && isset($to)) {
2124 $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
2125 }
2126 else {
2127 $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
2128 }
2129 if ($data) {
2130 foreach ($data as $json_line) {
2131 $data_array[] = json_decode($json_line, true);
2132 }
2133 return $data_array;
2134 }
2135 }
2136 if ($application == "autodiscover-mailcow") {
2137 if (isset($from) && isset($to)) {
2138 $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
2139 }
2140 else {
2141 $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
2142 }
2143 if ($data) {
2144 foreach ($data as $json_line) {
2145 $data_array[] = json_decode($json_line, true);
2146 }
2147 return $data_array;
2148 }
2149 }
2150 if ($application == "rspamd-history") {
2151 $curl = curl_init();
2152 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2153 if (!is_numeric($lines)) {
2154 list ($from, $to) = explode('-', $lines);
2155 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
2156 }
2157 else {
2158 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
2159 }
2160 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2161 $history = curl_exec($curl);
2162 if (!curl_errno($curl)) {
2163 $data_array = json_decode($history, true);
2164 curl_close($curl);
2165 return $data_array['rows'];
2166 }
2167 curl_close($curl);
2168 return false;
2169 }
2170 if ($application == "rspamd-stats") {
2171 $curl = curl_init();
2172 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2173 curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
2174 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2175 $stats = curl_exec($curl);
2176 if (!curl_errno($curl)) {
2177 $data_array = json_decode($stats, true);
2178 curl_close($curl);
2179 return $data_array;
2180 }
2181 curl_close($curl);
2182 return false;
2183 }
2184 return false;
2185}
2186function getGUID() {
2187 if (function_exists('com_create_guid')) {
2188 return com_create_guid();
2189 }
2190 mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
2191 $charid = strtoupper(md5(uniqid(rand(), true)));
2192 $hyphen = chr(45);// "-"
2193 return substr($charid, 0, 8).$hyphen
2194 .substr($charid, 8, 4).$hyphen
2195 .substr($charid,12, 4).$hyphen
2196 .substr($charid,16, 4).$hyphen
2197 .substr($charid,20,12);
2198}
2199function solr_status() {
2200 $curl = curl_init();
2201 $endpoint = 'http://solr:8983/solr/admin/cores';
2202 $params = array(
2203 'action' => 'STATUS',
2204 'core' => 'dovecot-fts',
2205 'indexInfo' => 'true'
2206 );
2207 $url = $endpoint . '?' . http_build_query($params);
2208 curl_setopt($curl, CURLOPT_URL, $url);
2209 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2210 curl_setopt($curl, CURLOPT_POST, 0);
2211 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2212 $response_core = curl_exec($curl);
2213 if ($response_core === false) {
2214 $err = curl_error($curl);
2215 curl_close($curl);
2216 return false;
2217 }
2218 else {
2219 curl_close($curl);
2220 $curl = curl_init();
2221 $status_core = json_decode($response_core, true);
2222 $url = 'http://solr:8983/solr/admin/info/system';
2223 curl_setopt($curl, CURLOPT_URL, $url);
2224 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2225 curl_setopt($curl, CURLOPT_POST, 0);
2226 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2227 $response_sysinfo = curl_exec($curl);
2228 if ($response_sysinfo === false) {
2229 $err = curl_error($curl);
2230 curl_close($curl);
2231 return false;
2232 }
2233 else {
2234 curl_close($curl);
2235 $status_sysinfo = json_decode($response_sysinfo, true);
2236 $status = array_merge($status_core, $status_sysinfo);
2237 return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
2238 }
2239 return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
2240 }
2241 return false;
2242}
2243
2244function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
2245 $now = time();
2246 foreach (glob($folder) as $filename) {
2247 if(strpos($filename, $ignore) !== false) {
2248 continue;
2249 }
2250 if (is_file($filename)) {
2251 if ($now - filemtime($filename) >= 60 * 60) {
2252 unlink($filename);
2253 }
2254 }
2255 }
2256}
2257
2258function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
2259 $now = time();
2260 foreach (glob($folder) as $filename) {
2261 if(strpos($filename, $ignore) !== false) {
2262 continue;
2263 }
2264 if (is_file($filename)) {
2265 if ($now - filemtime($filename) >= 60 * 60) {
2266 unlink($filename);
2267 }
2268 }
2269 }
2270}
2271
2272?>