Compare commits

...
Author SHA1 Message Date
FreddleSpl0itandGitHub d80b15b06c Merge pull request #7376 from smpaz7467/fix/extended-sender-acl-authsource
[Web] show external sender addresses regardless of authsource
2026-08-18 14:41:20 +02:00
FreddleSpl0itandGitHub ed4fe41d7a Merge pull request #7349 from smpaz7467/fix/login-redirect-deeplink
[Web] redirect deep links to the matching login page
2026-08-18 14:37:32 +02:00
FreddleSpl0itandGitHub 6b9d875773 Merge pull request #7347 from smpaz7467/fix/dns-zonefile-absolute-rhs
[Web] use absolute RHS names in generated DNS zonefile
2026-08-18 14:30:50 +02:00
FreddleSpl0itandGitHub 4d5b9d1c80 Merge pull request #7342 from FrauJulian/feat/show-password
[Web] Add show/hide password toggle on login pages
2026-08-18 14:25:22 +02:00
FreddleSpl0itandGitHub 8abf453e1c Merge pull request #7428 from mailcow/feat/forbid-external-alias
[Web] Add ACL + global switch to disable external alias goto
2026-08-18 14:13:39 +02:00
FreddleSpl0it 8bd9e12a73 [Web] Add ACL + global switch to disable external alias goto 2026-08-18 14:07:37 +02:00
FreddleSpl0itandGitHub c665d430de Merge pull request #7344 from smpaz7467/fix/forced-pw-change-raw-lang-key
[Web] translate password errors in the forced password change modal
2026-08-18 13:31:35 +02:00
FreddleSpl0itandGitHub 2195a674ff Merge pull request #7290 from wryfi/7150-fix-passwd-verify
Fix #7150: passwd-verify.lua wipes auth cache during any nginx outage
2026-08-18 13:27:01 +02:00
FreddleSpl0itandGitHub 089c44aee1 Merge pull request #7334 from DerLinkman/feat/postfix-tlspol-update
postfix-tlspol: upgrade to trixie + 1.11.0 update
2026-08-18 13:20:16 +02:00
FreddleSpl0itandGitHub 6644699e65 Merge pull request #7426 from mailcow/fix/web-hardening
[Web] Minor hardening across web UI and nginx
2026-08-18 09:10:09 +02:00
FreddleSpl0it 91d3b7246a [Web] Minor hardening across web UI and nginx 2026-08-18 09:08:17 +02:00
FreddleSpl0itandGitHub b362c7105d Merge pull request #7425 from mailcow/feat/redis-7.4.10
[Redis] Update to 7.4.10
2026-08-18 08:14:21 +02:00
Stephen Ritz 9cd16f0001 [Web] show external sender addresses regardless of authsource
The extended_sender_acl field sat inside the
`{% if not result.authsource or result.authsource == 'mailcow' %}` block
that hides the local password fields for mailboxes authenticating against
an external identity provider. As a result the "External sender addresses"
input was not rendered at all for keycloak / generic-oidc / ldap mailboxes,
even for a full admin, so those addresses could neither be reviewed nor
edited in the UI while the underlying sender_acl rows stayed active.

Extended sender ACLs are unrelated to local password management. Move the
field out of that block; it stays gated by acl.extend_sender_acl as intended.

Fixes #7365
2026-07-28 07:12:17 -07:00
Stephen RitzandClaude Opus 4.8 90ca1bf25a [Web] redirect deep links to the matching login page
An unauthenticated request to a deep link such as /admin/dashboard was
redirected to /, the user login, instead of the admin login. protect_route
always sent unauthenticated visitors to /.

Pick the login page from the request path: /admin/* redirects to /admin,
/domainadmin/* to /domainadmin, everything else to / as before.

Fixes #7284

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:39:19 -07:00
Stephen RitzandClaude Opus 4.8 e5d0ed8c4c [Web] use absolute RHS names in generated DNS zonefile
The DNS overview "Download" produces a $ORIGIN zonefile, but the
right-hand side of MX, CNAME and SRV records was emitted as a relative
name. A target such as mail.example.net is then read relative to the
origin and expands to mail.example.net.example.org., which is wrong.

The only prior attempt at making names absolute was
str_replace($domain, $domain . '.', ...), which appended a dot only to
targets that happened to contain the origin domain, so cross-domain
targets stayed relative. That same replace also corrupted any TXT value
containing the origin (e.g. a DMARC rua=mailto:x@example.org became
...@example.org.).

Absolutize the RHS per record type at export time only: MX and CNAME
targets, and the SRV target token, get a trailing dot; ports, the SRV
root target ".", IP addresses and TXT character strings are left as is.
The records used for the on-page DNS validation are untouched, so
matching against dns_get_record() output still works.

Fixes #6984

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:33:46 -07:00
Stephen RitzandClaude Opus 4.8 4cba448671 [Web] translate password errors in the forced password change modal
The forced password change modal posts to the JSON API and renders
data[0].msg directly. The API returns raw language keys rather than
translated strings, so a user who fails the complexity policy is shown
the literal text "password_complexity" instead of a message.

Every other password form renders errors through alertbox_log_parser(),
which resolves the key against $lang. The modal is the only one that
talks to the API directly, and it never resolved the key.

Resolve it against lang_danger, which base.twig already exposes for this
purpose. This also covers password_mismatch, password_empty and
access_denied, and applies to the admin path as well. Unknown keys still
fall through unchanged.

The lookup is guarded with hasOwnProperty because msg is attacker-
independent but dynamic: a bare lang_danger[msg] would resolve inherited
Object.prototype members such as "constructor" to a function, which
jQuery's .text() would then invoke as a callback.

Language files are untouched: prerequisites.inc.php loads lang.en-gb.json
as the base and merges the active locale over it, so locales that lack the
key inherit the English string.

Fixes #7301

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:11:17 -07:00
Paul SeebachandJulian Lechner 5408bc809d [Web] Use input-group-text for password toggle button
Replaces btn-outline-secondary with input-group-text so the toggle
button border matches the surrounding input group consistently.
2026-07-14 21:20:58 +02:00
Paul SeebachandJulian Lechner bd5c18b145 [Web] Add show/hide password toggle on login pages
Adds an eye icon button to the password field on the user, admin, and
domain admin login pages. Clicking it toggles between hidden and visible
password text, using Bootstrap Icons (bi-eye / bi-eye-slash).

Closes #6893
2026-07-14 21:20:58 +02:00
DerLinkman 0f06c8e563 postfix: prepare TLSRPT compatibility for tlspol 2026-07-13 19:34:58 +02:00
DerLinkman fffe1aec3d postfix-tlspol: upgrade to trixie + 1.11.0 update 2026-07-13 19:16:10 +02:00
Chris Haumesser ddcce811c1 Fixes #7150 (Dovecot passwd-verify.lua wipes auth cache during any nginx restart or unavailability)
- distinguishes between text and numeric responses from nginx
  - prevents auth cache from being invalidated due to nginx unavailability
2026-06-12 16:32:22 -06:00
21 changed files with 122 additions and 22 deletions
+3 -3
View File
@@ -1,17 +1,17 @@
FROM golang:1.25-bookworm AS builder
FROM golang:1.26-trixie AS builder
WORKDIR /src
ENV CGO_ENABLED=0 \
GO111MODULE=on \
NOOPT=1 \
VERSION=1.8.22
VERSION=1.11.0
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \
scripts/build.sh build-only
FROM debian:bookworm-slim
FROM debian:trixie-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
@@ -1,4 +1,4 @@
@version: 3.38
@version: 4.8
@include "scl.conf"
options {
chain_hostnames(off);
@@ -7,7 +7,7 @@ options {
dns_cache(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
bad_hostname("^gconfd$");
};
source s_src {
@@ -1,4 +1,4 @@
@version: 3.38
@version: 4.8
@include "scl.conf"
options {
chain_hostnames(off);
@@ -7,7 +7,7 @@ options {
dns_cache(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
bad_hostname("^gconfd$");
};
source s_src {
+10 -1
View File
@@ -32,8 +32,17 @@ function auth_password_verify(request, password)
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
-- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request
-- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this
-- as a backend outage and keep the cache entry, rather than wiping it as a mismatch.
if type(c) ~= "number" then
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
end
if c ~= 200 and c ~= 401 then
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
end
@@ -116,6 +116,7 @@ location ~ \.php$ {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_X_REAL_IP $remote_addr;
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
fastcgi_read_timeout 3600;
+1 -1
View File
@@ -150,7 +150,7 @@ smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
smtp_sasl_security_options =
smtp_sasl_mechanism_filter = plain, login
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERYwithTLSRPT
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
mail_name = Postcow
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
+27 -7
View File
@@ -380,13 +380,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
else {
$state = state_nomatch;
}
$state .= '<br />' . $current[$data_field[$current['type']]];
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]);
}
if ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dmarc') === 0 &&
$record[2] == $dmarc_link) {
$current['txt'] = str_replace(' ', '', $current['txt']);
$state = $current[$data_field[$current['type']]] . state_optional;
$state = htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
}
elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=spf') === 0 &&
@@ -396,7 +396,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
$state = state_good;
}
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
}
elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dkim') === 0 &&
@@ -426,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
if ($state == state_nomatch) {
$state = array();
foreach ($currents as $current) {
$state[] = $current[$data_field[$current['type']]];
$state[] = htmlspecialchars($current[$data_field[$current['type']]]);
}
$state = implode('<br />', $state);
}
@@ -436,12 +436,22 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
<td>%s</td>
<td class="dns-found">%s</td>
<td class="dns-recommended">%s</td>
</tr>', $record[0], $record[1], $record[2], $state);
</tr>', htmlspecialchars($record[0]), htmlspecialchars($record[1]), $record[2], $state);
$record[3] = explode('<br />', $state);
}
unset($record);
// Make a hostname RHS absolute so it is not read relative to $ORIGIN.
// Already-absolute names, the SRV root target "." and IP addresses are left alone.
$absolutize = function($host) {
$host = trim($host);
if ($host === '' || $host === '.' || substr($host, -1) === '.' || filter_var($host, FILTER_VALIDATE_IP)) {
return $host;
}
return $host . '.';
};
$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
foreach ($records as $record) {
if ($domain == substr($record[0], -strlen($domain))) {
@@ -462,22 +472,32 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
$val = str_replace(state_optional, '', $val);
$val = str_replace(state_good, '', $val);
if (strlen($val) > 0) {
// these are all TXT values, their RHS is a character string, not a name
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
}
}
}
else {
if ($record[1] == 'MX' || $record[1] == 'CNAME') {
$val = $absolutize($val);
}
elseif ($record[1] == 'SRV') {
// format here is "target port"; only the target is a name
$parts = explode(' ', $val, 2);
$parts[0] = $absolutize($parts[0]);
$val = implode(' ', $parts);
}
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
}
foreach ($vals as $val) {
$dns_data .= str_replace($domain, $domain . '.', $val);
$dns_data .= $val;
}
}
}
?>
</table>
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=htmlspecialchars($_GET['domain']);?>.txt' type='text/csv'>Download</a>
<script>
var zonefile_dl_link = document.getElementById('download-zonefile');
var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
+23 -2
View File
@@ -591,7 +591,7 @@ function logger($_data = false) {
$type = $return['type'];
$msg = null;
if (isset($return['msg'])) {
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}
$call = null;
if (isset($return['log'])) {
@@ -633,6 +633,18 @@ function logger($_data = false) {
return true;
}
}
function is_local_mailcow_domain($domain) {
// True if domain is a locally managed, active primary or alias domain
global $pdo;
$domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
if (empty($domain)) {
return false;
}
$stmt = $pdo->prepare("SELECT 1 FROM `domain` WHERE `domain` = :d AND `active` = 1
UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d2 AND `active` = 1 LIMIT 1");
$stmt->execute(array(':d' => $domain, ':d2' => $domain));
return (bool)$stmt->fetchColumn();
}
function hasDomainAccess($username, $role, $domain) {
global $pdo;
if (empty($domain) || !is_valid_domain_name($domain)) {
@@ -3551,7 +3563,16 @@ function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redir
if (isset($redirects['unauthenticated'])) {
header('Location: ' . $redirects['unauthenticated']);
} else {
header('Location: /');
// Send a deep link to the login page for its area instead of the user login at /,
// e.g. /admin/dashboard -> /admin rather than /
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
if (strpos($request_uri, '/admin/') === 0) {
header('Location: /admin');
} elseif (strpos($request_uri, '/domainadmin/') === 0) {
header('Location: /domainadmin');
} else {
header('Location: /');
}
}
exit();
}
+25
View File
@@ -750,6 +750,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
$goto_local_part = strstr($goto, '@', true);
$goto = $goto_local_part.'@'.$goto_domain;
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
!is_local_mailcow_domain($goto_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('external_goto_denied', htmlspecialchars($goto))
);
unset($gotos[$i]);
continue;
}
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `kind` REGEXP 'location|thing|group'
AND `username`= :goto");
@@ -2715,6 +2727,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
unset($gotos[$i]);
continue;
}
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
!is_local_mailcow_domain($goto_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('external_goto_denied', htmlspecialchars($goto))
);
unset($gotos[$i]);
continue;
}
if ($goto == $address) {
$_SESSION['return'][] = array(
'type' => 'danger',
+3 -2
View File
@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "19022026_1220";
$db_version = "18082026_1200";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -719,7 +719,8 @@ function init_db_schema()
"alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'"
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'",
"alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
+4
View File
@@ -246,6 +246,10 @@ $PW_RESET_TOKEN_LIMIT = 3;
// Maximum time in minutes a password reset token is valid
$PW_RESET_TOKEN_LIFETIME = 15;
// Globally forbid aliases with external (non-local) goto domains for ALL roles (incl. admins),
// overriding the per-domain-admin da_acl. false = defer to da_acl.
$ALIAS_DISABLE_EXTERNAL_DOMAINS = false;
// UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins
// true = required
// false = preferred
+2
View File
@@ -1169,6 +1169,7 @@ jQuery(function($){
if (item === null) { return true; }
item.user = escapeHtml(item.user);
item.call = escapeHtml(item.call);
if (item.msg) item.msg = escapeHtml(item.msg);
item.task = '<code>' + item.task + '</code>';
item.type = '<span class="badge fs-6 bg-' + item.type + '">' + item.type + '</span>';
});
@@ -1176,6 +1177,7 @@ jQuery(function($){
$.each(data, function (i, item) {
if (item === null) { return true; }
item.username = escapeHtml(item.username);
item.real_rip = escapeHtml(item.real_rip);
item.service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
});
} else if (table == 'general_syslog') {
+2
View File
@@ -1,6 +1,7 @@
{
"acl": {
"alias_domains": "Alias-Domains hinzufügen",
"alias_external_goto": "Aliase mit externen Ziel-Domains erlauben",
"app_passwds": "App-Passwörter verwalten",
"bcc_maps": "BCC-Maps",
"delimiter_action": "Delimiter-Aktionen (tags)",
@@ -443,6 +444,7 @@
"domain_not_found": "Domain %s nicht gefunden",
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
"extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen",
"external_goto_denied": "Externe Ziel-Adresse %s ist nicht erlaubt",
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
+2
View File
@@ -1,6 +1,7 @@
{
"acl": {
"alias_domains": "Add alias domains",
"alias_external_goto": "Allow aliases with external goto domains",
"app_passwds": "Manage app passwords",
"bcc_maps": "BCC maps",
"delimiter_action": "Delimiter action",
@@ -444,6 +445,7 @@
"domain_not_found": "Domain %s not found",
"domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
"extended_sender_acl_denied": "missing ACL to set external sender addresses",
"external_goto_denied": "External goto address %s is not allowed",
"extra_acl_invalid": "External sender address \"%s\" is invalid",
"extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
"fido2_verification_failed": "FIDO2 verification failed: %s",
+6
View File
@@ -4,6 +4,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
if (!isset($_SESSION['mailcow_cc_role'])) {
$_SESSION['oauth2_request'] = $_SERVER['REQUEST_URI'];
header('Location: /?oauth');
exit;
}
$request = OAuth2\Request::createFromGlobals();
@@ -24,6 +25,11 @@ if (!isset($_POST['authorized'])) {
exit;
}
if (empty($_SESSION['mailcow_cc_username'])) {
header('Location: /?oauth');
exit;
}
// print the authorization code if the user has authorized your client
$is_authorized = ($_POST['authorized'] == '1');
$oauth2_server->handleAuthorizeRequest($request, $response, $is_authorized, $_SESSION['mailcow_cc_username']);
+1
View File
@@ -49,6 +49,7 @@
<div class="input-group">
<div class="input-group-text"><i class="bi bi-lock-fill"></i></div>
<input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
<button type="button" class="input-group-text" style="cursor:pointer;" onclick="var i=document.getElementById('pass_user'),ic=this.querySelector('i');if(i.type==='password'){i.type='text';ic.className='bi bi-eye-slash';}else{i.type='password';ic.className='bi bi-eye';}" tabindex="-1" aria-label="Toggle password visibility"><i class="bi bi-eye"></i></button>
</div>
</div>
<div class="d-flex justify-content-between mt-4" style="position: relative">
+4
View File
@@ -473,6 +473,10 @@ function recursiveBase64StrToArrayBuffer(obj) {
window.location.reload();
} else {
var msg = (data && data[0] && data[0].msg) ? data[0].msg : 'Password change failed.';
// the API returns raw language keys, resolve them like the alert box does
if (Object.prototype.hasOwnProperty.call(lang_danger, msg)) {
msg = lang_danger[msg];
}
$('#changePWAlert').show().text(msg);
}
},
@@ -49,6 +49,7 @@
<div class="input-group">
<div class="input-group-text"><i class="bi bi-lock-fill"></i></div>
<input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
<button type="button" class="input-group-text" style="cursor:pointer;" onclick="var i=document.getElementById('pass_user'),ic=this.querySelector('i');if(i.type==='password'){i.type='text';ic.className='bi bi-eye-slash';}else{i.type='password';ic.className='bi bi-eye';}" tabindex="-1" aria-label="Toggle password visibility"><i class="bi bi-eye"></i></button>
</div>
</div>
<div class="d-flex justify-content-between mt-4" style="position: relative">
+1 -1
View File
@@ -267,6 +267,7 @@
<small class="text-muted">{{ lang.admin.password_reset_info }}</small>
</div>
</div>
{% endif %}
<div data-acl="{{ acl.extend_sender_acl }}" class="row mb-4">
<label class="control-label col-sm-2" for="extended_sender_acl">{{ lang.edit.extended_sender_acl }}</label>
<div class="col-sm-10">
@@ -279,7 +280,6 @@
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<label class="control-label col-sm-2" for="protocol_access">{{ lang.edit.allowed_protocols }}</label>
<div class="col-sm-10">
+1
View File
@@ -56,6 +56,7 @@
<div class="input-group">
<div class="input-group-text"><i class="bi bi-lock-fill"></i></div>
<input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
<button type="button" class="input-group-text" style="cursor:pointer;" onclick="var i=document.getElementById('pass_user'),ic=this.querySelector('i');if(i.type==='password'){i.type='text';ic.className='bi bi-eye-slash';}else{i.type='password';ic.className='bi bi-eye';}" tabindex="-1" aria-label="Toggle password visibility"><i class="bi bi-eye"></i></button>
</div>
</div>
<div class="mt-2 text-muted" style="font-size: 0.9rem;">
+1 -1
View File
@@ -382,7 +382,7 @@ services:
- postfix
postfix-tlspol-mailcow:
image: ghcr.io/mailcow/postfix-tlspol:1.8.23
image: ghcr.io/mailcow/postfix-tlspol:1.11.0
depends_on:
unbound-mailcow:
condition: service_healthy