This commit is contained in:
andryyy
2017-03-02 11:23:23 +01:00
parent 5f7fb2e7c2
commit d891bc8894
155 changed files with 26539 additions and 2916 deletions

225
data/web/inc/footer.inc.php Normal file
View File

@@ -0,0 +1,225 @@
<?php
include("inc/tfa_modals.php");
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
?>
<div id="RestartSOGo" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"><?=$lang['footer']['restart_sogo'];?></h4>
</div>
<div class="modal-body">
<p><?=$lang['footer']['restart_sogo_info'];?></p>
<hr />
<button class="btn btn-md btn-primary" id="triggerRestartSogo"><?=$lang['footer']['restart_now'];?></button>
<br /><br />
<div id="statusTriggerRestartSogo"></div>
</div>
</div>
</div>
</div>
<?php
endif;
?>
<div style="margin-bottom:100px"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="/js/bootstrap-switch.min.js"></script>
<script src="/js/bootstrap-slider.min.js"></script>
<script src="/js/bootstrap-select.min.js"></script>
<script src="/js/u2f-api.js"></script>
<script>
// Select language and reopen active URL without POST
function setLang(sel) {
$.post( "<?=$_SERVER['REQUEST_URI'];?>", {lang: sel} );
window.location.href = window.location.pathname + window.location.search;
}
$(document).ready(function() {
// Confirm TFA modal
<?php if (isset($_SESSION['pending_tfa_method'])):?>
$('#ConfirmTFAModal').modal({
backdrop: 'static',
keyboard: false
});
$('#ConfirmTFAModal').on('shown.bs.modal', function(){
$(this).find('#token').focus();
// If U2F
if(document.getElementById("u2f_auth_data") !== null) {
$.ajax({
type: "GET",
cache: false,
dataType: 'script',
url: "json_api.php",
data: {
'action':'get_u2f_auth_challenge',
'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>',
},
success: function(data){
data;
}
});
setTimeout(function() {
console.log("sign: ", req);
u2f.sign(req, function(data) {
var form = document.getElementById('u2f_auth_form');
var auth = document.getElementById('u2f_auth_data');
console.log("Authenticate callback", data);
auth.value = JSON.stringify(data);
form.submit();
});
}, 1000);
}
});
<?php endif; ?>
// Set TFA modals
$('#selectTFA').change(function () {
if ($(this).val() == "yubi_otp") {
$('#YubiOTPModal').modal('show');
$("option:selected").prop("selected", false);
}
if ($(this).val() == "u2f") {
$('#U2FModal').modal('show');
$("option:selected").prop("selected", false);
$.ajax({
type: "GET",
cache: false,
dataType: 'script',
url: "json_api.php",
data: {
'action':'get_u2f_reg_challenge',
'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>',
},
success: function(data){
data;
}
});
setTimeout(function() {
console.log("Register: ", req);
u2f.register([req], sigs, function(data) {
var form = document.getElementById('u2f_reg_form');
var reg = document.getElementById('u2f_register_data');
console.log("Register callback", data);
if (data.errorCode && data.errorCode != 0) {
var u2f_return_code = document.getElementById('u2f_return_code');
u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
u2f_return_code.innerHTML = 'Error code: ' + data.errorCode;
return;
}
reg.value = JSON.stringify(data);
form.submit();
});
}, 1000);
}
if ($(this).val() == "none") {
$('#DisableTFAModal').modal('show');
$("option:selected").prop("selected", false);
}
});
// Activate tooltips
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
// Hide alerts after n seconds
$("#alert-fade").fadeTo(7000, 500).slideUp(500, function(){
$("#alert-fade").alert('close');
});
// Remember last navigation pill
(function () {
'use strict';
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var id = $(this).parents('[role="tablist"]').attr('id');
var key = 'lastTag';
if (id) {
key += ':' + id;
}
localStorage.setItem(key, $(e.target).attr('href'));
});
$('[role="tablist"]').each(function (idx, elem) {
var id = $(elem).attr('id');
var key = 'lastTag';
if (id) {
key += ':' + id;
}
var lastTab = localStorage.getItem(key);
if (lastTab) {
$('[href="' + lastTab + '"]').tab('show');
}
});
})();
// Disable submit after submitting form
$('form').submit(function() {
if ($('form button[type="submit"]').data('submitted') == '1') {
return false;
} else {
$(this).find('button[type="submit"]').first().text('<?=$lang['footer']['loading'];?>');
$('form button[type="submit"]').attr('data-submitted', '1');
function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
$(document).on("keydown", disableF5);
}
});
// IE fix to hide scrollbars when table body is empty
$('tbody').filter(function (index) {
return $(this).children().length < 1;
}).remove();
// Init Bootstrap Selectpicker
$('select').selectpicker();
// Trigger SOGo restart
$('#triggerRestartSogo').click(function(){
$(this).prop("disabled",true);
$(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
$('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... ');
$.ajax({
method: 'get',
url: 'call_sogo_ctrl.php',
data: {
'ajax': true,
'ACTION': 'stop'
},
success: function(data) {
$('#statusTriggerRestartSogo').append(data);
$('#statusTriggerRestartSogo').append('<br />Starting SOGo... ');
$.ajax({
method: 'get',
url: 'call_sogo_ctrl.php',
data: {
'ajax': true,
'ACTION': 'start'
},
success: function(data) {
$('#statusTriggerRestartSogo').append(data);
$('#triggerRestartSogo').html('<span class="glyphicon glyphicon-ok"></span> ');
}
});
}
});
});
});
</script>
<?php
if (isset($_SESSION['return'])):
?>
<div class="container">
<div style="position:fixed;bottom:8px;right:25px;min-width:300px;max-width:350px;z-index:2000">
<div <?=($_SESSION['return']['type'] == 'danger') ? null : 'id="alert-fade"'?> class="alert alert-<?=$_SESSION['return']['type'];?>" role="alert">
<a href="#" class="close" data-dismiss="alert"> &times;</a>
<?=htmlspecialchars($_SESSION['return']['msg']);?>
</div>
</div>
</div>
<?php
unset($_SESSION['return']);
endif;
?>
</body>
</html>
<?php $stmt = null; $pdo = null; ?>

File diff suppressed because it is too large Load Diff

104
data/web/inc/header.inc.php Normal file
View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="<?= $_SESSION['mailcow_locale'] ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>mailcow UI</title>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min.js" integrity="sha384-XxcvoeNF5V0ZfksTnV+bejnCsJjOOIzN6UVwF85WBsAnU3zeYh5bloN+L4WLgeNE" crossorigin="anonymous"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/<?=strtolower(trim($DEFAULT_THEME));?>/bootstrap.min.css">
<link rel="stylesheet" href="/css/bootstrap-select.min.css">
<link rel="stylesheet" href="/css/bootstrap-slider.min.css">
<link rel="stylesheet" href="/css/bootstrap-switch.min.css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700&subset=latin,latin-ext">
<link rel="stylesheet" href="/inc/languages.min.css">
<link rel="stylesheet" href="/css/mailcow.css">
<link rel="stylesheet" href="/css/tables.css">
<?=(preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null;?>
<link rel="shortcut icon" href="/favicon.png" type="image/png">
<link rel="icon" href="/favicon.png" type="image/png">
</head>
<body style="padding-top:70px">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><img height="32" alt="mailcow-logo" style="margin-top:-5px;" src="/img/cow_mailcow.svg" /></a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<?php
if (isset($_SESSION['mailcow_locale'])) {
?>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?=$_SESSION['mailcow_locale'];?>"></span><span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li <?=($_SESSION['mailcow_locale'] == 'de') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "de"))) ?>"><span class="lang-xs lang-lbl-full" lang="de"></span></a></li>
<li <?=($_SESSION['mailcow_locale'] == 'en') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "en"))) ?>"><span class="lang-xs lang-lbl-full" lang="en"></span></a></li>
<li <?=($_SESSION['mailcow_locale'] == 'es') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "es"))) ?>"><span class="lang-xs lang-lbl-full" lang="es"></span></a></li>
<li <?=($_SESSION['mailcow_locale'] == 'nl') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "nl"))) ?>"><span class="lang-xs lang-lbl-full" lang="nl"></span></a></li>
<li <?=($_SESSION['mailcow_locale'] == 'pt') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "pt"))) ?>"><span class="lang-xs lang-lbl-full" lang="pt"></span></a></li>
</ul>
</li>
<?php
}
if (isset($_SESSION['mailcow_cc_role'])) {
?>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?=$lang['header']['mailcow_settings'];?><span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<?php
if (isset($_SESSION['mailcow_cc_role'])) {
if ($_SESSION['mailcow_cc_role'] == "admin") {
?>
<li <?=(preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/admin.php"><?=$lang['header']['administration'];?></a></li>
<?php
}
if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") {
?>
<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
<?php
}
if ($_SESSION['mailcow_cc_role'] != "admin") {
?>
<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
<?php
}
}
?>
</ul>
</li>
<?php
if ($_SESSION['mailcow_cc_role'] == "admin"):
?>
<li><a href data-toggle="modal" data-target="#RestartSOGo"><span style="font-size:12px" class="glyphicon glyphicon-refresh" aria-hidden="true"></span> <?=$lang['header']['restart_sogo'];?></a></li>
<?php
endif;
?>
<?php
}
if (!isset($_SESSION["dual-login"]) && isset($_SESSION['mailcow_cc_username'])):
?>
<li><a style="border-left:1px solid #E7E7E7" href="#" onclick="logout.submit()"><?=sprintf($lang['header']['logged_in_as_logout'], $_SESSION['mailcow_cc_username']);?></a></li>
<?php
elseif (isset($_SESSION["dual-login"])):
?>
<li><a style="border-left:1px solid #E7E7E7" href="#" onclick="logout.submit()"><?=sprintf($lang['header']['logged_in_as_logout_dual'], $_SESSION['mailcow_cc_username'], $_SESSION["dual-login"]["username"]);?></a></li>
<?php
endif;
?>
</ul>
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</nav>
<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>

281
data/web/inc/init.sql Normal file
View File

@@ -0,0 +1,281 @@
CREATE TABLE IF NOT EXISTS `admin` (
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`superadmin` TINYINT(1) NOT NULL DEFAULT '0',
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`active` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `alias` (
`address` VARCHAR(255) NOT NULL,
`goto` TEXT NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`active` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`address`),
KEY `domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `sender_acl` (
`logged_in_as` VARCHAR(255) NOT NULL,
`send_as` VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `spamalias` (
`address` VARCHAR(255) NOT NULL,
`goto` TEXT NOT NULL,
`validity` INT(11) NOT NULL,
PRIMARY KEY (`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `alias_domain` (
`alias_domain` VARCHAR(255) NOT NULL,
`target_domain` VARCHAR(255) NOT NULL,
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`active` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`alias_domain`),
KEY `active` (`active`),
KEY `target_domain` (`target_domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `domain` (
`domain` VARCHAR(255) NOT NULL,
`description` VARCHAR(255),
`aliases` INT(10) NOT NULL DEFAULT '0',
`mailboxes` INT(10) NOT NULL DEFAULT '0',
`maxquota` BIGINT(20) NOT NULL DEFAULT '0',
`quota` BIGINT(20) NOT NULL DEFAULT '0',
`transport` VARCHAR(255) NOT NULL,
`backupmx` TINYINT(1) NOT NULL DEFAULT '0',
`relay_all_recipients` TINYINT(1) NOT NULL DEFAULT '0',
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`active` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `domain_admins` (
`username` VARCHAR(255) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`active` TINYINT(1) NOT NULL DEFAULT '1',
KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `mailbox` (
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`name` VARCHAR(255),
`maildir` VARCHAR(255) NOT NULL,
`quota` BIGINT(20) NOT NULL DEFAULT '0',
`local_part` VARCHAR(255) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
`tls_enforce_in` TINYINT(1) NOT NULL DEFAULT '0',
`tls_enforce_out` TINYINT(1) NOT NULL DEFAULT '0',
`kind` VARCHAR(100) NOT NULL DEFAULT '',
`multiple_bookings` TINYINT(1) NOT NULL DEFAULT '0',
`wants_tagged_subject` TINYINT(1) NOT NULL DEFAULT '0',
`active` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`username`),
KEY `domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `quota2` (
`username` VARCHAR(100) NOT NULL,
`bytes` BIGINT(20) NOT NULL DEFAULT '0',
`messages` INT(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `filterconf` (
`object` VARCHAR(100) NOT NULL DEFAULT '',
`option` VARCHAR(50) NOT NULL DEFAULT '',
`value` VARCHAR(100) NOT NULL DEFAULT '',
`prefid` INT(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`prefid`),
KEY `object` (`object`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `imapsync` (
`id` INT NOT NULL AUTO_INCREMENT,
`user2` VARCHAR(255) NOT NULL,
`host1` VARCHAR(255) NOT NULL,
`authmech1` ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN',
`regextrans2` VARCHAR(255) DEFAULT '',
`authmd51` TINYINT(1) NOT NULL DEFAULT 0,
`domain2` VARCHAR(255) NOT NULL DEFAULT '',
`subfolder2` VARCHAR(255) NOT NULL DEFAULT '',
`user1` VARCHAR(255) NOT NULL,
`password1` VARCHAR(255) NOT NULL,
`exclude` VARCHAR(500) NOT NULL DEFAULT '',
`maxage` SMALLINT NOT NULL DEFAULT '0',
`mins_interval` VARCHAR(50) NOT NULL,
`port1` SMALLINT NOT NULL,
`enc1` ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS',
`delete2duplicates` TINYINT(1) NOT NULL DEFAULT '1',
`returned_text` TEXT,
`last_run` TIMESTAMP NULL DEFAULT NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`active` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `tfa` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp'),
`secret` VARCHAR(255) DEFAULT NULL,
`keyHandle` VARCHAR(255) DEFAULT NULL,
`publicKey` VARCHAR(255) DEFAULT NULL,
`counter` INT NOT NULL DEFAULT '0',
`certificate` TEXT,
`active` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
DROP VIEW IF EXISTS grouped_mail_aliases;
DROP VIEW IF EXISTS grouped_sender_acl;
DROP VIEW IF EXISTS grouped_domain_alias_address;
CREATE VIEW grouped_mail_aliases (username, aliases) AS
SELECT goto, IFNULL(GROUP_CONCAT(address SEPARATOR ' '), '') AS address FROM alias
WHERE address!=goto
AND active = '1'
AND address NOT LIKE '@%'
GROUP BY goto;
CREATE VIEW grouped_sender_acl (username, send_as) AS
SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as FROM sender_acl
WHERE send_as NOT LIKE '@%'
GROUP BY logged_in_as;
CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
CREATE TABLE IF NOT EXISTS sogo_acl (
c_folder_id INTEGER NOT NULL,
c_object character varying(255) NOT NULL,
c_uid character varying(255) NOT NULL,
c_role character varying(80) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_alarms_folder (
c_path VARCHAR(255) NOT NULL,
c_name VARCHAR(255) NOT NULL,
c_uid VARCHAR(255) NOT NULL,
c_recurrence_id INT(11) DEFAULT NULL,
c_alarm_number INT(11) NOT NULL,
c_alarm_date INT(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_cache_folder (
c_uid VARCHAR(255) NOT NULL,
c_path VARCHAR(255) NOT NULL,
c_parent_path VARCHAR(255) DEFAULT NULL,
c_type TINYINT(3) unsigned NOT NULL,
c_creationdate INT(11) NOT NULL,
c_lastmodified INT(11) NOT NULL,
c_version INT(11) NOT NULL DEFAULT '0',
c_deleted TINYINT(4) NOT NULL DEFAULT '0',
c_content longTEXT,
PRIMARY KEY (c_uid,c_path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_folder_info (
c_folder_id BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
c_path VARCHAR(255) NOT NULL,
c_path1 VARCHAR(255) NOT NULL,
c_path2 VARCHAR(255) DEFAULT NULL,
c_path3 VARCHAR(255) DEFAULT NULL,
c_path4 VARCHAR(255) DEFAULT NULL,
c_foldername VARCHAR(255) NOT NULL,
c_location INTeger NULL,
c_quick_location VARCHAR(2048) DEFAULT NULL,
c_acl_location VARCHAR(2048) DEFAULT NULL,
c_folder_type VARCHAR(255) NOT NULL,
PRIMARY KEY (c_path),
UNIQUE KEY c_folder_id (c_folder_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_quick_appointment (
c_folder_id INTeger NOT NULL,
c_name character varying(255) NOT NULL,
c_uid character varying(255) NOT NULL,
c_startdate INTeger,
c_enddate INTeger,
c_cycleenddate INTeger,
c_title character varying(1000) NOT NULL,
c_participants TEXT,
c_isallday INTeger,
c_iscycle INTeger,
c_cycleinfo TEXT,
c_classification INTeger NOT NULL,
c_isopaque INTeger NOT NULL,
c_status INTeger NOT NULL,
c_priority INTeger,
c_location character varying(255),
c_orgmail character varying(255),
c_partmails TEXT,
c_partstates TEXT,
c_category character varying(255),
c_sequence INTeger,
c_component character varying(10) NOT NULL,
c_nextalarm INTeger,
c_description TEXT,
CONSTRAINT sogo_quick_appointment_pkey PRIMARY KEY (c_folder_id, c_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_quick_contact (
c_folder_id INTeger NOT NULL,
c_name character varying(255) NOT NULL,
c_givenname character varying(255),
c_cn character varying(255),
c_sn character varying(255),
c_screenname character varying(255),
c_l character varying(255),
c_mail character varying(255),
c_o character varying(255),
c_ou character varying(255),
c_telephonenumber character varying(255),
c_categories character varying(255),
c_component character varying(10) NOT NULL,
CONSTRAINT sogo_quick_contact_pkey PRIMARY KEY (c_folder_id, c_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_sessions_folder (
c_id VARCHAR(255) NOT NULL,
c_value VARCHAR(255) NOT NULL,
c_creationdate INT(11) NOT NULL,
c_lastseen INT(11) NOT NULL,
PRIMARY KEY (c_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_store (
c_folder_id INTeger NOT NULL,
c_name character varying(255) NOT NULL,
c_content mediumTEXT NOT NULL,
c_creationdate INTeger NOT NULL,
c_lastmodified INTeger NOT NULL,
c_version INTeger NOT NULL,
c_deleted INTeger,
CONSTRAINT sogo_store_pkey PRIMARY KEY (c_folder_id, c_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS sogo_user_profile (
c_uid VARCHAR(255) NOT NULL,
c_defaults TEXT,
c_settings TEXT,
PRIMARY KEY (c_uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
INSERT INTO `admin` (username, password, superadmin, created, modified, active) SELECT 'admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1 WHERE NOT EXISTS (SELECT * FROM `admin`);
DELETE FROM `domain_admins`;
INSERT INTO `domain_admins` (username, domain, created, active) SELECT `username`, 'ALL', NOW(), 1 FROM `admin` WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);

1
data/web/inc/languages.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
data/web/inc/languages.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

506
data/web/inc/lib/U2F.php Normal file
View File

@@ -0,0 +1,506 @@
<?php
/* Copyright (c) 2014 Yubico AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace u2flib_server;
/** Constant for the version of the u2f protocol */
const U2F_VERSION = "U2F_V2";
/** Error for the authentication message not matching any outstanding
* authentication request */
const ERR_NO_MATCHING_REQUEST = 1;
/** Error for the authentication message not matching any registration */
const ERR_NO_MATCHING_REGISTRATION = 2;
/** Error for the signature on the authentication message not verifying with
* the correct key */
const ERR_AUTHENTICATION_FAILURE = 3;
/** Error for the challenge in the registration message not matching the
* registration challenge */
const ERR_UNMATCHED_CHALLENGE = 4;
/** Error for the attestation signature on the registration message not
* verifying */
const ERR_ATTESTATION_SIGNATURE = 5;
/** Error for the attestation verification not verifying */
const ERR_ATTESTATION_VERIFICATION = 6;
/** Error for not getting good random from the system */
const ERR_BAD_RANDOM = 7;
/** Error when the counter is lower than expected */
const ERR_COUNTER_TOO_LOW = 8;
/** Error decoding public key */
const ERR_PUBKEY_DECODE = 9;
/** Error user-agent returned error */
const ERR_BAD_UA_RETURNING = 10;
/** Error old OpenSSL version */
const ERR_OLD_OPENSSL = 11;
/** @internal */
const PUBKEY_LEN = 65;
class U2F
{
/** @var string */
private $appId;
/** @var null|string */
private $attestDir;
/** @internal */
private $FIXCERTS = array(
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
);
/**
* @param string $appId Application id for the running application
* @param string|null $attestDir Directory where trusted attestation roots may be found
* @throws Error If OpenSSL older than 1.0.0 is used
*/
public function __construct($appId, $attestDir = null)
{
if(OPENSSL_VERSION_NUMBER < 0x10000000) {
throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
}
$this->appId = $appId;
$this->attestDir = $attestDir;
}
/**
* Called to get a registration request to send to a user.
* Returns an array of one registration request and a array of sign requests.
*
* @param array $registrations List of current registrations for this
* user, to prevent the user from registering the same authenticator several
* times.
* @return array An array of two elements, the first containing a
* RegisterRequest the second being an array of SignRequest
* @throws Error
*/
public function getRegisterData(array $registrations = array())
{
$challenge = $this->createChallenge();
$request = new RegisterRequest($challenge, $this->appId);
$signs = $this->getAuthenticateData($registrations);
return array($request, $signs);
}
/**
* Called to verify and unpack a registration message.
*
* @param RegisterRequest $request this is a reply to
* @param object $response response from a user
* @param bool $includeCert set to true if the attestation certificate should be
* included in the returned Registration object
* @return Registration
* @throws Error
*/
public function doRegister($request, $response, $includeCert = true)
{
if( !is_object( $request ) ) {
throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
}
if( !is_object( $response ) ) {
throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
}
if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
}
if( !is_bool( $includeCert ) ) {
throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
}
$rawReg = $this->base64u_decode($response->registrationData);
$regData = array_values(unpack('C*', $rawReg));
$clientData = $this->base64u_decode($response->clientData);
$cli = json_decode($clientData);
if($cli->challenge !== $request->challenge) {
throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
}
$registration = new Registration();
$offs = 1;
$pubKey = substr($rawReg, $offs, PUBKEY_LEN);
$offs += PUBKEY_LEN;
// decode the pubKey to make sure it's good
$tmpKey = $this->pubkey_to_pem($pubKey);
if($tmpKey === null) {
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
}
$registration->publicKey = base64_encode($pubKey);
$khLen = $regData[$offs++];
$kh = substr($rawReg, $offs, $khLen);
$offs += $khLen;
$registration->keyHandle = $this->base64u_encode($kh);
// length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
$certLen = 4;
$certLen += ($regData[$offs + 2] << 8);
$certLen += $regData[$offs + 3];
$rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
$offs += $certLen;
$pemCert = "-----BEGIN CERTIFICATE-----\r\n";
$pemCert .= chunk_split(base64_encode($rawCert), 64);
$pemCert .= "-----END CERTIFICATE-----";
if($includeCert) {
$registration->certificate = base64_encode($rawCert);
}
if($this->attestDir) {
if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
}
}
if(!openssl_pkey_get_public($pemCert)) {
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
}
$signature = substr($rawReg, $offs);
$dataToVerify = chr(0);
$dataToVerify .= hash('sha256', $request->appId, true);
$dataToVerify .= hash('sha256', $clientData, true);
$dataToVerify .= $kh;
$dataToVerify .= $pubKey;
if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
return $registration;
} else {
throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
}
}
/**
* Called to get an authentication request.
*
* @param array $registrations An array of the registrations to create authentication requests for.
* @return array An array of SignRequest
* @throws Error
*/
public function getAuthenticateData(array $registrations)
{
$sigs = array();
foreach ($registrations as $reg) {
if( !is_object( $reg ) ) {
throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
}
$sig = new SignRequest();
$sig->appId = $this->appId;
$sig->keyHandle = $reg->keyHandle;
$sig->challenge = $this->createChallenge();
$sigs[] = $sig;
}
return $sigs;
}
/**
* Called to verify an authentication response
*
* @param array $requests An array of outstanding authentication requests
* @param array $registrations An array of current registrations
* @param object $response A response from the authenticator
* @return Registration
* @throws Error
*
* The Registration object returned on success contains an updated counter
* that should be saved for future authentications.
* If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
* token cloning or similar and appropriate action should be taken.
*/
public function doAuthenticate(array $requests, array $registrations, $response)
{
if( !is_object( $response ) ) {
throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
}
if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
}
/** @var object|null $req */
$req = null;
/** @var object|null $reg */
$reg = null;
$clientData = $this->base64u_decode($response->clientData);
$decodedClient = json_decode($clientData);
foreach ($requests as $req) {
if( !is_object( $req ) ) {
throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
}
if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
break;
}
$req = null;
}
if($req === null) {
throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
}
foreach ($registrations as $reg) {
if( !is_object( $reg ) ) {
throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
}
if($reg->keyHandle === $response->keyHandle) {
break;
}
$reg = null;
}
if($reg === null) {
throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
}
$pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
if($pemKey === null) {
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
}
$signData = $this->base64u_decode($response->signatureData);
$dataToVerify = hash('sha256', $req->appId, true);
$dataToVerify .= substr($signData, 0, 5);
$dataToVerify .= hash('sha256', $clientData, true);
$signature = substr($signData, 5);
if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
$ctr = unpack("Nctr", substr($signData, 1, 4));
$counter = $ctr['ctr'];
/* TODO: wrap-around should be handled somehow.. */
if($counter > $reg->counter) {
$reg->counter = $counter;
return $reg;
} else {
throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
}
} else {
throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
}
}
/**
* @return array
*/
private function get_certs()
{
$files = array();
$dir = $this->attestDir;
if($dir && $handle = opendir($dir)) {
while(false !== ($entry = readdir($handle))) {
if(is_file("$dir/$entry")) {
$files[] = "$dir/$entry";
}
}
closedir($handle);
}
return $files;
}
/**
* @param string $data
* @return string
*/
private function base64u_encode($data)
{
return trim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* @param string $data
* @return string
*/
private function base64u_decode($data)
{
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* @param string $key
* @return null|string
*/
private function pubkey_to_pem($key)
{
if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
return null;
}
/*
* Convert the public key to binary DER format first
* Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
*
* SEQUENCE(2 elem) 30 59
* SEQUENCE(2 elem) 30 13
* OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
* OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07
* BIT STRING(520 bit) 03 42 ..key..
*/
$der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
$der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
$der .= "\0".$key;
$pem = "-----BEGIN PUBLIC KEY-----\r\n";
$pem .= chunk_split(base64_encode($der), 64);
$pem .= "-----END PUBLIC KEY-----";
return $pem;
}
/**
* @return string
* @throws Error
*/
private function createChallenge()
{
$challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
if( $crypto_strong !== true ) {
throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
}
$challenge = $this->base64u_encode( $challenge );
return $challenge;
}
/**
* Fixes a certificate where the signature contains unused bits.
*
* @param string $cert
* @return mixed
*/
private function fixSignatureUnusedBits($cert)
{
if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
$cert[strlen($cert) - 257] = "\0";
}
return $cert;
}
}
/**
* Class for building a registration request
*
* @package u2flib_server
*/
class RegisterRequest
{
/** Protocol version */
public $version = U2F_VERSION;
/** Registration challenge */
public $challenge;
/** Application id */
public $appId;
/**
* @param string $challenge
* @param string $appId
* @internal
*/
public function __construct($challenge, $appId)
{
$this->challenge = $challenge;
$this->appId = $appId;
}
}
/**
* Class for building up an authentication request
*
* @package u2flib_server
*/
class SignRequest
{
/** Protocol version */
public $version = U2F_VERSION;
/** Authentication challenge */
public $challenge;
/** Key handle of a registered authenticator */
public $keyHandle;
/** Application id */
public $appId;
}
/**
* Class returned for successful registrations
*
* @package u2flib_server
*/
class Registration
{
/** The key handle of the registered authenticator */
public $keyHandle;
/** The public key of the registered authenticator */
public $publicKey;
/** The attestation certificate of the registered authenticator */
public $certificate;
/** The counter associated with this registration */
public $counter = -1;
}
/**
* Error class, returned on errors
*
* @package u2flib_server
*/
class Error extends \Exception
{
/**
* Override constructor and make message and code mandatory
* @param string $message
* @param int $code
* @param \Exception|null $previous
*/
public function __construct($message, $code, \Exception $previous = null) {
parent::__construct($message, $code, $previous);
}
}

475
data/web/inc/lib/Yubico.php Normal file
View File

@@ -0,0 +1,475 @@
<?php
/**
* Class for verifying Yubico One-Time-Passcodes
*
* @category Auth
* @package Auth_Yubico
* @author Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>
* @copyright 2007-2015 Yubico AB
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version 2.0
* @link http://www.yubico.com/
*/
require_once 'PEAR.php';
/**
* Class for verifying Yubico One-Time-Passcodes
*
* Simple example:
* <code>
* require_once 'Auth/Yubico.php';
* $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
*
* # Generate a new id+key from https://api.yubico.com/get-api-key/
* $yubi = new Auth_Yubico('42', 'FOOBAR=');
* $auth = $yubi->verify($otp);
* if (PEAR::isError($auth)) {
* print "<p>Authentication failed: " . $auth->getMessage();
* print "<p>Debug output from server: " . $yubi->getLastResponse();
* } else {
* print "<p>You are authenticated!";
* }
* </code>
*/
class Auth_Yubico
{
/**#@+
* @access private
*/
/**
* Yubico client ID
* @var string
*/
var $_id;
/**
* Yubico client key
* @var string
*/
var $_key;
/**
* URL part of validation server
* @var string
*/
var $_url;
/**
* List with URL part of validation servers
* @var array
*/
var $_url_list;
/**
* index to _url_list
* @var int
*/
var $_url_index;
/**
* Last query to server
* @var string
*/
var $_lastquery;
/**
* Response from server
* @var string
*/
var $_response;
/**
* Flag whether to use https or not.
* @var boolean
*/
var $_https;
/**
* Flag whether to verify HTTPS server certificates or not.
* @var boolean
*/
var $_httpsverify;
/**
* Constructor
*
* Sets up the object
* @param string $id The client identity
* @param string $key The client MAC key (optional)
* @param boolean $https Flag whether to use https (optional)
* @param boolean $httpsverify Flag whether to use verify HTTPS
* server certificates (optional,
* default true)
* @access public
*/
function __construct($id, $key = '', $https = 0, $httpsverify = 1)
{
$this->_id = $id;
$this->_key = base64_decode($key);
$this->_https = $https;
$this->_httpsverify = $httpsverify;
}
function Auth_Yubico($id, $key = '', $https = 0, $httpsverify = 1)
{
self::__construct();
}
/**
* Specify to use a different URL part for verification.
* The default is "api.yubico.com/wsapi/verify".
*
* @param string $url New server URL part to use
* @access public
*/
function setURLpart($url)
{
$this->_url = $url;
}
/**
* Get URL part to use for validation.
*
* @return string Server URL part
* @access public
*/
function getURLpart()
{
if ($this->_url) {
return $this->_url;
} else {
return "api.yubico.com/wsapi/verify";
}
}
/**
* Get next URL part from list to use for validation.
*
* @return mixed string with URL part of false if no more URLs in list
* @access public
*/
function getNextURLpart()
{
if ($this->_url_list) $url_list=$this->_url_list;
else $url_list=array('api.yubico.com/wsapi/2.0/verify',
'api2.yubico.com/wsapi/2.0/verify',
'api3.yubico.com/wsapi/2.0/verify',
'api4.yubico.com/wsapi/2.0/verify',
'api5.yubico.com/wsapi/2.0/verify');
if ($this->_url_index>=count($url_list)) return false;
else return $url_list[$this->_url_index++];
}
/**
* Resets index to URL list
*
* @access public
*/
function URLreset()
{
$this->_url_index=0;
}
/**
* Add another URLpart.
*
* @access public
*/
function addURLpart($URLpart)
{
$this->_url_list[]=$URLpart;
}
/**
* Return the last query sent to the server, if any.
*
* @return string Request to server
* @access public
*/
function getLastQuery()
{
return $this->_lastquery;
}
/**
* Return the last data received from the server, if any.
*
* @return string Output from server
* @access public
*/
function getLastResponse()
{
return $this->_response;
}
/**
* Parse input string into password, yubikey prefix,
* ciphertext, and OTP.
*
* @param string Input string to parse
* @param string Optional delimiter re-class, default is '[:]'
* @return array Keyed array with fields
* @access public
*/
function parsePasswordOTP($str, $delim = '[:]')
{
if (!preg_match("/^((.*)" . $delim . ")?" .
"(([cbdefghijklnrtuv]{0,16})" .
"([cbdefghijklnrtuv]{32}))$/i",
$str, $matches)) {
/* Dvorak? */
if (!preg_match("/^((.*)" . $delim . ")?" .
"(([jxe\.uidchtnbpygk]{0,16})" .
"([jxe\.uidchtnbpygk]{32}))$/i",
$str, $matches)) {
return false;
} else {
$ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
}
} else {
$ret['otp'] = $matches[3];
}
$ret['password'] = $matches[2];
$ret['prefix'] = $matches[4];
$ret['ciphertext'] = $matches[5];
return $ret;
}
/* TODO? Add functions to get parsed parts of server response? */
/**
* Parse parameters from last response
*
* example: getParameters("timestamp", "sessioncounter", "sessionuse");
*
* @param array @parameters Array with strings representing
* parameters to parse
* @return array parameter array from last response
* @access public
*/
function getParameters($parameters)
{
if ($parameters == null) {
$parameters = array('timestamp', 'sessioncounter', 'sessionuse');
}
$param_array = array();
foreach ($parameters as $param) {
if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
}
$param_array[$param]=$out[1];
}
return $param_array;
}
/**
* Verify Yubico OTP against multiple URLs
* Protocol specification 2.0 is used to construct validation requests
*
* @param string $token Yubico OTP
* @param int $use_timestamp 1=>send request with &timestamp=1 to
* get timestamp and session information
* in the response
* @param boolean $wait_for_all If true, wait until all
* servers responds (for debugging)
* @param string $sl Sync level in percentage between 0
* and 100 or "fast" or "secure".
* @param int $timeout Max number of seconds to wait
* for responses
* @return mixed PEAR error on error, true otherwise
* @access public
*/
function verify($token, $use_timestamp=null, $wait_for_all=False,
$sl=null, $timeout=null)
{
/* Construct parameters string */
$ret = $this->parsePasswordOTP($token);
if (!$ret) {
return PEAR::raiseError('Could not parse Yubikey OTP');
}
$params = array('id'=>$this->_id,
'otp'=>$ret['otp'],
'nonce'=>md5(uniqid(rand())));
/* Take care of protocol version 2 parameters */
if ($use_timestamp) $params['timestamp'] = 1;
if ($sl) $params['sl'] = $sl;
if ($timeout) $params['timeout'] = $timeout;
ksort($params);
$parameters = '';
foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
$parameters = ltrim($parameters, "&");
/* Generate signature. */
if($this->_key <> "") {
$signature = base64_encode(hash_hmac('sha1', $parameters,
$this->_key, true));
$signature = preg_replace('/\+/', '%2B', $signature);
$parameters .= '&h=' . $signature;
}
/* Generate and prepare request. */
$this->_lastquery=null;
$this->URLreset();
$mh = curl_multi_init();
$ch = array();
while($URLpart=$this->getNextURLpart())
{
/* Support https. */
if ($this->_https) {
$query = "https://";
} else {
$query = "http://";
}
$query .= $URLpart . "?" . $parameters;
if ($this->_lastquery) { $this->_lastquery .= " "; }
$this->_lastquery .= $query;
$handle = curl_init($query);
curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
if (!$this->_httpsverify) {
curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
}
curl_setopt($handle, CURLOPT_FAILONERROR, true);
/* If timeout is set, we better apply it here as well
in case the validation server fails to follow it.
*/
if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
curl_multi_add_handle($mh, $handle);
$ch[(int)$handle] = $handle;
}
/* Execute and read request. */
$this->_response=null;
$replay=False;
$valid=False;
do {
/* Let curl do its work. */
while (($mrc = curl_multi_exec($mh, $active))
== CURLM_CALL_MULTI_PERFORM)
;
while ($info = curl_multi_info_read($mh)) {
if ($info['result'] == CURLE_OK) {
/* We have a complete response from one server. */
$str = curl_multi_getcontent($info['handle']);
$cinfo = curl_getinfo ($info['handle']);
if ($wait_for_all) { # Better debug info
$this->_response .= 'URL=' . $cinfo['url'] ."\n"
. $str . "\n";
}
if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
$status = $out[1];
/*
* There are 3 cases.
*
* 1. OTP or Nonce values doesn't match - ignore
* response.
*
* 2. We have a HMAC key. If signature is invalid -
* ignore response. Return if status=OK or
* status=REPLAYED_OTP.
*
* 3. Return if status=OK or status=REPLAYED_OTP.
*/
if (!preg_match("/otp=".$params['otp']."/", $str) ||
!preg_match("/nonce=".$params['nonce']."/", $str)) {
/* Case 1. Ignore response. */
}
elseif ($this->_key <> "") {
/* Case 2. Verify signature first */
$rows = explode("\r\n", trim($str));
$response=array();
while (list($key, $val) = each($rows)) {
/* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
$val = preg_replace('/=/', '#', $val, 1);
$row = explode("#", $val);
$response[$row[0]] = $row[1];
}
$parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
sort($parameters);
$check=Null;
foreach ($parameters as $param) {
if (array_key_exists($param, $response)) {
if ($check) $check = $check . '&';
$check = $check . $param . '=' . $response[$param];
}
}
$checksignature =
base64_encode(hash_hmac('sha1', utf8_encode($check),
$this->_key, true));
if($response['h'] == $checksignature) {
if ($status == 'REPLAYED_OTP') {
if (!$wait_for_all) { $this->_response = $str; }
$replay=True;
}
if ($status == 'OK') {
if (!$wait_for_all) { $this->_response = $str; }
$valid=True;
}
}
} else {
/* Case 3. We check the status directly */
if ($status == 'REPLAYED_OTP') {
if (!$wait_for_all) { $this->_response = $str; }
$replay=True;
}
if ($status == 'OK') {
if (!$wait_for_all) { $this->_response = $str; }
$valid=True;
}
}
}
if (!$wait_for_all && ($valid || $replay))
{
/* We have status=OK or status=REPLAYED_OTP, return. */
foreach ($ch as $h) {
curl_multi_remove_handle($mh, $h);
curl_close($h);
}
curl_multi_close($mh);
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
if ($valid) return true;
return PEAR::raiseError($status);
}
curl_multi_remove_handle($mh, $info['handle']);
curl_close($info['handle']);
unset ($ch[(int)$info['handle']]);
}
curl_multi_select($mh);
}
} while ($active);
/* Typically this is only reached for wait_for_all=true or
* when the timeout is reached and there is no
* OK/REPLAYED_REQUEST answer (think firewall).
*/
foreach ($ch as $h) {
curl_multi_remove_handle ($mh, $h);
curl_close ($h);
}
curl_multi_close ($mh);
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
if ($valid) return true;
return PEAR::raiseError('NO_VALID_ANSWER');
}
}
?>

View File

@@ -0,0 +1,103 @@
<?php
//ini_set("session.cookie_secure", 1);
//ini_set("session.cookie_httponly", 1);
session_start();
if (isset($_POST["logout"])) {
if (isset($_SESSION["dual-login"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION["dual-login"]);
}
else {
session_unset();
session_destroy();
session_write_close();
setcookie(session_name(),'',0,'/');
}
}
require_once 'inc/vars.inc.php';
if (file_exists('./inc/vars.local.inc.php')) {
include_once 'inc/vars.local.inc.php';
}
// Yubi OTP API
require_once 'inc/lib/Yubico.php';
// U2F API
require_once 'inc/lib/U2F.php';
$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']);
// PDO
$dsn = "$database_type:host=$database_host;dbname=$database_name";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
?>
<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
<?php
exit;
}
$_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
setcookie('language', $DEFAULT_LANG);
if (isset($_COOKIE['language'])) {
switch ($_COOKIE['language']) {
case "de":
$_SESSION['mailcow_locale'] = 'de';
setcookie('language', 'de');
break;
case "en":
$_SESSION['mailcow_locale'] = 'en';
setcookie('language', 'en');
break;
case "es":
$_SESSION['mailcow_locale'] = 'es';
setcookie('language', 'es');
break;
case "nl":
$_SESSION['mailcow_locale'] = 'nl';
setcookie('language', 'nl');
break;
case "pt":
$_SESSION['mailcow_locale'] = 'pt';
setcookie('language', 'pt');
break;
}
}
if (isset($_GET['lang'])) {
switch ($_GET['lang']) {
case "de":
$_SESSION['mailcow_locale'] = 'de';
setcookie('language', 'de');
break;
case "en":
$_SESSION['mailcow_locale'] = 'en';
setcookie('language', 'en');
break;
case "es":
$_SESSION['mailcow_locale'] = 'es';
setcookie('language', 'es');
break;
case "nl":
$_SESSION['mailcow_locale'] = 'nl';
setcookie('language', 'nl');
break;
case "pt":
$_SESSION['mailcow_locale'] = 'pt';
setcookie('language', 'pt');
break;
}
}
require_once 'lang/lang.en.php';
include 'lang/lang.'.$_SESSION['mailcow_locale'].'.php';
require_once 'inc/functions.inc.php';
require_once 'inc/triggers.inc.php';
(!isset($_SESSION['mailcow_cc_username'])) ? init_db_schema() : null;

133
data/web/inc/tfa_modals.php Normal file
View File

@@ -0,0 +1,133 @@
<div class="modal fade" id="YubiOTPModal" tabindex="-1" role="dialog" aria-labelledby="YubiOTPModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header"><b><?=$lang['tfa']['yubi_otp'];?></b></div>
<div class="modal-body">
<form role="form" method="post">
<div class="form-group">
<input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
</div>
<hr>
<p class="help-block"><?=$lang['tfa']['api_register'];?></p>
<div class="form-group">
<input type="text" class="form-control" name="yubico_id" id="yubico_id" placeholder="Yubico API ID" autocomplete="off" required>
</div>
<div class="form-group">
<input type="text" class="form-control" name="yubico_key" id="yubico_key" placeholder="Yubico API Key" autocomplete="off" required>
</div>
<hr>
<div class="form-group">
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
<input type="text" name="otp_token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
<input type="hidden" name="tfa_method" value="yubi_otp">
</div>
</div>
<button class="btn btn-sm btn-default" type="submit" name="set_tfa"><?=$lang['user']['save_changes'];?></button>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="U2FModal" tabindex="-1" role="dialog" aria-labelledby="U2FModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header"><b><?=$lang['tfa']['u2f'];?></b></div>
<div class="modal-body">
<form role="form" method="post" id="u2f_reg_form">
<div class="form-group">
<input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
</div>
<div class="form-group">
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
</div>
<hr>
<p><?=$lang['tfa']['waiting_usb_register'];?></p>
<div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
<input type="hidden" name="token" id="u2f_register_data"/>
<input type="hidden" name="tfa_method" value="u2f">
<input type="hidden" name="set_tfa"/><br/>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="DisableTFAModal" tabindex="-1" role="dialog" aria-labelledby="DisableTFAModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header"><b><?=$lang['tfa']['delete_tfa'];?></b></div>
<div class="modal-body">
<form role="form" method="post">
<div class="input-group">
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
<span class="input-group-btn">
<input type="hidden" name="tfa_method" value="none">
<button class="btn btn-danger" type="submit" name="set_tfa"><?=$lang['tfa']['delete_tfa'];?></button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
<?php
if (isset($_SESSION['pending_tfa_method'])):
$tfa_method = $_SESSION['pending_tfa_method'];
?>
<div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><b><?=$lang['tfa'][$tfa_method];?></b></div>
<div class="modal-body">
<?php
switch ($tfa_method) {
case "yubi_otp":
?>
<form role="form" method="post">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
<input type="text" name="token" id="token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
<input type="hidden" name="tfa_method" value="yubi_otp">
</div>
</div>
<button class="btn btn-sm btn-default" type="submit" name="verify_tfa_login"><?=$lang['login']['login'];?></button>
</form>
<?php
break;
case "u2f":
?>
<form role="form" method="post" id="u2f_auth_form">
<p><?=$lang['tfa']['waiting_usb_auth'];?></p>
<div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
<input type="hidden" name="token" id="u2f_auth_data"/>
<input type="hidden" name="tfa_method" value="u2f">
<input type="hidden" name="verify_tfa_login"/><br/>
</form>
<?php
break;
case "totp":
?>
<div class="empty"></div>
<?php
break;
case "hotp":
?>
<div class="empty"></div>
<?php
break;
}
?>
</div>
</div>
</div>
</div>
<?php
endif;
?>

View File

@@ -0,0 +1,173 @@
<?php
if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST["token"])) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']);
header("Location: /user.php");
}
}
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]);
if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin";
header("Location: /admin.php");
}
elseif ($as == "domainadmin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "domainadmin";
header("Location: /mailbox.php");
}
elseif ($as == "user") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "user";
header("Location: /user.php");
}
elseif ($as != "pending") {
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']);
unset($_SESSION['mailcow_cc_username']);
unset($_SESSION['mailcow_cc_role']);
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => $lang['danger']['login_failed']
);
}
}
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
if (isset($_GET["duallogin"])) {
if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :duallogin");
$stmt->execute(array(':duallogin' => $_GET["duallogin"]));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username'];
$_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role'];
$_SESSION['mailcow_cc_username'] = $_GET["duallogin"];
$_SESSION['mailcow_cc_role'] = "user";
header("Location: /user.php");
}
}
}
if (isset($_POST["edit_admin_account"])) {
edit_admin_account($_POST);
}
if (isset($_POST["dkim_delete_key"])) {
dkim_delete_key($_POST);
}
if (isset($_POST["dkim_add_key"])) {
dkim_add_key($_POST);
}
if (isset($_POST["add_domain_admin"])) {
add_domain_admin($_POST);
}
if (isset($_POST["delete_domain_admin"])) {
delete_domain_admin($_POST);
}
}
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
if (isset($_POST["edit_user_account"])) {
edit_user_account($_POST);
}
if (isset($_POST["mailbox_reset_eas"])) {
mailbox_reset_eas($_POST);
}
if (isset($_POST["edit_spam_score"])) {
edit_spam_score($_POST);
}
if (isset($_POST["edit_delimiter_action"])) {
edit_delimiter_action($_POST);
}
if (isset($_POST["add_policy_list_item"])) {
add_policy_list_item($_POST);
}
if (isset($_POST["delete_policy_list_item"])) {
delete_policy_list_item($_POST);
}
if (isset($_POST["edit_tls_policy"])) {
edit_tls_policy($_POST);
}
if (isset($_POST["add_syncjob"])) {
add_syncjob($_POST);
}
if (isset($_POST["edit_syncjob"])) {
edit_syncjob($_POST);
}
if (isset($_POST["delete_syncjob"])) {
delete_syncjob($_POST);
}
if (isset($_POST["set_time_limited_aliases"])) {
set_time_limited_aliases($_POST);
}
}
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
if (isset($_POST["edit_domain_admin"])) {
edit_domain_admin($_POST);
}
if (isset($_POST["set_tfa"])) {
set_tfa($_POST);
}
if (isset($_POST["unset_tfa_key"])) {
unset_tfa_key($_POST);
}
if (isset($_POST["add_policy_list_item"])) {
add_policy_list_item($_POST);
}
if (isset($_POST["delete_policy_list_item"])) {
delete_policy_list_item($_POST);
}
if (isset($_POST["mailbox_add_domain"])) {
mailbox_add_domain($_POST);
}
if (isset($_POST["mailbox_add_alias"])) {
mailbox_add_alias($_POST);
}
if (isset($_POST["mailbox_add_alias_domain"])) {
mailbox_add_alias_domain($_POST);
}
if (isset($_POST["mailbox_add_mailbox"])) {
mailbox_add_mailbox($_POST);
}
if (isset($_POST["mailbox_add_resource"])) {
mailbox_add_resource($_POST);
}
if (isset($_POST["mailbox_edit_alias"])) {
mailbox_edit_alias($_POST);
}
if (isset($_POST["mailbox_edit_domain"])) {
mailbox_edit_domain($_POST);
}
if (isset($_POST["mailbox_edit_mailbox"])) {
mailbox_edit_mailbox($_POST);
}
if (isset($_POST["mailbox_edit_alias_domain"])) {
mailbox_edit_alias_domain($_POST);
}
if (isset($_POST["mailbox_edit_resource"])) {
mailbox_edit_resource($_POST);
}
if (isset($_POST["mailbox_delete_domain"])) {
mailbox_delete_domain($_POST);
}
if (isset($_POST["mailbox_delete_alias"])) {
mailbox_delete_alias($_POST);
}
if (isset($_POST["mailbox_delete_alias_domain"])) {
mailbox_delete_alias_domain($_POST);
}
if (isset($_POST["mailbox_delete_mailbox"])) {
mailbox_delete_mailbox($_POST);
}
if (isset($_POST["mailbox_delete_resource"])) {
mailbox_delete_resource($_POST);
}
}
?>

38
data/web/inc/vars.inc.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
error_reporting(E_ERROR | E_WARNING);
//error_reporting(E_ALL);
/*
PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!
This file will be reset on upgrades.
*/
// SQL database connection variables
$database_type = "mysql";
$database_host = "mysql";
$database_user = getenv('DBUSER');
$database_pass = getenv('DBPASS');
$database_name = getenv('DBNAME');
// Other variables
$mailcow_hostname = getenv('MAILCOW_HOSTNAME');
// Where to go after adding and editing objects
// Can be "form" or "previous"
// "form" will stay in the current form, "previous" will redirect to previous page
$FORM_ACTION = "previous";
// File locations should not be changed
$MC_DKIM_TXTS = "/data/dkim/txt";
$MC_DKIM_KEYS = "/data/dkim/keys";
// Change default language, "en", "pt", "de" or "nl"
$DEFAULT_LANG = "en";
// Change theme (default: lumen)
// Needs to be one of those: cerulean, cosmo, cyborg, darkly, flatly, journal, lumen, paper, readable, sandstone,
// simplex, slate, spacelab, superhero, united, yeti
// See https://bootswatch.com/
$DEFAULT_THEME = "lumen";
?>