Dovecot
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 6a451cd9..649aa4e7 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -251,20 +251,60 @@ function password_check($password1, $password2) {
return true;
}
-function last_login($user) {
+function last_login($action, $username) {
global $pdo;
- $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
- WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
- AND JSON_EXTRACT(`call`, "$[1]") = :user
- AND `type` = "success" ORDER BY `time` DESC LIMIT 1');
- $stmt->execute(array(':user' => $user));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (!empty($row)) {
- return $row;
- }
- else {
- return false;
+ switch ($action) {
+ case 'get':
+ if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+ $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service` FROM `sasl_logs`
+ WHERE `username` = :username
+ AND `success` = 1
+ GROUP BY `real_rip`, `service`
+ ORDER BY `datetime` DESC
+ LIMIT 5;');
+ $stmt->execute(array(':username' => $username));
+ $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($sasl as $k => $v) {
+ if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+ $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')';
+ }
+ }
+ }
+ else {
+ $sasl = array();
+ }
+ if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
+ $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
+ WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
+ AND JSON_EXTRACT(`call`, "$[1]") = :username
+ AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET 1');
+ $stmt->execute(array(':username' => $username));
+ $ui = $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+ else {
+ $ui = array();
+ }
+
+ return array('ui' => $ui, 'sasl' => $sasl);
+ break;
+ case 'reset':
+ if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+ $stmt = $pdo->prepare('DELETE FROM `sasl_logs`
+ WHERE `username` = :username
+ AND `success` = 1;');
+ $stmt->execute(array(':username' => $username));
+ }
+ if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
+ $stmt = $pdo->prepare('DELETE FROM `logs`
+ WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
+ AND JSON_EXTRACT(`call`, "$[1]") = :username
+ AND `type` = "success"');
+ $stmt->execute(array(':username' => $username));
+ }
+ return true;
+ break;
}
+
}
function flush_memcached() {
try {
@@ -1862,6 +1902,26 @@ function get_logs($application, $lines = false) {
return $data;
}
}
+ if ($application == "sasl") {
+ if (isset($from) && isset($to)) {
+ $stmt = $pdo->prepare("SELECT * FROM `sasl_logs` ORDER BY `id` DESC LIMIT :from, :to");
+ $stmt->execute(array(
+ ':from' => $from - 1,
+ ':to' => $to
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT * FROM `sasl_logs` ORDER BY `id` DESC LIMIT :lines");
+ $stmt->execute(array(
+ ':lines' => $lines + 1,
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ if (is_array($data)) {
+ return $data;
+ }
+ }
// Redis
if ($application == "dovecot-mailcow") {
if (isset($from) && isset($to)) {
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 8bea647b..12e78047 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -3503,18 +3503,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false;
}
$mailboxdata = array();
- $last_imap_login = $redis->Get('last-login/imap/' . $_data);
- $last_smtp_login = $redis->Get('last-login/smtp/' . $_data);
- $last_pop3_login = $redis->Get('last-login/pop3/' . $_data);
- if ($last_imap_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
- $last_imap_login = '0';
- }
- if ($last_smtp_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
- $last_smtp_login = '0';
- }
- if ($last_pop3_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
- $last_pop3_login = '0';
- }
if (preg_match('/y|yes/i', getenv('MASTER'))) {
$stmt = $pdo->prepare("SELECT
`domain`.`backupmx`,
@@ -3575,10 +3563,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['quota_used'] = intval($row['bytes']);
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
- $mailboxdata['last_imap_login'] = $last_imap_login;
- $mailboxdata['last_smtp_login'] = $last_smtp_login;
- $mailboxdata['last_pop3_login'] = $last_pop3_login;
-
if ($mailboxdata['percent_in_use'] === '- ') {
$mailboxdata['percent_class'] = "info";
}
@@ -3592,11 +3576,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['percent_class'] = "success";
}
+ // Determine last logins
+ $stmt = $pdo->prepare("SELECT MAX(`datetime`) AS `datetime`, `service` FROM `sasl_logs`
+ WHERE `username` = :mailbox
+ AND `success` = 1
+ GROUP BY `service` DESC");
+ $stmt->execute(array(':mailbox' => $_data));
+ $SaslLogsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($SaslLogsData as $SaslLogs) {
+ if ($SaslLogs['service'] == 'imap') {
+ $last_imap_login = strtotime($SaslLogs['datetime']);
+ }
+ else if ($SaslLogs['service'] == 'smtp') {
+ $last_smtp_login = strtotime($SaslLogs['datetime']);
+ }
+ else if ($SaslLogs['service'] == 'pop3') {
+ $last_pop3_login = strtotime($SaslLogs['datetime']);
+ }
+ }
+ if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+ $last_imap_login = 0;
+ }
+ if (!isset($last_smtp_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+ $last_smtp_login = 0;
+ }
+ if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+ $last_pop3_login = 0;
+ }
+ $mailboxdata['last_imap_login'] = $last_imap_login;
+ $mailboxdata['last_smtp_login'] = $last_smtp_login;
+ $mailboxdata['last_pop3_login'] = $last_pop3_login;
+
if (!isset($_extra) || $_extra != 'reduced') {
$rl = ratelimit('get', 'mailbox', $_data);
$stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM `domain` WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $row['domain']));
$DomainQuota = $stmt->fetch(PDO::FETCH_ASSOC);
+
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`active`), 0) AS `pushover_active` FROM `pushover` WHERE `username` = :username AND `active` = 1");
$stmt->execute(array(':username' => $_data));
$PushoverActive = $stmt->fetch(PDO::FETCH_ASSOC);
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 797ca422..c349593c 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
- $db_version = "27052021_2000";
+ $db_version = "03062021_2320";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -510,6 +510,30 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
+ "sasl_logs" => array(
+ "cols" => array(
+ "id" => "INT NOT NULL AUTO_INCREMENT",
+ "success" => "TINYINT(1) NOT NULL DEFAULT '0'",
+ "service" => "VARCHAR(32) NOT NULL DEFAULT ''",
+ "app_password" => "INT",
+ "username" => "VARCHAR(255) NOT NULL",
+ "real_rip" => "VARCHAR(64) NOT NULL",
+ "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)"
+ ),
+ "keys" => array(
+ "primary" => array(
+ "" => array("id")
+ ),
+ "key" => array(
+ "username" => array("username"),
+ "service" => array("service"),
+ "success" => array("success"),
+ "datetime" => array("datetime"),
+ "real_rip" => array("real_rip")
+ )
+ ),
+ "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+ ),
"quota2" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php
index 478bb4bf..4389ab35 100644
--- a/data/web/inc/triggers.inc.php
+++ b/data/web/inc/triggers.inc.php
@@ -24,19 +24,16 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin";
- $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
header("Location: /admin");
}
elseif ($as == "domainadmin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "domainadmin";
- $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
header("Location: /mailbox");
}
elseif ($as == "user") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "user";
- $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
$http_parameters = explode('&', $_SESSION['index_query_string']);
unset($_SESSION['index_query_string']);
if (in_array('mobileconfig', $http_parameters)) {
diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js
index fa9ffbb5..866dba2c 100644
--- a/data/web/js/site/debug.js
+++ b/data/web/js/site/debug.js
@@ -271,7 +271,7 @@ jQuery(function($){
{"name":"role","title":"Role"},
{"name":"remote","title":"IP"},
{"name":"msg","title":lang.message,"style":{"word-break":"break-all"}},
- {"name":"call","title":"Call","breakpoints": "all"},
+ {"name":"call","title":"Call","breakpoints": "all"}
],
"rows": $.ajax({
dataType: 'json',
@@ -301,6 +301,43 @@ jQuery(function($){
}
});
}
+ function draw_sasl_logs() {
+ ft_api_logs = FooTable.init('#sasl_logs', {
+ "columns": [
+ {"name":"success","title":lang.success,"filterable": false,"style":{"width":"30px"}},
+ {"name":"username","title":lang.username},
+ {"name":"service","title":lang.service},
+ {"name":"real_rip","title":"IP"},
+ {"sorted": true,"sortValue": function(value){res = new Date(value);return res.getTime();},"direction":"DESC","name":"datetime","formatter":function date_format(datetime) { var date = new Date(datetime.replace(/-/g, "/")); return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});},"title":lang.login_time,"style":{"width":"170px"}},
+ ],
+ "rows": $.ajax({
+ dataType: 'json',
+ url: '/api/v1/get/logs/sasl',
+ jsonp: false,
+ error: function () {
+ console.log('Cannot draw sasl log table');
+ },
+ success: function (data) {
+ return process_table_data(data, 'sasl_log_table');
+ }
+ }),
+ "empty": lang.empty,
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"delay": 1200,"position": "left","connectors": false,"placeholder": lang.filter_table,"connectors": false},
+ "sorting": {"enabled": true},
+ "on": {
+ "destroy.ft.table": function(e, ft){
+ $('.refresh_table').attr('disabled', 'true');
+ },
+ "ready.ft.table": function(e, ft){
+ table_log_ready(ft, 'sasl_logs');
+ },
+ "after.ft.paging": function(e, ft){
+ table_log_paging(ft, 'sasl_logs');
+ }
+ }
+ });
+ }
function draw_acme_logs() {
ft_acme_logs = FooTable.init('#acme_log', {
"columns": [
@@ -666,6 +703,20 @@ jQuery(function($){
item.task = '
' + item.task + '
';
item.type = '
' + item.type + '';
});
+ } else if (table == 'sasl_log_table') {
+ $.each(data, function (i, item) {
+ if (item === null) { return true; }
+ item.username = escapeHtml(item.username);
+ if (item.service == "smtp") { item.service = '
' + item.service.toUpperCase() + '
'; }
+ else if (item.service == "imap") { item.service = '
' + item.service.toUpperCase() + '
'; }
+ else { item.service = '
' + item.service.toUpperCase() + '
'; }
+ if (item.success == 0) {
+ item.success = '
';
+ }
+ else {
+ item.success = '
';
+ }
+ });
} else if (table == 'general_syslog') {
$.each(data, function (i, item) {
if (item === null) { return true; }
@@ -750,6 +801,7 @@ jQuery(function($){
draw_api_logs();
draw_rl_logs();
draw_ui_logs();
+ draw_sasl_logs();
draw_netfilter_logs();
draw_rspamd_history();
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js
index c2ff3897..0ccabcf4 100644
--- a/data/web/js/site/user.js
+++ b/data/web/js/site/user.js
@@ -42,6 +42,7 @@ $(document).ready(function() {
});
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
+
});
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
@@ -70,8 +71,65 @@ jQuery(function($){
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
}
acl_data = JSON.parse(acl);
- var last_login = $('.last_login_date').data('time');
- $('.last_login_date').text(unix_time_format(last_login));
+
+ $('.clear-last-logins').on('click', function () {
+ if (confirm(lang.delete_ays)) {
+ last_logins('reset');
+ }
+ })
+
+ function last_logins(action) {
+ if (action == 'get') {
+ $.ajax({
+ dataType: 'json',
+ url: '/api/v1/get/last-login/' + encodeURIComponent(mailcow_cc_username),
+ jsonp: false,
+ error: function () {
+ console.log('error reading last logins');
+ },
+ success: function (data) {
+ $('.last-login').html();
+ if (data.ui.time) {
+ $('.last-login').html('
' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time));
+ } else {
+ $('.last-login').text(lang.no_last_login);
+ }
+ if (data.sasl) {
+ $('.last-login').append('
');
+ $.each(data.sasl, function (i, item) {
+ var datetime = new Date(item.datetime.replace(/-/g, "/"));
+ var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+ if (item.service == "smtp") { service = '' + item.service.toUpperCase() + '
'; }
+ else if (item.service == "imap") { service = ' ' + item.service.toUpperCase() + '
'; }
+ else { service = '' + item.service.toUpperCase() + '
'; }
+ if (item.real_rip.startsWith("Web")) {
+ real_rip = item.real_rip;
+ } else {
+ real_rip = '' + item.real_rip + '';
+ }
+ $('.last-login').append('- ' +
+ local_datetime + ' ' + service + ' ' + lang.from + ' ' +
+ real_rip +
+ '
');
+ })
+ $('.last-login').append('
');
+ }
+ }
+ })
+ } else if (action == 'reset') {
+ $.ajax({
+ dataType: 'json',
+ url: '/api/v1/get/reset-last-login/' + encodeURIComponent(mailcow_cc_username),
+ jsonp: false,
+ error: function () {
+ console.log('cannot reset last logins');
+ },
+ success: function (data) {
+ last_logins('get');
+ }
+ })
+ }
+ }
function draw_tla_table() {
ft_tla_table = FooTable.init('#tla_table', {
@@ -132,7 +190,7 @@ jQuery(function($){
{"name":"log","title":"Log"},
{"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active,"formatter": function(value){return 1==value?'
':0==value&&'
';}},
{"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status},
- {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+ {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"260px","width":"260px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
],
"empty": lang.empty,
"rows": $.ajax({
@@ -324,6 +382,7 @@ jQuery(function($){
draw_tla_table();
draw_wl_policy_mailbox_table();
draw_bl_policy_mailbox_table();
+ last_logins('get');
// FIDO2 friendly name modal
$('#fido2ChangeFn').on('show.bs.modal', function (e) {
diff --git a/data/web/json_api.php b/data/web/json_api.php
index a1198f2f..e8e9e888 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -306,7 +306,6 @@ if (isset($_GET['query'])) {
$_SESSION["mailcow_cc_role"] = "domainadmin";
}
$_SESSION["mailcow_cc_username"] = $process_fido2['username'];
- $_SESSION['mailcow_cc_last_login'] = last_login($process_fido2['username']);
$_SESSION["fido2_cid"] = $process_fido2['cid'];
unset($_SESSION["challenge"]);
$_SESSION['return'][] = array(
@@ -640,6 +639,21 @@ if (isset($_GET['query'])) {
}
break;
+ case "last-login":
+ if ($object) {
+ $data = last_login('get', $object);
+ process_get_return($data);
+ }
+ break;
+
+ // Todo: move to delete
+ case "reset-last-login":
+ if ($object) {
+ $data = last_login('reset', $object);
+ process_get_return($data);
+ }
+ break;
+
case "transport":
switch ($object) {
case "all":
@@ -800,6 +814,17 @@ if (isset($_GET['query'])) {
}
echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
break;
+ case "sasl":
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
+ $logs = get_logs('sasl', $extra);
+ }
+ else {
+ $logs = get_logs('sasl');
+ }
+ echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
+ break;
case "watchdog":
// 0 is first record, so empty is fine
if (isset($extra)) {
@@ -1458,7 +1483,6 @@ if (isset($_GET['query'])) {
process_delete_return(dkim('delete', array('domains' => $items)));
break;
case "domain":
- file_put_contents('/tmp/dssaa', $items);
process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
break;
case "alias-domain":
diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json
index 494cf1fa..a35ecf82 100644
--- a/data/web/lang/lang.de.json
+++ b/data/web/lang/lang.de.json
@@ -119,6 +119,10 @@
"validation_success": "Erfolgreich validiert"
},
"admin": {
+ "success": "Erfolg",
+ "service": "Dienst",
+ "login_time": "Zeit",
+ "username": "Benutzername",
"access": "Zugang",
"action": "Aktion",
"activate_api": "API aktivieren",
@@ -489,6 +493,10 @@
"started_at": "Gestartet am",
"solr_status": "Solr Status",
"uptime": "Uptime",
+ "success": "Erfolg",
+ "service": "Dienst",
+ "login_time": "Zeit",
+ "username": "Benutzername",
"started_on": "Gestartet am",
"static_logs": "Statische Logs",
"system_containers": "System & Container",
@@ -1032,6 +1040,9 @@
"excludes": "Ausschlüsse",
"expire_in": "Ungültig in",
"force_pw_update": "Das Passwort für diesen Benutzer
muss geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.",
+ "from": "von",
+ "recent_successful_connections": "Kürzlich erfolgreiche Verbindungen",
+ "clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
"generate": "generieren",
"hour": "Stunde",
"hourly": "Stündlich",
@@ -1041,6 +1052,7 @@
"is_catch_all": "Ist Catch-All-Adresse für Domain(s)",
"last_mail_login": "Letzter Mail-Login",
"last_run": "Letzte Ausführung",
+ "last_ui_login": "Letzte UI Anmeldung",
"loading": "Lade...",
"mailbox_details": "Mailbox-Details",
"messages": "Nachrichten",
diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json
index 932eb34d..fc015299 100644
--- a/data/web/lang/lang.en.json
+++ b/data/web/lang/lang.en.json
@@ -117,6 +117,10 @@
"validation_success": "Validated successfully"
},
"admin": {
+ "success": "Success",
+ "service": "Service",
+ "login_time": "Login time",
+ "username": "Username",
"access": "Access",
"action": "Action",
"activate_api": "Activate API",
@@ -486,8 +490,12 @@
"size": "Size",
"started_at": "Started at",
"solr_status": "Solr status",
- "uptime": "Uptime",
"started_on": "Started on",
+ "uptime": "Uptime",
+ "success": "Success",
+ "service": "Service",
+ "login_time": "Time",
+ "username": "Username",
"static_logs": "Static logs",
"system_containers": "System & Containers",
"xmpp_status": "XMPP status"
@@ -1030,6 +1038,9 @@
"excludes": "Excludes",
"expire_in": "Expire in",
"force_pw_update": "You
must set a new password to be able to access groupware related services.",
+ "from": "from",
+ "recent_successful_connections": "Seen successful connections",
+ "clear_recent_successful_connections": "Clear seen successful connections",
"generate": "generate",
"hour": "hour",
"hourly": "Hourly",
@@ -1039,6 +1050,7 @@
"is_catch_all": "Catch-all for domain/s",
"last_mail_login": "Last mail login",
"last_run": "Last run",
+ "last_ui_login": "Last UI login",
"loading": "Loading...",
"mailbox_details": "Mailbox details",
"messages": "messages",
diff --git a/data/web/user.php b/data/web/user.php
index 61a04be2..466ffc4d 100644
--- a/data/web/user.php
+++ b/data/web/user.php
@@ -22,15 +22,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
@@ -181,15 +174,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
[=$lang['user']['change_password'];?>]
[=$lang['user']['client_configuration'];?>]
[=$lang['user']['show_sieve_filters'];?>]
-
-
- (=$_SESSION['mailcow_cc_last_login']['remote'];?>)
-
-
+
+
=$lang['user']['recent_successful_connections'];?>
+
+
=$lang['user']['clear_recent_successful_connections'];?>